深入理解volatile关键字
CPU Cache 和 Java 内存模型
volatile关键字只能修饰类变量和实例变量,对于方法参数、局部变量以及实例常量、类常量都不能进行修饰。
CPU Cache 模型
三级缓存
CPU Cache又是由很多个Cache Line 构成,Cache Line 可以认为是 CPU Cache中的最小缓存单位,大小64字节
CPU 缓存一致性问题
缓存不一致性问题的主流解决方法:
- 总线加锁
- 缓存一致性协议
MESI协议:
当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是说在其他的CPU Cache中也存在一个副本,那么进行如下操作: - 读取操作,不做任何处理,只是将Cache中的数据读取到寄存器
- 写入操作,发出信号通知其他CPU将该变量的Cache line置为无效状态,其他CPU在进行该变量读取的时候不得不到主内存中再次获取
Java 内存模型
Java的内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主内存之间的抽象关系
- 共享变量存储于主内存中,每个线程都可以访问
- 每个线程都有私有的工作内存或者称为本地内存
- 工作内存只存储该线程对共享变量的副本
- 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
- 工作内存和Java内存模型一样也是一个抽象的概念,它其实并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等。
并发编程的特性
- 原子性
- 有序性
- 可见性
原子性
在一次的操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行
- 多个原子性的操作在一起就不再是原子性操作了
- 简单的读取与赋值操作是原子性的,将一个变量赋给另一个变量的操作不是原子性的
- Java内存模型(JMM)只保证了基本读取和赋值的原子性操作,其他的均不保证
synchronized
和 JUC
中的lock
可以使代码具有原子性,volatile
不行
想要int
等类型的自增操作具备原子性,可以使用JUC包下的原子封装类java.util.concurrent.atomic.*
可见性
当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值
三种方式保证可见性:
-
synchronized
关键字
确保同一时间只有一个线程能获得锁然后执行同步方法,确保在锁释放之前,会将对变量的修改刷新到主内存中 -
JUC
中的显式锁Lock
确保同一时间只有一个线程能获得锁然后执行同步方法,确保在锁释放之前,会将对变量的修改刷新到主内存中 -
volatile
关键字
当一个变量被volatile
关键字修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然要修改工作内存,但是修改结束后会立刻将其刷新到主内存中。
有序性
程序代码在执行过程中的先后顺序
指令重排序
Happens-before原则:
- 程序次序规则
- 锁定规则
-
volatile
变量规则 - 传递规则
- 线程启动规则
- 线程中断规则
- 线程终结规则
- 对象终结规则
volatile关键字
语义
- 保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改
volatile
修饰的变量,另一个线程会立即看到最新的值 - 禁止对指令进行重排序
实现
其实被volatile
修饰的变量存在一个 lock;
前缀,这个前缀实际上相当于是一个内存屏障,该内存屏障会为指令的执行提供如下几个保障:
- 确保指令重排序时不会将其后面的代码排到内存屏障之前
- 确保指令重排序时不会将其前面的代码排到内存屏障之后
- 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成
- 强制将线程工作内存中值的修改刷新至主内存中
- 如果是写操作,则会导致其他线程的工作内存(CPU Cache)中的缓存数据失效
使用场景
- 开关控制
可见性的特点 - 状态标记
顺序性特点 -
Singleton
设计模式的 double-check
顺序性特点
volatile
和 synchronized
- 使用上的区别
-
volatile
只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量等 -
synchronized
关键字不能用于对变量的修饰,只能修饰方法或语句块 -
volatile
修饰的变量可以为null
,synchronized
关键字同步语句块的monitor
对象不能为null
-
- 对原子性的保证
-
volatile
无法保证原子性 -
synchronized
是一种排他的机制,因此被synchronized
关键字修饰的同步代码使无法被中途打断的,因此能保证代码的原子性
-
- 对可见性的保证
- 两者都可以保证共享资源在多线程间的可见性,但是实现机制完全不同
-
synchronized
借助于JVM指令monitor enter
和monitor exit
对通过排他的方式使得同步代码串行化,在monitor exit
时所有共享资源都将会被刷新到主内存中 -
volatile
使用机器指令(偏硬件)lock;
的方式迫使其他线程工作内存中的数据失效,不得不到主内存中进行再次加载
- 对有序性的保证
-
synchronized
关键字所修饰的同步方法可以保证顺序性,但这种顺序性是以程序的串行化执行换来的,在synchronized
修饰的代码块中代码指令也会发生指令重排序的情况 -
volatile
关键字禁止JVM编译器以及处理器对其进行重排序,所以它能保证有序性
-
-
volatile
不会使线程陷入阻塞 -
synchronized
会使线程进入阻塞状态