likes
comments
collection
share

Java并发编程之可见性分析 volatile

作者站长头像
站长
· 阅读数 15

可见性

对于什么是可见性,比较官方的解释就是:一个线程对共享变量的修改,另一个线程能够立刻看到。

说的直白些,就是两个线程共享一个变量,无论哪一个线程修改了这个变量,则另外的一个线程都能够看到上一个线程对这个变量的修改。这里的共享变量,指的是多个线程都能够访问和修改这个变量的值,那么,这个变量就是共享变量。

例如,线程A和线程B,它们都是直接修改主内存中的共享变量,无论是线程A修改了共享变量,还是线程B修改了共享变量,则另一个线程从主内存中读取出来的变量值,一定是修改过的值,这就是线程的可见性。

Java并发编程之可见性分析 volatile

可见性问题

可见性问题,可以这样理解:一个线程修改了共享变量,另一个线程不能立刻看到,这是由CPU添加了缓存导致的问题。

理解了什么是可见性,再来看可见性问题就比较好理解了。既然可见性是一个线程修改了共享变量后,另一个线程能够立刻看到对共享变量的修改,如果不能立刻看到,这就会产生可见性的问题。

单核CPU不存在可见性问题

理解可见性问题我们还需要注意一点,那就是 在单核CPU上不存在可见性问题。 这是为什么呢?

因为在单核CPU上,无论创建了多少个线程,同一时刻只会有一个线程能够获取到CPU的资源来执行任务,即使这个单核的CPU已经添加了缓存。这些线程都是运行在同一个CPU上,操作的是同一个CPU的缓存,只要其中一个线程修改了共享变量的值,那另外的线程就一定能够访问到修改后的变量值。

Java并发编程之可见性分析 volatile

多核CPU存在可见性问题

单核CPU由于同一时刻只会有一个线程执行,而每个线程执行的时候操作的都是同一个CPU的缓存,所以,单核CPU不存在可见性问题。但是到了多核CPU上,就会出现可见性问题了。

这是因为在多核CPU上,每个CPU的内核都有自己的缓存。当多个不同的线程运行在不同的CPU内核上时,这些线程操作的是不同的CPU缓存。一个线程对其绑定的CPU的缓存的写操作,对于另外一个线程来说,不一定是可见的,这就造成了线程的可见性问题。

Java并发编程之可见性分析 volatile

例如,上面的图中,由于CPU是多核的,线程A操作的是CPU-01上的缓存,线程B操作的是CPU-02上的缓存,此时,线程A对变量V的修改对线程B是不可见的,反之亦然。

Java中的可见性问题

使用Java语言编写并发程序时,如果线程使用变量时,会把主内存中的数据复制到线程的私有内存,也就是工作内存中,每个线程读写数据时,都是操作自己的工作内存中的数据。

Java并发编程之可见性分析 volatile

volatile 保证可见性原理

首先我们来了解以下JMM中的数据原子操作:

  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用):从工作内存读取数据来计算
  • assign(赋值):将计算好的值从新赋值到工作内存中
  • store(存储):将工作内存数据写入到主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

Java中的volatile关键字是通过调用C语言实现的,而在更底层的实现上,即汇编语言的层面上,用volatile关键字修饰后的变量在操作时,最终解析的汇编指令会在指令前加上lock前缀指令

来保证工作内存中读取到的数据是主内存中最新的数据。具体的实现原理是在硬件层面上通过:MESI缓存一致性协议:多个cpu从主内存读取数据到高速缓存中,如果其中一个cpu修改了数据 ,会通过总线立即回写到主内存中,其他cpu会通过总线嗅探机制感知到缓存中数据的变化并将工作内存中的数据失效,再去读取主内存中的数据。

IA32架构软件开发者手册对lock前缀指令的解释: 1.会将当前处理器缓存行的数据立即回写到系统内存中, 2.这个写回内存的操作会引起其他cpu里缓存了该内存地址的数据失效(MESI协议)

Java并发编程之可见性分析 volatile

现在我们知道了volatile可以保证变量的可见性,我们还应该知道volatile不可以保证原子性:

  volatile无法保证原子性:如:两个线程同时read主内存中相同的值,load到工作内存中,两个线程的cpu又同时use了count值并进行了计算且assign回工作内存,但其中一个线程通过总线store回主内存的 速度更快,于是由于(总线)MESI缓存一致性协议下的cpu总线嗅探机制就会使得另一个线程工作内存中的变量副本失效,导致之前的操作结果丢失(可以结合图片理解)。