可见性
可见性: 就是说一个线程对共享变量的修改,另一个线程能够立刻看到
通俗点说,就是两个线程共享一个变量,无论哪一个线程修改了这个变量,另外一个线程都能够立刻看到上一个线程对这个变量的修改
产生线程安全问题的原因
计算机是利用CPU进行数据运算的,但是CPU只能对内存中的数据进行运算,对于磁盘中的数据,必须要先读取到内存,CPU才能进行运算。cpu,内存,磁盘都会影响计算机的处理性能,同时这三者之间有个核心的矛盾点,就是三者在处理速度上的差异。CPU的计算速度是非常快的,其次是内存、最后是IO设备(比如磁盘),也就是说CPU的计算速度是远远高于内存以及磁盘设备的I/O速度的。
为了平衡这三者之间的速度差异,最大化的利用CPU。所以在硬件层面、操作系统层面、编译器层面做出了很多的优化
- CPU增加了高速缓存
- 操作系统增加了进程、线程。通过CPU的时间片切换最大化的提升CPU的使用率
- 编译器的指令优化,更合理的去利用好CPU的高速缓存
每一种优化,都会带来相应的问题,而这些问题是导致线程安全性问题的根源。
CPU高速缓存
解决的问题:CPU高速缓存的出现主要是为了解决CPU运算速度和内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。
打开任务管理器 可以看到CPU的3个缓存
思考: 有了高速缓存后会带来什么问题?
分析:CPU0和CPU1并行处理同时从内存中加载数据到缓存中,此时CPU0改变了这个值,会再同步到内存中,CPU1什么时候再读到这个最新的值是不确定的,因此会存在缓存一致性问题,那么应该怎么解决呢?
在解决这个问题之前,先来了解以下伪共享和缓存行填充
伪共享和缓存行填充
缓存行:CPU的缓存是由多个缓存行组成的,最小交互单元
伪共享:
当有两个线程读取同一缓存行的不同值时,就会去竞争这同一缓存行,这就是伪共享问题。
思考:怎么解决?对齐填充
缓存一致性问题和缓存一致性协议
- 总线锁
- 缓存锁
- 缓存一致性协议(MESI MOSI) MESI表示缓存的四种状态 修改 失效 独占 共享
如果是S状态 修改时 要先把其它缓存设置为失效 失效状态从内存中读
snoopy协议会监听总线上的事件
假设a=1这个数据要进行修改,刚读进来是共享状态,当需要修改时,会通过总线发送一个指令,让其它缓存失效,然后修改完后,其它缓存从内存中读数据,就解决了缓存一致性的问题。
CPU指令重排序
cpu层面是如何导致指令重排序的? 如下图
CPU0要写入一个数据时,其它CPU在失效的时候,CPU0是处于阻塞状态的,要等到所有的缓存行失效后,再做写入操作,保证缓存一致性。 异步的思想就是会把数据加载到storebuffer中,继续执行其它的指令,等到其它的缓存行都失效后,再把数据从store buffe加载出来到缓存行
这段代码是如何存在指令重排序的
当CPU从内存中加载a=1时,会先加载到store buffer中,会让其它的缓存行失效,然后当前缓存行中的a=0(缓存一致性)变更为独占状态。然后CPU0从内存中读取b=0放到缓存行中,然后执行b=A+1此时计算出来的结果为1,然后此时其它CPU全部失效了,这时候CPU0再从store buffer中把a=1加载到缓存行,就造成了指令的重排序。
Store Forwarding
如何优化指令重排序?store forwarding就是CPU0从缓存行中读取数据,那么按照上面的例子,b=a+1加载到缓存行的数据,就是a=1了
分析: 假设a=0 存在于cpu1的缓存行中,b=0存在于cpu0的缓存行中 且都为独占状态。cpu0执行的两个代码为写操作 更新操作 CPU1只进行读。当CPU读取b的值时,就向cpu0发起请求,此时cpu0将b变更后设为共享状态。紧接着 a就在cpu1中,因此断言失败,此时 CPU1收到了CPU0的读请求,于是才让a=0缓存行失效,然后加载到cpu0中并变为独占状态后 在进行修改 。
引出的问题怎么办?
Invalid Queue
这个图就相当于 让其它缓存行失效的过程, 由同步 变为了 异步 直接丢到了invalidate中进行处理 提高效率。
CPU性能的博弈之路:
内存屏障
CPU层面不知道 什么时候不允许优化 什么时候优化
- 读屏障
- 写屏障
- 全屏障
Lock: 缓存锁总线锁
在不同的CPU架构中,实现内存屏障的指令不同
JMM模型
Happens-Before模型(告诉你哪些场景不会存在指令重排序问题)
并不是所有的程序指令都会存在可见性或者指令重排序问题,其实质性描述的是可见性规则
规则1:程序顺序型规则(as-if-serial)
不管程序如何重排序,单线程的执行结果一定不会发生变化
规则2:传递性规则
如果A happens before B B happens before C 那么A happens before C成立
规则3:volatile变量规则
规则4:监视器锁规则
规则5:Start规则
规则6:join规则
总结
可见性导致的原因 1.CPU的高速缓存 2.指令重排序
MESI协议保证缓存的一致性
指令重排序在不同的架构中有着不同的内存屏障指令来解决