目录
3.4.3、double-checked locking问题
JMM(Java Memory Model),它定义了主存、工作内存等抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面:
先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止。
- public class Test1 {
- private static final Logger LOGGER = LoggerFactory.getLogger(Test1.class);
-
- static boolean run = true;
-
- public static void main(String[] args) throws InterruptedException {
- Thread t = new Thread(()->{
- while (run) {
- // ...
- }
- });
- t.start();
-
- Thread.sleep(1000);
- LOGGER.debug("停止t");
- run = false; // 线程t不会如预想的停下来
- }
- }
-
- // 结果:
- 19:35:44.615 [main] DEBUG com.multiThreads.test13.Test1 - 停止t
- (并没有终止)
为什么呢?分析一下:



volatile(易变关键字)。
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
- public class Test1 {
- private static final Logger LOGGER = LoggerFactory.getLogger(Test1.class);
-
- // 方式1:易变关键字
- static volatile boolean run = true;
-
- // 方式2:锁对象
- final static Object lock = new Object();
-
- public static void main(String[] args) throws InterruptedException {
- Thread t = new Thread(()->{
- while (run) {
- // ...
- }
- });
- t.start();
-
- Thread.sleep(1000);
- LOGGER.debug("停止t");
- run = false;
- }
- }
-
- 结果:
- 19:35:44.615 [main] DEBUG com.multiThreads.test13.Test1 - 停止t
-
- Process finished with exit code 0
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。volatile只能保证看到最新值,不能解决指令交错。
注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级曹总,性能相对更低。
两阶段终止(Two Termination):在一个线程T1中如何“优雅”终止线程T2?这里的【优雅】指的是给T2一个料理后事的机会。
1、错误思路
2、正确思路
-
-
- public class Test {
- private static final Logger LOGGER = LoggerFactory.getLogger(Test.class);
-
- public static void main(String[] args) throws InterruptedException {
- TwoPhaseTermination tpt = new TwoPhaseTermination();
- tpt.start();
-
- Thread.sleep(3500);
- LOGGER.debug("停止监控");
- tpt.stop();
- }
- }
-
- class TwoPhaseTermination {
- private static final Logger LOGGER = LoggerFactory.getLogger(TwoPhaseTermination.class);
-
- // 监控线程
- private Thread monitorThread;
- // 是否停止
- private volatile boolean isStop;
-
- // 启动监控线程
- public void start() {
- monitorThread = new Thread(()->{
- while(true) {
- Thread currentThread = Thread.currentThread();
- // 是否被打断
- if (isStop) {
- LOGGER.debug("料理后事");
- break;
- }
- try {
- Thread.sleep(1000);
- LOGGER.debug("执行监控记录");
- } catch (InterruptedException e) {
- // 该异常会清空打断标记(isInterrupt = false)
- e.printStackTrace();
- }
- }
- }, "monitor");
- monitorThread.start();
- }
-
- // 停止监控线程
- public void stop() {
- isStop = true;
- monitorThread.interrupt();
- }
- }
-
- 结果:
- 10:53:31.821 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 执行监控记录
- 10:53:32.825 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 执行监控记录
- 10:53:33.839 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 执行监控记录
- 10:53:34.324 [main] DEBUG com.multiThreads.test14.Test - 停止监控
- 10:53:34.324 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 料理后事
- java.lang.InterruptedException: sleep interrupted
- at java.lang.Thread.sleep(Native Method)
- at com.multiThreads.test14.TwoPhaseTermination.lambda$start$0(Test.java:38)
- at java.lang.Thread.run(Thread.java:748)
-
- [点击并拖拽以移动]
-
Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。
例如:
- public class Test {
- private static final Logger LOGGER = LoggerFactory.getLogger(Test.class);
-
- public static void main(String[] args) throws InterruptedException {
- TwoPhaseTermination tpt = new TwoPhaseTermination();
- tpt.start();
- tpt.start();
- tpt.start();
-
- Thread.sleep(3500);
- LOGGER.debug("停止监控");
- tpt.stop();
- }
- }
-
- class TwoPhaseTermination {
- private static final Logger LOGGER = LoggerFactory.getLogger(TwoPhaseTermination.class);
-
- // 监控线程
- private Thread monitorThread;
- // 是否停止
- private volatile boolean isStop = false;
-
- // 判断是否执行过start()方法
- private boolean starting = false;
-
- // 启动监控线程
- public void start() {
- synchronized (this) {
- if (starting) {
- return;
- }
- starting = true;
- }
- monitorThread = new Thread(() -> {
- while (true) {
- Thread currentThread = Thread.currentThread();
- // 是否被打断
- if (isStop) {
- LOGGER.debug("料理后事");
- break;
- }
- try {
- Thread.sleep(1000);
- LOGGER.debug("执行监控记录");
- } catch (InterruptedException e) {
- // 该异常会清空打断标记(isInterrupt = false)
- e.printStackTrace();
- }
- }
- }, "monitor");
- monitorThread.start();
- }
-
- // 停止监控线程
- public void stop() {
- isStop = true;
- monitorThread.interrupt();
- }
- }
-
- 结果:
- 11:09:35.158 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 执行监控记录
- 11:09:36.161 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 执行监控记录
- 11:09:37.163 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 执行监控记录
- 11:09:37.665 [main] DEBUG com.multiThreads.test14.Test - 停止监控
- 11:09:37.665 [monitor] DEBUG com.multiThreads.test14.TwoPhaseTermination - 料理后事
- java.lang.InterruptedException: sleep interrupted
- at java.lang.Thread.sleep(Native Method)
- at com.multiThreads.test14.TwoPhaseTermination.lambda$start$0(Test.java:49)
- at java.lang.Thread.run(Thread.java:748)
-
- Process finished with exit code 0
JVM会在不影响正确性的前提下,可以调整语句的执行顺序。思考下面一段代码:
- static int i;
- static int j;
-
- // 在某个线程内执行如下赋值操作
- i = ...;
- j = ...;
可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是:
- i = ...;
- j = ...;
也可以是:
- j = ...;
- i = ...;
这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性。
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。为什么这么做呢?可以想到指令还可以再划分为一个个更小的阶段,例如,每条指令都可以分为:取指令-指令译码-执行指令-内存访问-数据写回这5个阶段。

