【问题提出】:如何解决多线程之间共享变量的可见性,一致性问题?
一、Volatile缓存可见性
(1)JMM内存模型:主内存和工作内存
Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型建立的:
如图,有多个线程运行在不同CPU上,同时读取一个static变量或者实例变量,为了提高程序运行速度,会把主内存中的共享变量加载到工作内存中去。
——所以,如果在一个线程中改变了静态变量,另一个线程很有可能察觉不到这个改变。
eg:
public class VolatileVisibilityTest {
private static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException{
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("waiting data...");
while(!initFlag){
}
System.out.println("======success");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
prepareData();
}
}).start();
}
public static void prepareData(){
System.out.println("preparing data...");
initFlag = true;
System.out.println("prepare end...");
}
}
——线程一中有一个死循环等待,initFlag如果为false,就会一直等待,知道initFlag为true时才会显示Success;
但是当线程二中将initFlag改为true后,线程一还没结束循环,这就证明了线程二改掉的是加载到其工作内存中的静态变量,即便它会被存储回主内存中,线程一也不会去读取这个修改后的值。
【解决方案】对变量加一个volatile关键字
private static volatile boolean initFlag = false;
(2)为什么加了volatile的变量副本间能相互感知?
① Java内存底层交互模型:
-
read(读取):从主内存读取数据;
-
load(载入):将主内存读取到的数据写入工作内存;
-
use(使用):从工作内存读取数据来计算;
-
store(存储):将工作内存数据写入主内存;
-
write(写入):将store过去的变量值赋值给主内存中的变量;
-
lock(锁定):将主内存变量加锁,标识为线程独占状态;
-
unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量。
-
② 早期volatile缓存可见性底层实现(总线加锁:性能低):
第一个到达主内存的线程read操作之前,会对主内存加一把锁(lock),直到数据同步回主内存,才会把锁释放掉(unlock)。在这期间,其他线程无法读取(read)主内存共享变量,当锁被释放掉后,主内存的共享变量的值已经被同步为最新的值(即initFlag=true),其他线程再读取,就是最新数据。——线程之间要等待
——性能太低!
③ MESI缓存一致性协议
多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会立马同步回主内存,其他CPU通过总线嗅探机制可以感知到数据的变化,从而将自己缓存里的数据变为失效。
当CPU读自己的工作内存中的数据时,发现数据已经空了,就会马上重新从主内存中执行read操作,解决了可见性问题。
——为了使过程变得更快,lock操作需要进一步解释,见下文。
(3)Volatile缓存可见性底层实现原理(lock & MESI)
底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定),并写回到主内存。
IA-32架构软件开发者手册对lock指令的解释:
① 会将当前处理器缓存行的数据立即写回到系统内存;
② 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)。
在store之前加锁(lock)了,而不是在read前加锁(这个在主内存中的写入操作非常快,因而这个锁很快就会结束)。当需要被存储(store)的数据通过总线后,此时就会被嗅探到,会导致其它线程失效了,这些线程再去读主内存数据时,因为加锁了,需要等待数据写回(write)操作完成后,释放锁(unlock),其他线程才能去读取主内存中的数据,避免过早去读取。——缓存一致性
简单归纳总结:
(1)所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存;
(2)所有volatile修饰的变量在使用之前必须重新读取主内存的值;
二、并发编程的三大特性:可见性、原子性、有序性
volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制。
可见性:共享变量被同时操作的时候,两个线程之间能够相互感知到变量的修改。