浅析 Java 内存模型 二

用户头像
朱华
关注
发布于: 2020 年 10 月 11 日

先行发生原则(Happens-Before)

先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,比如说操作 A 先行发生于操作 B,其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。



单线程规则

一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。



锁操作(掌握)

一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这 里必须强调的是“同一个锁”,而“后面”是指时间上的先后。



volatile 变量规则

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。



线程启动

Thread 对象的 start() 方法先行发生于此线程的每一个动作。



线程 join

线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread::join() 方法是否结束、Thread::isAlive() 的返回值等手段检测线程是否已经终止执行。





传递性

如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。



中断

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted() 方法检测到是否有中断发生。



构造方法

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。



工具类的 Happens-Before 原则



  1. 线程安全的容器 get 一定能看到在此之前的 put 等存入动作

  2. CountDownLatch

  3. Semaphore

  4. Future

  5. 线程池

  6. CyclicBarrier



Happens-before 实例演示



public class FieldVisibility {
int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}



在上一小节中讲到有第四种情况(低概率)的发生:没给 b 加 volatile,那么有可能出现 `a = 1, b = 3`。因为 a 虽然被修改了,但是其他线程不可见,而 b 恰好其他线程可见,这就造成了 `a = 1, b = 3`。



若是给 b 加了 volatile,不仅 b 被影响,也可以实现轻量级同步,这就是所谓的“近朱者赤”。

b 之前的写入(对应 `b = a`)对读取 b 后的代码(print b)都可见,所以在 writerThread 里对 a 的赋值,一定会对 readerThread 里的读取可见,所以这里的 a 即使不加 volatile,只要读到的是 3,就可以有 happens-before 原则保证了读取到的都是 3 而不可能读取到 1。



原子性



什么是原子性



也就是一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。



Java 中的原子操作有哪些?



  • 除 long 和 double 之外的基本数据类型(int、byte、boolean、short、char、float)的赋值操作。

  • 所有引用(reference) 的赋值操作,不管是 32 位的机器还是 64 位的机器。

  • java.concurrent.Atomic.* 包下所有类的原子操作。



long 和 double 的原子性



  • 结论:在 32 位上的 JVM 上,long 和 double 的操作不是原子的,但是在 64 位的 JVM 上是原子的

  • 在实际中,在商用 JVM 中不会出现



原子操作 + 原子操作 != 原子操作

简单地把原子操作组合在一起,并不能保证整体依然具有原子性。



发布于: 2020 年 10 月 11 日 阅读数: 15
用户头像

朱华

关注

见自己,见天地,见众生。 2018.08.07 加入

还未添加个人简介

评论

发布
暂无评论
浅析 Java 内存模型 二