java volatile 多线程_Java多线程之volatile

在学习Volatile之前有必要简单了解一下物理内存模型和Java的内存模型,这样对理解Volatile大有好处。

寄存器

首先我们要知道的是所有运算操作都是在CPU的寄存器中进行的,而CUP的执行涉及到数据的读取和写入两个步骤。CUP能访问到的所有数据都在计算机的主存当中,但是由于直接读取主存会很慢(相对于CPU缓存来说),所在在CPU和主存之间增加了一层Cache层,就是CPU缓存,CPU缓存一般有2级甚至3级,每一级的大小和效率各不相同,这里我们可以先不去了解,只需要统一当做是CPU缓存即可。

当CPU的缓存的引入后那么计算会怎样呢?首先把运算所需要的数据从主存中复制一份到CUP Cache,然后CPU直接对Cache中的数据进行运算操作,当运算结束时再把数据刷新回主存当中。大概流程如下图:

5aa61ab58bdd419c9982d9a7e2d26af1.png

这在单线程的场景下是没有问题的,但是如果是多个线程同时操作共享变量就会出现并发问题,例如现在有变量i=0 ,有两个线程对变量进行i++运算,线程A把i加载到本地内存(线程内存,类似CUP Cache),对i进行+1操作,而线程B同样的把i加载到本地内存,对i进行+1操作,此时在A的本地内存中i的值为1,在B的本地内存中i的值同样是1,这是A和B线程把本地内存中的i刷新到主内存,所以最终i的值是1,而不是我们期望的2。

java内存模型

6c77eb70d68a5b35ff8f36b59014dcf7.png

java的内存模型是一个抽象的概念,跟硬件的内存模型类似,它涵盖了缓存,寄存器,编译器优化以及硬件等。java内存模型跟计算机硬件的结构并不完全一样,例如计算机物理内存没有栈内存和堆内存的划分,这两者都对应到物理的主内存,当然也有可能有一部分是对应到CPU Cache和寄存器中。如下图

818b585648415dd2fe2fb178dece402d.png

并发的三大特性

1.原子性

原子性是指在一次操作或多次操作中,要么所有操作都执行成功,要么所有操作都不执行。

1) x = 1

线程把x=1写入到工作内存,然后再刷新到主内存。这个赋值为原子性操作

2) x++ 运算

x++操作涉及到3个步骤:

1,首先把x从主内存中加载到工作内存(线程内存)。

2,对x进行加1运算。

3, 把x刷新到主内存。

假设x的初始值为1,在步骤2的时候有可能有其他线程同样的x++操作,最终两个线程把x刷新到主内存后x的值为2,而不是预期期望的3。所以x++不是原子性的。

3) x = x + 1 运算

同x++一样,非原子性。

4) y = x

该赋值操作包含两个步骤

1,把x从主内存加载到工作内存。

2,把x的值赋给y,再把y刷新到主内存。

虽然两步都是原子性的,但是合起来就不是原子性的了。

从上面可以看出:

1,多个原子性操作在一起就不再是原子性操作。

2,简单的读取赋值操作是原子性,把一个变量赋给另一个变量就不是原子性的。

结论:volatile不具备原子性语义

2.有序性

我们在编写程序的时候,原则上代码逻辑是由上到下执行,但是在Java的内存模型中,允许编译器和处理器对指令进行重排来优化程序的执行,在单线程的情况下,重排不会对执行结果造成影响,但是在多线程下,指令重排就有可能影响到最终的结果了。

如下面代码:

private boolean flag = false;

private Object obj;

public Object getObj(){

if(!flag){

//1

obj = new Object();

//2

flag = true;

}

return obj;

}

原始代码经过指令重排后可能会变成

public Object getObj(){

if(!flag){

//1

flag = true;

//2

obj = new Object();

}

return obj;

}

单线程下返回的obj永远不会为null,但是多线程下,当线程A执行到if(!flag)时,线程B就刚好执行完注解1的代码,而还未执行注解2的代码,这是线程A的条件不满足,直接返回obj,而这时obj还是为null,会有NPE的风险错误。

如果变量采用volatile修饰,如下:

private volatile boolean flag = false;

那么就保证

flag = true;

obj = new Object();

之后执行。

总体概括volatile的确保有序性逻辑如下:

private volatile int i=0;

方法体{

//a代码片段

//b代码片段

//c代码片段

i = 2;

//d代码片段

//e代码片段

//f代码片段

}

volatile能确保 a,b,c在i=2赋值之前执行,有可能经过指令重排后执行顺序是a,c,b或者b,a,c。无论哪种顺序都保证在i=2赋值语句执行之前完成执行。而d,e,f保证在赋值语句之后执行,同样d,e,f的执行的顺序也可能经过重排。

结论,volatile保证有序性

3.可见性

一般线程对变量进行运算是需要先把变量从主内存加载到工作内存,然后运算完再刷回到主内存中,但是什么时候刷回主内存是不确定的,在刷回主内存之前,其他线程是看不到变量的变化的。

volatile修饰的变量,在工作内存中修改完会立刻刷回到主内存,同时会把其他线程的工作内存中的共享变量致为失效,致使其他线程需要重新从主内存中加载最新的值。保证可见性的还有另外两中方式synchronized和Lock的lock

结论,volatile保证可见性

如果对你有用的,麻烦点收藏,你的支持是我创作的动力,谢谢

参考资料

《Java高并发编程详解》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值