Java并发编程-解决可见性与有序性问题

从可见性与有序性问题的原因着手

导致可见性问题的原因是缓存,导致有序性问题的原因是编译优化,那么解决二者的最直接方法就是禁用缓存和编译优化。但是这样程序的性能将会受到很大程度降低。

这里较为合理的方案是按需禁用缓存和编译优化。Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体包括:volatilesynchronizedfinal关键字和Happens-Before规则。

volatile关键字

“当变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。---《Java并发编程实战》”

当将一个变量声明为volatile类型,则表明对于这个变量的读写不使用CPU缓存,必须从内存中读取或写入。

从对volatile的描述可以看出,被volatile修饰的变量不写入缓存,且不参与重排序,这就解决了可见性与有序性的问题,但这是否保证了线程安全呢?答案是否定的,volatile无法保证原子性--引起并发过程中Bug的另一个源头。关于原子性的保证,将在后面进行讨论。

Happens-Before规则

"In computer science, the happened-before relation (denoted: → ) is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow)." ---wikipedia

"In Java specifically, a happens-before relationship is a guarantee that memory written to by statement A is visible to statement B, that is, that statement A completes its write before statement B starts its read." ---wikipedia

Happens-Before规则所描述的是:先后发生的两个操作,其结果必须也体现操作的先后顺序,即:前一个操作的结果对后续操作是可见的。Happens-Before规则具体有以下六项:

1.程序的顺序性规则

如以下代码,按照程序的执行顺序,"x = 12;" Happens-Before 于 "v = true;",对于x的赋值一定发生在对v赋值之前,即对x的操作对后续操作可见。

class VolatileExample {
    int x = 0;
    volatile boolean v = false;
    public void writer() {
        x = 12;
        v = true;
    }
    public void reader() {
        if(v == true){
            //x=?
        }
    }
}
复制代码

2.volatile变量规则

规则2代表针对volatile变量的写操作,Happens-Before 于后续对这个变量的读操作。

3.传递性

规则3表示:若 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。 正如上面的示例代码,假设线程A先调用writer()方法,线程B之后调用reader()方法,则:

  • 1.根据规则1,"x = 12" Happens-Before "v = true";
  • 2.根据规则2,写变量 "v = true" Happens-Before 于读变量 "v = true";
  • 3.再根据规则3,"x = 12" Happens-Before 读变量 "v = true"。

这就意味着,线程A对x的写操作对于线程B对x的读操作是可见的,则线程B读取的x为12。

4.管程中锁的规则

规则4是指一个锁的解锁操作 Happens-Before 于后续对这个锁的加锁。

管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现。

结合规则3,获得锁的线程在解锁之前的操作 Happens-Before 解锁操作,加上规则4的内容,则上述操作 Happens-Before 后续的加锁操作,也就是该操作对后面获得锁的线程来说是可见的。

5.线程start()规则

规则5指主线程A启动子线程B后,子线程B能够看到A在启动子线程B之前的操作。

也就是线程A调用线程B的start()方法,那么该start()操作 Happens-Before 于线程B中的任意操作。

Thread B = new Thread() {
    //主线程调用B.strat()之前所有对共享变量的修改
    //此处皆可见,var为20
}
//对共享变量赋值
var = 20;
//主线程启动子线程
B.start();
复制代码

6.线程join()规则

猜你喜欢

转载自juejin.im/post/5ca088e4518825550b35bdb4