谈谈对Volatile的理解?
Volatile是虚拟机提供的轻量级的同步机制
Volatile的三大特性:
- 保证内存可见性
- 不保证原子性
- 禁止重排序
学习Volatile的特性之前,我们需要先了解一下什么是JMM?
JMM(Java内存模型Java Memory Model,简称JMM),是一种抽象的概念,本身并不存在,它描述的是一种规范,规范了程序中各个变量的访问方式。
JMM关于同步的定义:
- 线程解锁前,必须把共享内存的值写到主内存。
- 线程加锁前,必须读取主内存的最新值到自己的工作内存中。
- 加锁解锁必须是同一把锁。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程
通过前面的JMM介绍,我们知道每个对内存中的共享变量的操作都是拷贝一份到自己的工作内存中操作,之后在写回到主内存中。
这会导致,比如A线程读取最新值后做了相关运算,但还未来得及写回到主内存,而此时B线程又对同一变量进行了操作,A线程内存中的变量对B来说不可见,这就造成了内存可见性问题。
内存可见性代码验证
Talk is cheap. Show me the code
public class VolatileDemo1 {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
try {
Thread.sleep(3);
data.assignment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
while (data.getNum() == 0) {
}
System.out.println("num:" + data.getNum());
}
}
class Data {
// 不加Volatile关键字
private int num = 0;
public int getNum() {
return num;
}
public void assignment() {
this.num = 1;
}
public void setNum(int num) {
this.num = num;
}
}
先不要往下看,思考一下代码的执行结果是什么?
控制台会打印出num:1吗?
会出现死循环的可能,因为main线程刚开始读取到的num=0,new的新线程在自己的工作内存内修改num的值为1,但对main线程不可见,所以将会导致死循环。
那如果解决了,这个时候我们的Volatile就可以出场了。在num前加上Volatile关键字,其它代码与上面同步。
还记得Volatile的第一条特性吗,Volatile保证内存可见性。新线程修改了Volatile的值为num=1后,把值写回到主内存,main线程就能够读取到值,程序可以正常打印出num:1
Volatile保证内存可见性,但不保证原子性。
原子性是什么:原子性是不可分割,某个线程在处理业务时,中间不能被其它线程插队,加塞。需要完整性,要么全成功,要么全失败。
观察一下以下代码,输出的num值是多少?
public class VolatileDemo {
private static volatile int num = 0;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
num++;
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("number的值是:" + num);
}
}
num的值会是20000吗?为什么
多次的运行结果:19488,17888,18288,20000,19222
为什么多次的运行结果都不一样,结果怎么会小于20000呢?
前面提到了Volatile的第二条特性不保证原子性,num++在JVM字节码中分成了三个指令:拿到原始值num、执行+1操作、把累加后的值写回。
假如线程A拿到num值进行+1操作,还没来的及写回主内存中,线程B拿到num的值+1后写会到主内存,此时num的值为1。但是因为线程A被挂起了导致没有拿到最新值1,num+1后把结果1写到主内存。如果正常执行此刻的num的值应该为2,但因为多线程执行的问题导致此时num值少了1。
如何解决原子性问题。
方法一:可以加synchronized锁。(重量级)
方法二:使用AtomicInteger类。
提供代码演示方法二:
public class VolatileDemo {
private static volatile int num = 0;
static AtomicInteger num1 = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
num++;
num1.getAndIncrement();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("number的值是:" + num);
System.out.println("num1的值是:" + num1);
}
}
运行多次,发现number的值小于20000,而num1的值正好等于20000。
此时就会有问题,那AtomicInteger是如何保证原子性的呢?
查看源码,是CAS实现的,CAS之后会单另提出。
Volatile的第三大特性,禁止重排序。
我们看见的往往不一定是真实。眼见也不一定为实。
计算机在执行程序时,往往为了提高性能,编译器和处理器常常会对指令做重排序。
- 单线程环境里,程序保证最终执行结果和代码的执行的结果一致。
- 处理器在重排序时会考虑指令见的数据依赖性。
- 多线程环境中线程交替执行,由于重排序的存在,能否保证一致性结果无法预测。
public class Reorder {
int a = 0;
boolean flag = false;
public void method1() {
a = 1;
flag = true;
}
public void method2() {
if (flag) {
a = a + 5;
System.out.println("a 的值是:" + a);
}
}
public static void main(String[] args) {
Reorder reorder = new Reorder();
// 单线程调用
// reorder.method1();
// reorder.method2();
// 多线程
new Thread(reorder::method1).start();
new Thread(reorder::method2).start();
}
}
如果是单线程调用,结果肯定是6,因为单线程保证结果一致性。
多个线程执行,a=1,flag=false,之间并没有数据依赖性,则可能会发现重排序,导致先执行flag=true,此时a=0,会导出输出的结果可能是5。
而Volatile关键字会增加内存屏障,禁止重排序。可以通过Volatile关键字告诉编辑器或者虚拟机,我这已经是最好的方案了,你别给我瞎优化了。瞎优化出了问题你背锅嘛!!!