1 volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后 那么就具备了两层语义:
1)保证了不同线程对共享变量进行操作时的可见性
即一个线程修改了共享变量的值,那么修改后的值对其他线程来说是立即可见的
2)禁止进行指令重排序

  1. public class TestVolatile extends Thread{
  2. private boolean flag = false; //注意这里
  3. //private volatile boolean flag = false; 若改成这个就没问题了
  4. public void setStop(boolean flag) {
  5. this.flag = flag;
  6. }
  7. @Override
  8. public void run() {
  9. String name = Thread.currentThread().getName();
  10. System.out.println(name + "==执行==");
  11. while (true) {
  12. if (flag) {
  13. System.out.println(name + "==退出==");
  14. break;
  15. }
  16. }
  17. System.out.println(name + "==线程结束==");
  18. }
  19. public static void main(String[] args) throws Exception {
  20. TestVolatile tv = new TestVolatile();
  21. tv.start();
  22. Thread.sleep(3000);
  23. tv.setStop(true);
  24. //在主线程方法里修改了flag的值
  25. //但是另一个线程还一直在死循环中 看不到主线程这里修改的值
  26. System.out.println("main finish");
  27. }
  28. }

不加volatile的运行结果:
![7FZ]KEJOIC(SPYJX4%EFMB.png
tv线程在运行的时 会flag变量的值拷贝一份放在自己的工作内存当中
当主线程线程更改了flag变量的值之后 但是还没来得及写入主存当中
主线程转去做其他事情了 那么tv线程由于不知道主线程对flag变量的更改 因此还会一直循环下去

但是用volatile修饰之后就变得不一样了:
HL~5D$~I3KCGK%2F6]J(T)B.png
第一:使用volatile关键字会强制将修改的值立即写入主存
第二:使用volatile关键字的话 当线程A进行修改时 会导致线程B的工作内存中缓存变量的缓存行无效
(反映到硬件层的话 就是CPU的L1或者L2缓存中对应的缓存行无效)
比如这里 主线程修改了flag的值 会导致tv线程的工作内存中缓存变量的缓存行无效
所以tv线程再次读取变量flag的值时会去主存读取
那么tv线程读取到的就是最新的正确的值

2 volatile保证原子性吗?

volatile无法保证对变量的操作的原子性

比如自增操作是不具备原子性的,它包括从主存中读取值复制到工作内存中、在工作内存中对变量副本进行加1操作、写回主存
那么就是说自增操作的三个子操作可能会分割开执行 就有可能导致下面这种情况出现:
假如某个时刻变量 i 的值为1
1)线程A对变量进行自增操作 线程A读取了变量的值 然后此时线程A被阻塞了
2)然后线程B对变量也进行自增操作
由于线程A只是对变量 i 进行读取操作 而没有对变量进行修改操作 所以不会导致线程B的工作内存中缓存变量 i 的缓存行无效
那么线程B会直接去主存读取 i 的值放入线程内存中 i 的值为1
然后进行加1操作 然后将值写入主存
3)线程B将2写回主存 不会把线程A的缓存行设为无效吗?
只有在做读取操作时 发现自己缓存行无效 才会去读主存的值
而线程A的读取操作在线程B写入之前已经做过了 所以这里线程A可以继续做自增了
所以线程A对 i 进行加1操作后 i 的值为2 最后将值写回主存
如上两个线程分别进行了一次自增操作后 i 只增加了1

所以若想保证原子性 可以:
1)采用synchronized
2)采用Lock
3)采用AtomicInteger
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类 即对基本数据类型的 自增(加1操作)、自减(减1操作)、以及加法操作(加一个数)、减法操作(减一个数)进行了封装 保证这些操作是原子性操作
atomic是利用CAS来实现原子性操作的(Compare And Swap)CAS实际上是利用处理器提供的CMPXCHG指令实现的 而处理器执行CMPXCHG指令是一个原子性操作

3 volatile能保证有序性吗?

volatile关键字能禁止指令重排序 所以volatile能在一定程度上保证有序性
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时 在其前面的操作的更改肯定全部已经进行
且结果已经对后面的操作可见 在其后面的操作肯定还没有进行
2)在进行指令优化时 不能将在对volatile变量访问的语句放在其后面执行
也不能把volatile变量后面的语句放到其前面执行
举个简单的例子:

  1. x = 2; //语句1
  2. y = 0; //语句2
  3. volatile flag = true; //语句3
  4. x = 4; //语句4
  5. y = -1; //语句5

由于flag变量为volatile变量 那么在进行指令重排序的过程的时候 不会将语句3放到语句1、语句2前面
也不会讲语句3放到语句4、语句5后面
但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的
并且volatile关键字能保证 执行到语句3时 语句1和语句2必定是执行完毕了的
且语句1和语句2的执行结果对语句3、语句4、语句5是可见的
指令重排在单例模式中的影响 基于双重检验的单例模式(懒汉型)

  1. public class Singleton {
  2. private static Singleton instance; //注意这里
  3. //private static volatile Singleton instance;
  4. private Singleton() {}
  5. public static Singleton getInstance() {
  6. if (instance == null) {
  7. //先判断是否为空 不为空先加锁
  8. synchronized(Singleton.class) {
  9. //加锁时有可能别的线程已经创建好对象了 所以加锁完后还需要再判断一次是否为空
  10. if (instance == null)
  11. instance = new Singleton();// 非原子操作
  12. }
  13. }
  14. return instance;
  15. }
  16. }

instance= new Singleton()并不是一个原子操作 其实际上可以抽象为下面几条JVM指令:
1)分配内存空间
2)初始化对象(需要用到1中分配的空间)
3)设置instance指向分配的内存地址
所以上述三条指令也可重排序为:
1)分配内存空间
2)设置instance指向分配的内存地址
3)初始化对象(需要用到1中分配的空间)
为什么这里用了synchronized还要用volatile?
具体来说就是synchronized虽然保证了原子性 但却没保证指令重排序的正确性 且**程序可能是在多核CPU上执行**
比如线程A执行的是重排序后的指令 所以A线程的instance还没有造出来 但已经被赋值了(即先分配内存空间后instance指向分配的内存地址)
而B线程这时过来了(发现instance不为null)错以为instance已经被实例化出来,一用才发现instance尚未被初始化,这就造成了空指针

volatile底层原理

下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现 加入volatile关键字时 会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏)内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置 也不会把前面的指令排到内存屏障的后面
即在执行到内存屏障这句指令时 在它前面的操作已经全部完成
2)它会强制将对缓存的修改操作立即写入主存
3)如果是写操作 它会导致其他CPU中对应的缓存行无效