线程安全性原理
1)volatile
主线程里面改变的值,无法使得T1线程可见,导致依然退不出来。
volatile的目的在于:在多个处理器下,保证多个共享变量的可见性。
可见性: 别的线程修改的值,别的线程无法连读取到最新的值。
hsdis工具:
多了一个Lock的汇编指令。 基于总线或者缓存,达到可见性的目的。
2)可见性到底是什么? --》硬件层面了解可见性的本质
CPU、内存、磁盘IO设备。
矛盾点: 3者之间的速度差异很大。
程序中要依赖CPU、还依赖内存,还可能依赖网络IO。 程序的性能取决于短板(木桶原理)。
cpu 1ms, 内存10ms,IO设备100ms,因此计算需要100ms
我们需要最大化的利用CPU资源:
1.CPU增加高速缓存:
CPU调度线程去计算的时候,要依赖于内存,要读取数据。 2个CPU要基于总线去通信,然后从主内存取数据。 在主内存读取IO的过程中,我们的CPU
将会被阻塞,这我们是无法接受的。因此我们引入了CPU的高速缓存。 因此内存和CPU之间增加了高速缓存,我们CPU先从高速缓存中读取数据。
有3种缓存:L1、L2、 L3。 其中L1 L2是CPU私有的
指令缓存
数据缓存
共享的L3
L2Cache
L1d L1i
regs
解决了速度访问差异,但是引来了新的问题:缓存不一致问题:因此CPU层面
1.总线锁:
CPU要从主内存拿数据时,一定会先通过总线,主内存访问的互斥特性。 因此又导致串行。 因此引入了缓存锁(降低了锁的控制粒度)
2.缓存锁
缓存一致性协议(MESI、。。。):
缓存行的4种状态:
CPU0:
cache i = 0;
Shared: 被多个CPU缓存了
Invalid:有一个缓存已经失效了
Modify: Exclusive
嗅探协议: 一个CPU改为了1,,那么状态由S变为M,让CPU1的缓存行处于失效状态,使得CPU1箱再次读取I的值,需要去主内存拿,
而不是去自己的缓存行去拿,因此自己的缓存行失效了,这时就打到了缓存一致性的效果。
缓存处于: MES状态都可以读取,只有I状态的CPU,必须从主内存读取。
总体:对于MESI,就缓存一致性。 但是肯定还是没有解决缓存可见性问题,不然也就没必要增加volatile关键字了。
各个CPU的缓存行需要一致
什么时候数据同步到主内存呢?
主内存i=0
CPU1: CPU2:
i=0 i=0
i++ --》i=1
这时,CPU1修改为了i=1,但是还没有同步到主内存
storebuffer:
让storebuffer, 是一个异步流程。提高CPU利用率。但是再次带来了可见性的问题--》说明硬件层面无法彻底解决可见性问题--》引出内存屏障。
value = 3;
void cpu0(){
value=10; S->M状态。 --》会先写storebuffer--》通知其他cpu,让其他cpu缓存失效(这个是异步的)。 因此晚于isFinish。CPU的乱序性。重排序。
重排序会导致可见性问题。 因此硬件层面怎么优化,都无法彻底解决可见性问题。
因此CPU层面提供了内存屏障的指令。程序里面在适当的地方加入内存屏障,从而达到可见性的目的。
storeMemoryBarrier(); // 屏障指令
isFinish = true; E状态。
}
void cpu1(){
if(isFinish){ // true
loadMemoryBarrier(); // 使得直接和主内存交互
assert value ==10; // false
}
}
异步化带来的可见性问题。
CPU层面提供3种屏障:
写屏障: store barrier
读屏障: load barrier
全屏障: full barrier 读+写
volatile: lock(缓存锁)-->内存屏障--》可见性
内存屏障、重排序 --》和平台以及硬件有关,但是java是一次编译到处运行。--》因此引入了JMM
2.引入线程、进程的概念。 线程阻塞情况下,让CPU进行上下文切换。
3.指令的优化: 重排序
3)可见性到底是什么? --》什么是JMM
可见性问题的根本原因: 高速缓存和重排序。
禁止缓存和禁止重排序。
JMM最核心的价值:解决 有序性和可见性 的问题。
语言级别的抽象内存模型:
使用内存屏障去禁止重排序。
主内存
工作内存 工作内存
线程A 线程B
线程无法直接读取主内存,而是要先读取工作内存。工作内存可以认为是高速缓存。
主内存存在共享变量,都缓存了isStop
volatile、synchronized、final、happens-before
源代码--》编译器的重排序(解决方案: 内存屏障)--》CPU层面的重排序(指令级、内存 解决方案:内存屏障)--》最终执行的指令
as-if-serial: 不是所有的程序都会进行重排序,要数据依赖规则,不能改变单线程的执行结果:
int a = 1;
int b = a;
被volatile修饰的变量,storeload
4)Happens-Before
可见性的保障--》volatile以外,还提供了其它方法
A happens-before B的含义: A操作的结果对于B是可见的,而不是A在B操作之前发生
5)哪些操作会建立Happens-Before
1.程序的顺序规则
1 Happens-Before 2 3 Happens-Before 4
2.volatile
2 Happens-Before 3
一个变量的写操作,一定会对于后面的读操作可见
3.传递性规则
1 Happens-Before 2 2 Happens-Before 3 3 Happens-Before 4 一定存在--》1 Happens-Before 4
4.start规则
x=10时在main先发生了,再去启动t1线程--》那么t1一定拿到的x是10
5.join规则
t1 join了,那么t1中执行完x=100,在main线程打印x一定是100
t1.join() 会阻塞主线程。。必须等待阻塞释放。 只要存活,则一直wait。 还是基于wait和notify来实现的。
join的目的是为了:执行结果对后面的程序可见。
6.锁的规则
synchronized。。在t1释放锁以后的操作一定在t2加锁之前
6)synchronized: 可以解决原子性、有序性、可见性(使用Happens-Before)
7)volatile: 可以解决可见性。 但是不能解决原子性问题。 禁止指令重排序来打到可见性效果。