术语参考:
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在80年代到90年代中叶占据了计算机架构的重要地位。
提示:分阶段、分工是提升效率的关键!
现代CPU支持多级指令流水线,例如支持同时执行 取指令-指令译码-执行指令-内存访问-数据写回 的处理器,就可以称之为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行这5条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC=1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令的吞吐率。
提示:奔腾四(Pentium 4)支持高达35级流水线,但由于功耗太高被废弃。
- int num = 0;
- boolean ready = false;
-
- // 线程1执行此方法
- public void actor1(I_Result r) {
- if (ready) {
- r.r1 = num + num;
- } else {
- r.r1 = 1;
- }
- }
-
- // 线程2执行此方法
- public void actor2(I_Result r) {
- num = 2;
- ready = true;
- }
I_Result是一个对象,有一个属性r1用来保存结果,问,可能的结果有几种?
有同学这么分析:
实际上结果还有可能是0,这种情况下:线程2执行ready=true,切换到线程1,进入if分支,相加为0,再切回线程2执行num=2。
这种现象就是指令重排,是JIT编译器在运行时的一些优化,这个现象需要通过大量测试才能复现。
volatile的底层实现原理是内存屏障,Memory Barrier (Memory Fence)。
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中:
- public void actor2(I_Result r) {
- num = 2;
- ready = true; // ready 是 volatile赋值带写屏障
- // 写屏障
- }
而读屏障(ifence)保证在读屏障之后,对共享变量的读取,加载的是主存中最新数据:
- public void actor1(I_Result r) {
- // 读屏障
- // ready 是 volatile 读取值带读屏障
- if (ready) {
- r.r1 = num + num;
- } else {
- r.r1 = 1;
- }
- }
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后:
- public void actor2(I_Result r) {
- num = 2;
- ready = true; // ready 是 volatile赋值带写屏障
- // 写屏障
- }
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前:
- public void actor1(I_Result r) {
- // 读屏障
- // ready 是 volatile 读取值带读屏障
- if (ready) {
- r.r1 = num + num;
- } else {
- r.r1 = 1;
- }
- }
还是那句话,不能解决指令交错:
以著名的double-checked locking单例模式为例:
- class Singleton {
- private Singleton() {}
-
- private static Singleton INSTANCE = null;
-
- public static synchronized Singleton getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new Singleton();
- }
- return INSTANCE;
- }
- }
以上的实现特点是:
但在多线程环境下,上面的代码是有问题的。
happens-before规定了对共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见。
- public class Test1 {
- static int x;
- static Object m = new Object();
-
- public static void main(String[] args) {
- new Thread(()->{
- synchronized (m) {
- x = 10;
- }
- },"t1").start();
- new Thread(()->{
- synchronized (m) {
- System.out.println(x);
- }
- },"t2").start();
- }
- }
-
- 结果:10
- public class Test1 {
- volatile static int x;
-
- public static void main(String[] args) {
- new Thread(()->{
- x = 10;
- },"t1").start();
-
- new Thread(()->{
- System.out.println(x);
- },"t2").start();
- }
- }
-
- 结果:10
- public class Test1 {
- static int x;
-
- public static void main(String[] args) {
- x = 10;
-
- new Thread(()->{
- System.out.println(x);
- },"t2").start();
- }
- }
- public class Test1 {
- static int x;
-
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() -> {
- x = 10;
- }, "t1");
- t1.start();
-
- t1.join();
- System.out.println(x);
- }
- }
-
- 结果:10
- public class Test1 {
- static int x;
-
- public static void main(String[] args) throws InterruptedException {
- Thread t2 = new Thread(()->{
- while(true) {
- if (Thread.currentThread().isInterrupted()) {
- System.out.println(x);
- break;
- }
- }
- }, "t2");
- t2.start();
-
- new Thread(()->{
- try {
- Thread.sleep(1000);
- x = 10;
- t2.interrupt();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }, "t1").start();
-
- while(!t2.isInterrupted()) {
- Thread.yield();
- }
- System.out.println(x);
- }
- }
-
- 结果:
- 10
- 10
- public class Test2 {
- volatile static int x;
- static int y;
-
- public static void main(String[] args) {
- new Thread(()->{
- y = 10;
- x = 20;
- }).start();
-
- new Thread(()->{
- // x=20对t2可见,同时y=10也对t2可见
- System.out.println(x);
- }, "t2").start();
- }
- }