赞
踩
大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。
本文已收录到我的技术网站:https://skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经
可见性是指一个线程对共享变量所作的修改能够被其他线程及时地看到。
在单核时代,其实是不存在可见性问题的,因为所有的线程都是在一个CPU中工作的,一个线程的写操作对于其他的线程一定是可见的。
但是,在多核时代,每个 CPU 都有自己的缓存。一个线程对共享变量的修改可能只是在它所在 CPU 的本地缓存中进行,而不是在主内存中进行。这就可能导致其他线程看不到这个修改,从而引发可见性问题。
解决可见性的方案有两种:
volatile
修饰共享变量:一个变量被声明为 volatile
后,对这个变量的读写操作都是在主内存中进行的,从而保证了不同线程之间对该变量修改的可见性。synchronized
。当一个线程成功获取锁进入一个同步块时,它会看到由其他线程在相同同步块内对共享变量的修改。这部分内容在 volatile 的实现原理中有,但是为了更好地阅读,大明哥直接复制过来了。
对于 volatile 变量,会在写入 volatile
变量的指令前添加 lock
前缀(汇编层面),当某个线程写入 volatile 变量时,其值会被强制刷入主内存,而其他处理器的缓存由于遵守了缓存一致性协议(MESI
协议),其他处理器的工作内存会被标志为无效。当其他处理器来访问这个变量时,由于它们的本地缓存是无效的,它们就不得不从主内存中重新加载这个变量的最新值。这样就保证了线程的可见性。
lock
前缀是用于实现原子操作的一种机制。当它用于一个指令前,它会锁定一个特定的内存地址,确保该指令执行期间,该内存地址不会被其他处理器访问。
MESI协议,即缓存一致性协议,它是一种用于维护多处理器系统中缓存一致性的协议。从上面我们知道,每个处理器都有自己的工作内存,这可能导致同一内存位置的多个副本同时存在于不同的缓存中。为了保证这些副本的一致性,引入 MESI 协议来保证一致性。
其核心思想:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
MESI 代表四种缓存行状态:Modified
(修改)、Exclusive
(独占)、Shared
(共享)和Invalid
(无效)。
其工作流程如下:
读数据
写数据
volatile 通过在在每个读操作前都加上**Load屏障,强制从主内存读取最新的数据,在每个写操作后加上Store屏障,强制将数据刷新到主内存。**这样每次写都能将最新数据刷入到主内存,读都能从主内存读取最新数据,以此达到可见性。
下面以 i++ 为例来阐述下:
如上图所示,流程如下:
i
时,遇到 Load 屏障
,需要强制从主内存中读取得到 i = 0
,加载到工作内存中。i++
操作得到 i = 1
,执行 assign指令进行赋值,遇到 Store 屏障
,需要将 i = 1
强制刷新回主内存,此时主内存数据 i = 1
。i
,也遇到Load 屏障
,强制从主内存读取 i
的最新值, i = 1
,执行 i++
操作,得到 i = 2
,同样在执行 assign 赋值后,遇到Store屏障
立即将数据刷新回主内存,此时主内存数据 i = 2
。这里可能有小伙们会认为,线程 A 和线程 B 同时执行,都从主内存读取 i = 0
,然后执行 i++
,最后主内存数据 i = 1
,会不会存在这种情况?会,但是我们通过同步机制让他们不会,为什么?因为这个操作不是原子操作,在并发情况下会产生线程安全问题,我们是需要采用同步或者锁机制来保护的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。