volatile非线程安全解析
Java内存模型
java使用的是共享变量模型,如下图所示
线程1要读取线程2修改后的值必须要线程2写回到内存,线程1再读取。
Jvm又是如何读取主存变量到线程中的呢?
内存间的相互操作
lock 将对象变成线程独占的状态
unlock 将线程独占状态的对象的锁释放出来
read 从主内存读数据
load 将从主内存读取的数据写入工作内存
use 工作内存使用对象
assign 对工作内存中的对象进行赋值
store 将工作内存中的对象传送到主内存当中
write 将对象写入主内存当中,并覆盖旧值
Volatile语义
Volatile的第一个语义就是保证此线程的可见性,一个线程对此变量的更改其他线程是立即可知的。也就是说 assign,store,write这三个操作是原子的,中间不会中断,会马上同步回主存,就好像直接操作主存一样,并通过缓存一致性通知其他缓存中的副本过期。普通变量可能会在assign,store,write之间插入其他操作,导致更改后的数据无法马上同步回主存,其他线程读取的可能是过期的旧数据。
其他缓存?过期?
Cpu与内存数据读取
在多核cup时代,不同线程可能在不同cup的核心中执行,由于cup处理速度和内存的读取速度大概相差大约一百倍,为了让cpu性能不浪费,cpu中做了一个高速缓存,cpu在处理的时候会把一批可能用到的数据载入到缓存中,等执行完毕再写回内存,cpu与内存的架构图如下
Java内存模中的数据型与cup缓存与主存的对应关系如下
共享内存的变量与线程栈中的变量副本有可能在主存中,也有可能在cpu缓存中或者cpu寄存器中。
Volatile对应的执行代码
看下边代码
Java代码: instance = new Singleton();//instance是volatile变量
汇编代码: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);
有volatile修饰的共享变量进行写操作的时候会多第二行汇编代码,查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
1.将当前处理器缓存行的数据会写回到系统内存(缓存行概念请自行查找)。
2.这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(普通变量没有马上执行这个写回操作)。
缓存一致协议
怎么理解这个缓存无效,这就得说说缓存一致协议(MESI协议)(涉及到缓存行的概念,请自行查找)
cache状态
描述
M(Modified)
这行数据有效,缓存数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
E(Exclusive)
这行数据有效,缓存数据和内存中的数据一致,数据只存在于本Cache中。
S(Shared)
这行数据有效,缓存数据和内存中的数据一致,数据存在于很多Cache中。
I(Invalid)
缓存数据无效。
cache操作
缓存一致协议(MESI)协议中,每个cache的控制器不仅知道自己的操作,也通过监听知道其他CPU中cache的操作;
local read(LR):表示本内核读本Cache中的值;
local write(LW):表示本内核写本Cache中的值;
remote read(RR):表示其它内核读其它Cache中的值;
remote write(RW):表示其它内核写其它Cache中的值;
MESI状态之间的变化过程如下:
当前状态 |
事件 |
行为 |
下一个状态 |
I(Invalid) |
Local Read |
如果其它Cache没有这份数据,本Cache从内存中取数据,Cache line状态变成E; 如果其它Cache有这份数据,且状态为M,则将数据更新到内存,本Cache再从内存中取数据,2个Cache 的Cache line状态都变成S; 如果其它Cache有这份数据,且状态为S或者E,本Cache从内存中取数据,这些Cache 的Cache line状态都变成S |
E/S |
Local Write |
从内存中取数据,在Cache中修改,状态变成M; 如果其它Cache有这份数据,且状态为M,则要先将数据更新到内存; 如果其它Cache有这份数据,则其它Cache的Cache line状态变成I |
M |
|
Remote Read |
既然是Invalid,别的核的操作与它无关 |
I |
|
Remote Write |
既然是Invalid,别的核的操作与它无关 |
I |
|
E(Exclusive) |
Local Read |
从Cache中取数据,状态不变 |
E |
Local Write |
修改Cache中的数据,状态变成M |
M |
|
Remote Read |
数据和其它核共用,状态变成了S |
S |
|
Remote Write |
数据被修改,本Cache line不能再使用,状态变成I |
I |
|
S(Shared) |
Local Read |
从Cache中取数据,状态不变 |
S |
Local Write |
修改Cache中的数据,状态变成M, 其它核共享的Cache line状态变成I |
M |
|
Remote Read |
状态不变 |
S |
|
Remote Write |
数据被修改,本Cache line不能再使用,状态变成I |
I |
|
M(Modified) |
Local Read |
从Cache中取数据,状态不变 |
M |
Local Write |
修改Cache中的数据,状态不变 |
M |
|
Remote Read |
这行数据被写到内存中,使其它核能使用到最新的数据,状态变成S |
S |
|
Remote Write |
这行数据被写到内存中,使其它核能使用到最新的数据,由于其它核会修改这行数据, 状态变成I |
I |
此表为引用《大话大话处理器》,如有侵权请联系删除
为什么i++不是线程安全的
为什么多线程中i++不是线程安全的,首先++操作不是原子的,要经过读取计算和写回
3和4是连续操作不间断,5,6,7也是连续不间断的,6把旧值覆盖了新值。
这就是为什么volatile定义的变量在多线程做++操作时也是线程不安全的原因。
语义1总结
简而言之就是用关键字修饰的就是修改后及时写回主存,对其他线程可见可理解为其他线程探嗅到自己缓存中的变量是过期的(不同线程在同一核心道理相同)。
Volatile语义2
Volatile第二层意思就是禁止指令重排序。
Jvm为了优化性能会采用指令重排序
Int a=1;
Int b=2;
Jvm的执行顺序可能是先b=2;然后再a=1,因为当代码执行到a=1时a对象可能被加锁了,这时如果等待锁释放显然浪费了时间,所以先执行b=2,但是用volatile修饰的关键字在操作的时候必须是在指定位置的,即如果这个变量的操作在第五行那会保证前四行都执行完才会执行第五行。
本文转载自:https://blog.****.net/chenaima1314/article/details/78723265