开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第28天,点击查看活动详情
在并发编程中,volital和synchronize都扮演着很重要的角色。synchronize可以保证可见性、禁止指令重排序和原子性。而volital只能保证可见性和禁止指令重排序。volital相当于轻量级的synchronize。
1.volital定义
可见性意思是一个线程修改变量的值时,另一个线程可以读取到这个修改后的值。
volital使用恰当比synchronize更能提高cpu的运行效率,因为volital不需要线程的上下文切换和调度。
Java语言规范第3版中对volatile的定义如下:
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便(读多写少)。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
2.MESI协议
因为volatile底层使用的是缓存一致性协议,会根据MESI原则。
MESI分别代表缓存行数据所处的四种状态,通过对这四种状态的切换,来达到对缓存数据进行管理的目的。
M 修改 Modified : 该缓存行有效,且该缓存行数据被修改了,和内存中的数据不一致,数据只存在于本缓存行中,此时状态为M ;
E 独占 Exclusive : 该缓存行有效,且该缓存行数据和内存中的数据一致,此时状态为E;
S 共享 Shared : 该缓存行有效,且该缓存行数据和内存中的数据一致,数据同时存在于其他缓存中(同步到主内存中完成后 , 标记为共享状态 ; 共享状态的变量才能被线程加载到工作内存中),此时状态为S ;
I 失效 Invalid : 该缓存行无效,线程 A , B 中都使用了同一个共享变量 , 如果在线程 A 中修改该变量 , 线程 B 中的变量更新前都标记为失效状态,此时A状态为M状态,B状态为I状态 ;
1、CPU1从内存中将变量i加载到缓存中,并将变量i的状态改为E(独享),并通过总线嗅探机制对内存中变量i的操作进行嗅探(因为有volatile关键字修饰,如果没有修饰则不会进行嗅探,说明可见性)
2、此时,CPU2读取变量i,总线嗅探机制会将CPU1中的变量i的状态置为S(共享),并将变量i加载到CPU2的缓存中,状态为S
3、CPU1对变量i进行修改操作,此时CPU1中的变量i会被置为M(修改)状态,而CPU2中的变量i会被通知,改为I(无效)状态,此时CPU2中的变量i做的任何修改都不会被写回内存中(高并发情况下可能出现两个CPU同时修改变量i,并同时向总线发出将各自的缓存行更改为M状态的情况,此时总线会采用相应的裁决机制进行裁决,将其中一个置为M状态,另一个置为I状态,且I状态的缓存行修改无效)
4、CPU1将修改后的数据写回内存,并将变量i置为E(独占)状态
5、此时,CPU2通过总线嗅探机制得知变量i已被修改,会重新去内存中加载变量i,同时CPU1和CPU2中的变量i都改为S状态!
这就是完整的MESI流程,因为第3步中,高并发竞争总线会采用相应的裁决机制进行裁决,将其中一个置为M状态,另一个置为I状态,且I状态的缓存行修改无效,浪费一次i++。