大家好,我是大明哥,一个专心「死磕 Java」系列创作的硬核程序员。 本文已收录到我的技能网站:skjava.com。有全网最优质的系列文章、Java 全栈技能文档以及大厂完整面经


什么是可见性?

可见性是指一个线程对共享变量所作的修正能够被其他线程及时地看到。

在单核年代,其实是不存在可见性问题的,因为一切的线程都是在一个CPU中作业的,一个线程的写操作关于其他的线程一定是可见的。

可是,在多核年代,每个 CPU 都有自己的缓存。一个线程对共享变量的修正或许只是在它所在 CPU 的本地缓存中进行,而不是在主内存中进行。这就或许导致其他线程看不到这个修正,然后引发可见性问题。

解决可见性的方案有两种:

  1. 运用 volatile 修饰共享变量:一个变量被声明为 volatile 后,对这个变量的读写操作都是在主内存中进行的,然后确保了不同线程之间对该变量修正的可见性。
  2. 运用同步机制,比方锁或许 synchronized。当一个线程成功获取锁进入一个同步块时,它会看到由其他线程在相同同步块内对共享变量的修正。

volatile 是怎么确保可见性的?

这部分内容在 volatile 的实现原理中有,可是为了更好地阅览,大明哥直接仿制过来了。

关于 volatile 变量,会在写入 volatile 变量的指令前增加 lock 前缀(汇编层面),当某个线程写入 volatile 变量时,其值会被强制刷入主内存,而其他处理器的缓存由于遵守了缓存共同性协议(MESI 协议),其他处理器的作业内存会被标志为无效。当其他处理器来访问这个变量时,由于它们的本地缓存是无效的,它们就不得不从主内存中重新加载这个变量的最新值。这样就确保了线程的可见性。

lock 前缀是用于实现原子操作的一种机制。当它用于一个指令前,它会确定一个特定的内存地址,确保该指令履行期间,该内存地址不会被其他处理器访问。

MESI 协议

MESI协议,即缓存共同性协议,它是一种用于维护多处理器系统中缓存共同性的协议。从上面咱们知道,每个处理器都有自己的作业内存,这或许导致同一内存位置的多个副本一起存在于不同的缓存中。为了确保这些副本的共同性,引进 MESI 协议来确保共同性。

其中心思维:当 CPU 写数据时,假如发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会宣布信号告诉其他CPU将该变量的缓存行置为无效状况,因此当其他CPU需求读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

MESI 代表四种缓存行状况:Modified(修正)、Exclusive(独占)、Shared(共享)和Invalid(无效)。

  1. Modified(修正) :数据有用,数据已被修正,且只存在于当时缓冲中。这个状况下的缓存行数据于主内存数据不共同,在数据被写回主内存之前,任何对这个缓存行的读取或写入操作都只会发生在这个缓存中。
  2. Exclusive(独占) :数据有用,且只存在于当时缓存中。这个状况下的缓存行数据与主内存中的数据是共同的。假如 CPU 需求写入这个缓存行,它可以直接改变状况到Modified,而无需与其他处理器或主内存通讯。
  3. Shared(共享) :数据有用,且或许存在于多个 CPU 的缓存中,而且数据与主内存中的数据是共同的。这个状况下的缓存行任何 CPU 都可以读到,但假如某个 CPU 需求写入,它必须首要告诉其他具有该缓存行副本的 CPU,使它们的副本无效。
  4. Invalid(无效) :数据无效。假如有 CPU 需求读取这个缓存行数据,它必须从具有有用副本的其他缓存或主内存中读取数据。

其作业流程如下:

读数据

  • 假如数据在本地缓存中而且状况是 Modified、Exclusive 或 Shared,处理器直接从缓存中读取,因为这三种状况的数据是有用的。

  • 假如数据不在本地缓存中,或许缓存行状况是 Invalid,处理器向其他缓存发送读取恳求:

    • 假如其他缓存中没有该数据,或许都是 Invalid 状况,处理器从主内存读取数据,并将本地缓存行状况设置为 Exclusive。
    • 假如其他缓存中有该数据而且至少一个是 Shared 或 Modified 状况,处理器从具有该数据的缓存仿制数据,并将一切具有该数据的缓存行状况设置为 Shared。

写数据

  • 假如数据在本地缓存且状况是 Modified,处理器直接写入本地缓存。

  • 假如数据在本地缓存且状况是 Exclusive,处理器将缓存行状况改为 Modified,并履行写入操作。

  • 假如数据在本地缓存且状况是 Shared 或许不在本地缓存中,处理器向其他缓存发送失效告诉:

    • 其他缓存假如有该数据,则将其缓存行状况设置为 Invalid。
    • 本地缓存将数据写入,并将缓存行状况设置为 Modified。

内存屏障

volatile 经过在在每个读操作前都加上**Load屏障,强制从主内存读取最新的数据,在每个写操作后加上Store屏障,强制将数据改写到主内存。**这样每次写都能将最新数据刷入到主内存,读都能从主内存读取最新数据,以此到达可见性。

下面以 i++ 为例来阐述下:

Java 面试宝典:什么是可见性?volatile 是怎么确保可见性的?

如上图所示,流程如下:

  • 线程 A 读取 i 时,遇到 Load 屏障,需求强制从主内存中读取得到 i = 0,加载到作业内存中。
  • 线程 A 履行 i++ 操作得到 i = 1,履行 assign指令进行赋值,遇到 Store 屏障,需求将 i = 1 强制改写回主内存,此刻主内存数据 i = 1
  • 然后线程 B 读取 i,也遇到Load 屏障,强制从主内存读取 i 的最新值, i = 1,履行 i++ 操作,得到 i = 2,同样在履行 assign 赋值后,遇到Store屏障立行将数据改写回主内存,此刻主内存数据 i = 2

这儿或许有小伙们会以为,线程 A 和线程 B 一起履行,都从主内存读取 i = 0,然后履行 i++,最后主内存数据 i = 1,会不会存在这种状况?会,可是咱们经过同步机制让他们不会,为什么?因为这个操作不是原子操作,在并发状况下会发生线程安全问题,咱们是需求采用同步或许锁机制来维护的。