记录一下,我前一段时间面试被问到比较多的一个问题--volatile关键字
一、volatile特性:
volatile是Java虚拟机提供的轻量级的同步机制。主要有三大特性:
-
保证可见性
-
不保证原子性
-
禁止指令重排序
1、保证可见性
举个例子,比如说有两个线程,他们的代码里都需要读取data这个变量的值,那么他们都会从主内存里加载data变量的值到自己的工作内存,然后才可以使用那个值,这样的话,每个线程拿到的data都是0。
这样,在线程代码运行的过程中,对data的值都可以直接从工作内存里加载了,不需要再从主内存里加载了。
为什么让线程每次都从主内存加载变量的值?
因为线程运行的代码对应的是一些指令,是由CPU执行的!但是CPU每次执行指令运算的时候,也就是执行我们写的代码的时候,要是每次需要一个变量的值,都从主内存加载,性能会比较差!
根据上面的图,可能看到一个问题:
假如说线程1修改了data变量的值为1,然后将这个修改写入自己的本地工作内存。那么此时,线程1的工作内存里的data值为1。然而,主内存里的data值还是为0,线程2的工作内存里的data值还是0啊!
这就导致,线程1和线程2其实都是在操作一个变量data,但是线程1修改了data变量的值之后,线程2是看不到的,一直都是看到自己本地工作内存中的一个旧的副本的值!
这就是所谓的java并发编程中的可见性问题。
多个线程并发读写一个共享变量的时候,有可能某个线程修改了变量的值,但是其他线程看不到,也就是对其他线程不可见
Java就是利用volatile来提供可见性的。当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。
其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
2、不保证原子性
Java中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行。比如:
i = 2; j = i; i++; i = i + 1;
上面4个操作中,i=2是读取操作,必定是原子性操作,j=i你以为是原子性操作,其实吧,分为两步,一是读取i的值,然后再赋值给j,这就是2步操作了,称不上原子操作,i++和i = i + 1其实是等效的,读取i的值,加1,再写回主存,那就是3步操作了。所以上面的举例中,最后的值可能出现多种情况,就是因为满足不了原子性。
volatile为什么不保证原子性?
举个例子:i++在JMM中分为三个指令:
- 从主内存中拿到原始值
- 在线程工作内存中进行加1操作
- 把累加后的值写回主内存
如果第二个线程在第一个线程读取旧值和写回新值期间读取i的值,
那么第二个线程就会与第一个线程看到同一个值,并执行相同值的加1操作,这也就造成了线程安全问题。
原子性问题,得依赖Synchronized、ReentrantLock等加锁机制来解决。
3、禁止指令重排序
int a=10; //1
int b=20; //2
int c= a+b; //3
以上这段简单的代码,理想情况下它的执行顺序是:1>2>3
。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3
。这就叫做指令重排。
但是不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的
但是在一些获取状态或者初始化等情况下,就有可能出现问题。
所以加上 volatile
之后可以防止这样的重排优化,保证业务的正确性。
volatile的写-读与锁的释放-获取有相同的内存效果。
volatile写-读的内存语义:
当写一个volatile变量时,JMM会把线程A对应的本地内存中的共享变量值刷新到主内存。
当读一个volatile变量时,JMM会把线程B对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
线程A写一个volatile变量,随后线程B读这个volatile变量,实质上是线程A通过主内存向线程B发送消息。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
- volatile写插入内存模型:
- volatile读插入内存模型: