线程安全性的原理分析
volatile
volatile 的作用
volatile 可以使得在多处理器环境下保证了共享变量跨线程1.写入的内存可见性、2.禁止指令重排序。
注意:volatile修饰的变量如果是对象或数组之类的,其含义是对象获数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性
volatile如何保证?
汇编指令显示,在修改带有 volatile 修饰的成员变量时,会多一个 lock 指令。lock是一种控制指令,在多处理器环境下,lock 汇编指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果。
Java 编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器的重排序。
硬件引发的问题
CPU高速缓存
将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中。通过高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题,缓存一致性。
CPU高速缓存结构图
缓存一致性
首先,有了高速缓存的存在以后,每个 CPU 的处理过程是,先将计算需要用到的数据缓存在 CPU 高速缓存中,在 CPU进行计算时,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中。在整个运算过程完成后,再把缓存中的数据同步到主内存。由于在多CPU种,每个线程可能会运行在不同的CPU内,并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个 CPU 中,如果在不同 CPU 中运行的不同线程。
同一份内存的缓存值不一样就会存在缓存不一致的问题,为了解决缓存不一致的问题,在 CPU 层面做了很多事情,主要提供了两种解决办法,总线锁和缓存锁
总线锁和缓存锁
总线锁
在多 cpu 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据。
缓存锁
总线锁定的开销比较大,这种机制显然是不合适的。
所以引入了缓存锁,它核心机制是基于缓存一致性协议来实现的。保证对于被多个 CPU 缓存的同一份数据是一致的。
缓存一致性协议
为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有MSI,MESI,MOSI 等。最常见的就是 MESI 协议。
MESI
MESI 表示缓存行的四种状态,分别是
- M(Modify) 表示共享数据只缓存在当前 CPU 缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致;
- E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU 缓存中,并且没有被修改;
- S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致;
- I(Invalid) 表示缓存已经失效在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听(snoop)其它 Cache 的读写操作。
对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则:
CPU 读请求:缓存处于 M、E、S 状态都可以被读取,I 状态 CPU 只能从主存中读取数据
CPU 写请求:缓存处于 M、E 状态才可以被写。对于 S 状态的写,需要将其他 CPU 中缓存行置为无效才可写。
使用总线锁和缓存锁机制之后,CPU 对于内存的操作大概可以抽象成下面这样的结构。从而达到缓存一致性效果
总结
由于 CPU 高速缓存的出现使得 如果多个 cpu 同时缓存了相同的共享数据时,可能存在可见性问题。也就是 CPU0 修改了自己本地缓存的值对于 CPU1 不可见。不可见导致的后果是 CPU1 后续在对该数据进行写入操作时,是使用的脏数据。使得数据最终的结果不可预测。
所以引入了缓存一致性协议,使得cpu之间互相通知invalidate,cpu0发送消息给cpu1,在接受cpu1回复的这段时间里是阻塞的,所以又引入了storebufferes,CPU0 只需要在写入共享数据时,直接把数据写入到 storebufferes 中,同时发送 invalidate 消息,然后继续去处理其他指令,最后将storebufferes 同步到主存
CPU 指令重排序
由多cpu引起的问题,cpu乱序执行。
为了解决重排序问题,在 CPU 层面提供了 memory barrier(内存屏障)的指令,从硬件层面来看这个 memroy barrier 就是 CPU flushstore bufferes 中的指令。软件层面可以决定在适当的地方来插入内存屏障。
lfence(读屏障) :处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的。
sfence(写屏障) :通知处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步
到主内存写屏障之前的指令的结果对屏障之后的读或者写是可见的
mfence(全屏障):确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作
HappenBefore
表示的是前一个操作的结果对于后续操作是可见的,所以它是一种表达多个线程之间对于内存的可见性。所以我们可以认为在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before 关系。这两个操作可以是同一个线程,也可以是不同的线程。
J MM 中 有 哪些方法建立 happen- before 规则
1.程序顺序规则
2.volatile 变量规则
3.传递性规则
4.start 规则,如果线程 A 执行操作 ThreadB.start(),那么线程 A 的 ThreadB.start()操作 happens-before 线程 B 中的任意操作
5.join 规则
6.监视器锁的规则,对一个锁的解锁,happens-before 于随后对这个锁的加锁