背景
JMM称为java内存模型,用于规范线程之间的数据共享问题,出现此模型的原因和硬件的发展有关。
早期的单核cpu下不会存在该问题,后来多核cpu和cpu多重缓存机制加快了数据的读写,但是也产生了不同步问题。
JMM之后通过cpu的总线机制优化了这个情况,该总线是主内存和CPU缓存之间的一个过渡区,可以通知各个线程变量变化。加了Volatile的变量可以被总线监视。
过程
如图A所示:
假如某个CPU有两个核心,每个核心执行一个线程,硬件角度上他们两个是不同的寄存器,但是却共享主内存的同一个变量。
刚开始两者都读取同一个变量到自己的线程内(自己的寄存器内),产生各自的一个副本变量,之后线程B修改false变量,此时若变量为一般变量,则不会触发cpu总线机制。
若此时一个Volatile变量发生了变化,总线机制会通过MIES修改变量的信息。
MEIS是缓存一致性,是CPU用来通知线程缓存变化的一种手段,四个字母表示四种状态,从字节级别进行标识变量。
M(Modified):数据被修改了,属于有效状态,但是数据只处于本Cache,和内存不一致。
E(Exclusive):数据独占,属于有效状态,数据仅在本Cache,和内存一致。
S(Shared):数据非独占,属于有效状态,数据存于多个Cache,和内存一致。
I(Invalid):数据无效。
简单来说 ,当关键变量只被一个线程读取时,他的状态是E独享,此时根本不会出现缓存不一致现象,但如果此时又被另外一个线程读取了,总线会通知拥有此线程将此变量的头信息进行修改,标识变成了S,此时如果线程再修改自己寄存器内的变量,由于此时标识是S,则会通知总线,总线再通知将所有拥有此变量的头信息标识改为M,此时其他线程就得知这个变量被修改了,就从主内存中重新刷新数据到自己的线程里,此时标识又会从M改为S。
至于Volatile,打断点可以得知此处是通过C++的Lock机制通知系统底层提示总线进行操作。
图A
Volatile不能保证原子性
传统情况下,一个线程写,多个线程读,写线程的操作会通过MEIS协议告知其他线程刷新值,所以不会出现问题。
但是如果是多线程下,线程A操作完数据并将值刷新到主内存,此时线程B恰好也完成操作,此时却被总线通知要重新读取,结果就会抛弃之前的计算结果,重新读取旧值,又由于缺少CAS的重试,导致之前的操作没有重新再执行,所以1000次累加计算中,总会有几次操作被撤销导致数据丢失。
Volatile的重排序和内存屏障
在多线程环境下,如果一个变量要被多个线程共享,进行写操作时,可能会发生A线程先处理完,之后B再处理,但是A还没有刷新到主内存中,B读取脏数据,成了这样是不符合预期情况的,称为重排序问题。
简单来说就是多个线程将预期的顺序打乱了,会出现预料之外的结果。
加了Volatile的关键字会让CPU把数据及时刷新到主内存中,防止B读取数据时是脏数据,称为内存屏障,这点和事务的脏读有点像。
Volatile导致的重排序和和双层检验锁
new 一个对象的操作实际上是三步:
1.分配对象的内存空间
2.调用构造函数初始化
3.将对象赋值给变量
在多线程环境下,重排序可能会产生对象多次初始化操作。
如:单例双层检验锁中的成员变量若没有Volatile,多线程环境下可能会发生重排序问题导致线程A还没有执行第三步,线程B却又进入到锁中并通过了if又准备执行一遍初始化。
Volatile导致效率变低的原因和伪共享问题
首先确定一个空的java对象在64位环境下占据6个字节,头结点8个字节,,AbstractPaddingObject自动填充字节为8的倍数,可以提高一些性能。
多线程下,一个写线程,多个读线程,因为读线程每次都要从主内存刷新,所以效率降低了一部分。
伪共享问题一般只有在主内存变量加上了Volatile关键字,对另外的线程及时可见,另外线程读取时会以64字节缓存行读取,若没有Volatile则不会出现以64字节行读取主内存。
参考:蚂蚁课堂