多线程下变量的不可见性
- 多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量后,另一个线程不能立即获取修改后的最新值。
- 解决方案:
- 加锁:使用synchronized关键字或者使用JDK的Lock
- 使用volatile关键字
- volatile解决共享变量不可见问题的过程:
- 线程2将共享变量从主内存读取到数据放到其对应的工作内存
- 线程2在工作内存中更改了共享变量副本的值,使用volatile关键字会强制将修改的值立即写入主存
- 使用volatile关键字的话,当线程2将共享变量从工作内存中写回主内存后,其他线程工作内存中的共享变量副本无效(CPU主线嗅探机制)(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
- 线程1再次读取共享变量时需要从主内存中读取最新的值放到自己的工作内存中
volatile特性
- volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。
- 可以保证可见性
- 不保证原子性
- 如何保证volatile变量的原子性:
- 使用加锁机制
- 使用Atomic类
- 禁止指令重排序,保证有序性
- volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题
- volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
重排序导致的问题
- 指令重排序导致多个线程操作之间的不可见性
happens-before
happens-before:前一个操作的结果可以被后续的操作获取
happens-before 规则
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 线程1写入了volatile变量,然后线程2读取该变量,那么线程1写入volatile遍历之前的写操作对线程2可见(线程1和线程2可以是同一个线程)
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
- 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
volatile 重排序规则(volatile可以禁止指令重排序)
- 写volatile变量是,无论前一个操作是什么,都不能重排序
- 读volatile变量时,无论后一个操作是什么,都不能重排序
- 先写volatile变量,后读volatile变量是,不能重排序
volatile原理和实现机制——内存屏障
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile与synchronized的区别
- volatile只能修饰实例变量和类变量,synchronized修饰的是代码块、方法
- volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全),而synchronized是一种排他(互斥)的机制,集保证原子性也保证可见性
- volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题
- volatile可以看作是轻量版synchronized,volatile不保证原子性,但是如果对一个共享变量进行赋值,而没有其他操作,那么可以用volatile代替synchronized,因为赋值本身就是原子性的,而volatile又保证了可见性,所以可以保证线程安全。