一、JMM内存模型
首先看一下CPU多核并发缓存架构,由于CPU和内存数据读写之间的差距过大,添加了缓存来缓和这个差距。
下面是JMM内存模型,每个线程能够从主内存读取共享变量,将读取到的共享变量保存在线程自己的工作内存中。需要注意的是线程与线程之间无法进行直接的通信。
二、深入理解Volatile关键字
一段代码如下,线程A和线程B共享变量initFlag。线程B先运行,线程A后运行。随后线程A将initFlag变为true,保存这个操作后主内存中的initFlag也会变为true。但是线程B不知道initFlag已经被改变了,线程B会一直执行不会停下。
public class VolatileVisibilitySample {
//共享静态变量
private static volatile boolean initFlag = false;
public void refresh(){
this.initFlag = true;
String name = Thread.currentThread().getName();
System.out.println(name+":修改了共享变量initFlag");
}
public void load(){
String name = Thread.currentThread().getName();
while(!initFlag){
}
System.out.println(name+":嗅探到了共享变量initFlag的改变");
}
public static void main(String[] args) {
VolatileVisibilitySample sample = new VolatileVisibilitySample();
Thread threadA = new Thread(()->{
sample.refresh();
},"线程A");
Thread threadB = new Thread(()->{
sample.load();
},"线程B");
threadB.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
在多线程的环境下,如果一个共享变量被被Volatile关键字修饰,那么在一个线程中改变了这个共享变量,其余的线程会受到改变的通知,并从主内存中读取新的值。当我们将initFlag用Volatile关键字修饰,线程A做的操作将会被线程B知道,线程B就会停下。实际上这个通知是通过MESI缓存一致性协议实现的,其本质是一个监听操作,如下图。当线程2将修改操作保存到主线程时,会通过总线,这里就会通知线程1,共享变量被改变了。
值得一提的是,在执行store和write这两个原子操作的时候,会有一个加锁和解锁的操作。
另外,在这里再详细说一下线程对共享变量的八个原子操作。如下:
多线程有三个原则:可见性、原子性、顺序性。显然Volatile关键字满足可见性,不过它不满足原子性,见下面代码:
public class VolatileAtomicSample {
private static volatile int counter = 0;
public static void main(String[] args) {
for(int i=0;i<10;i++){
Thread thread = new Thread(()->{
for(int j=0;j<1000;j++){
//此处并不是原子操作
counter++;
}
});
thread.start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
理论上counter的值最后应该是10000,但是实际运行中counter会出现小于10000的情况。
说明在counter++时,有的自增操作被覆盖了,所以Volatile不满足原子性。
另外Volatile关键字满足有序性,在此不做验证。