您的位置:首页 > 移动开发

由一道题了解Happens - Before

2017-09-07 20:32 525 查看
首先来看一道题目:

public class Test1 {
private int a=1, b=2;

public void foo(){  // 线程1
a=3;
b=4;
}

public int getA(){ // 线程2
return a;
}
public int getB(){ // 线程2
return b;
}
}


上面的代码,当线程1执行foo方法的时候,线程2访问getA和getB会得到什么样的结果?

答案:

A:a=1, b=2  // 都未改变
B:a=3, b=4  // 都改变了
C:a=3, b=2  //  a改变了,b未改变
D:a=1, b=4  //  b改变了,a未改变


上面的A,B,C都好理解,但是D可能会出乎一些人的预料。一些不了解JMM的同学可能会问怎么可能 b=4语句会先于 a=3 执行?

这是一个多线程之间内存可见性(Visibility)顺序不一致的问题。有两种可能会造成上面的D选项。

1) Java编译器的重排序(Reording)操作有可能导致执行顺序和代码顺序不一致。

关于Reording:

Java语言规范规定了JVM要维护内部线程类似顺序化语义(within-thread as-is-serial semantics):只要程序的最终结果等同于它在严格的顺序化环境中执行的结果,那么上述所有的行为都是允许的。

详细可以看另一篇博文:详解volatile

2) 从线程工作内存写回主存时顺序无法保证。

下图描述了JVM中主存和线程工作内存之间的交互:



先简单认为线程在修改一个变量时,先拷贝入线程工作内存中,在线程工作内存修改后再写回主存(Main Memery)中。

假设例子中Reording后顺序仍与代码中的顺序一致,那么接下来呢?

有意思的事情就发生在线程把Working Copy Memery中的变量写回Main Memery的时刻。

线程1把变量写回Main Memery的过程对线程2的可见性顺序也是无法保证的。

上面的列子,a=3; b=4; 这两个语句在 Working Copy Memery中执行后,写回主存的过程对于线程2来说同样可能出现先b=4;后a=3;这样的相反顺序。

正因为上面的那些问题,JMM中一个重要问题就是:如何让多线程之间,对象的状态对于各线程的“可视性”是顺序一致的。

它的解决方式就是 Happens-before 规则:

JMM为所有程序内部动作定义了一个偏序关系,叫做happens-before。要想保证执行动作B的线程看到动作A的结果(无论A和B是否发生在同一个线程中),A和B之间就必须满足happens-before关系。

Happens-Before规则

其意思就是说,如果操作A先行发生于操作B,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。

线程A中执行如下操作:i=1
线程B中执行如下操作:j=i
线程C中执行如下操作:i=2


假设线程A中的操作”i=1“ happen—before线程B中的操作“j=i”,那么就可以保证在线程B的操作执行后,变量j的值一定为1,即线程B观察到了线程A中操作“i=1”所产生的影响;现在,我们依然保持线程A和线程B之间的happen—before关系,同时线程C出现在了线程A和线程B的操作之间,但是C与B并没有happen—before关系,那么j的值就不确定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B就存在读取到不是最新数据的风险,不具备线程安全性。

我们现在来看一下“Happens-before”规则都有哪些

① 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。

② 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。

③ volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。

④ 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。

⑤ 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。

⑥ 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。

⑦ 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。

⑧ 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C

我们重点关注的是②,③,这两条也是我们通常编程中常用的。

分析ConcurrenHashMap时也会看到使用到锁(ReentrantLock),Volatile,final等手段来保证happens-before规则的。

ConcurrentHashMap

使用锁方式实现“Happens-before”是最简单,容易理解的。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  线程