记得之前在说volitile的时候,曾经讲到过java内存模型,当时只是提到volitile可以保证变量在不同线程之间的可见性。今天我们就来详细说一下java的内存模型,聊一聊为什么同一个共享变量在不同的线程的值是不同的,以及volitile是如何做到保证共享变量在不同线程之间的其可见性的。
- java内存模型的定义
java内存模型描述的是主内存与线程工作内存的关系,它规定了所有的变量存储在主内存中,每条线程有自己的工作内存。
线程的工作内存中保存了该线程用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存。
不同线程之间无法直接访问对方工作内存中的变量,线程之间变量的传递均需要自己的工作内存与主内存进行数据同步。
这样讲起来可能有点抽象,我们通过一段代码来深刻理解一下java内存模型。
public class VolatileTest {
public int i = 0;
public void go() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (i != 0) {
break;
}
}
System.out.println("线程结束");
}
}).start();
}
public void come() {
new Thread(new Runnable() {
@Override
public void run() {
i =1;
System.out.println("修改i值");
}
}).start();
}
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
volatileTest.go();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
volatileTest.come();
}
}
在这一段代码里有一个共享变量 i,提供了两个方法go与come,开启了两个线程,这两个线程中都用到了 i 这个变量,将这个变量的值读进它们的工作内存中用一个变量副本来接收,在come线程中修改了它的工作内存中的 i 的值,同步回主线程中,不过在go线程中由于一直在死循环,用的还是go线程工作内存中的 i 的副本,没有读取到come线程已经修改过后的 i 的值,所以go线程就会一直运行。
如果我们想要让go线程获知 come线程修改的 i 值,可以用volitile修饰 i 值,至于其原理,我们等会再说。
- java内存模型原子操作
这些原子操作描述了共享变量如何从主内存进入工作内存、工作内存中的数据如何进入主内存的步骤。
主要有一下几种:- lock 作用于主存变量,把一个变量标识成为线程独占状态。
- unlock 作用于主存变量,把一个锁定的变量释放出来,释放后的变量才能被其他线程锁定。
- read 作用于主内存变量,把一个变量的值从主内存传输到线程的工作内存。
- load 作用于工作内存变量,把read操作从主内存中得到的变量值放入到工作内存变量副本。
- use 作用于工作内存变量,把工作内存中的一个变量的值传递给执行引擎,每当jvm遇到一个需要使用到变量的值的字节码指令时会执行这个操作。
- assign 作用于工作内存变量,把一个从执行引擎收到的值付给工作内存变量,每当jvm遇到一个给变量赋值的指字节码指令时执行这个操作。
- store作用于工作内存变量,把工作内存中一个变量的值传给主存。
- write作用于主存变量,把store操作从工作内存中得到的变量放入到主存变量中。
从下图可以看出这些原子操作用在什么地方。
我们拿come线程来说吧,首先come线程从主内存中将i的值read到工作内存中,然后通过load操作将 i 的值赋值给工作内存中的 i,因为在come线程中需要修改 i 的值,所以使用ues操作将 i 的值传递给come线程的执行引擎,修改完成后通过assign操作将修改后的 i 的值重新赋值给工作内存中的 i,在通过store将 i 的值传递给主内存,用write操作修改主内存中的 i 的值,这就是总的执行流程。不过come线程修改的 i 的值不会被go线程感知到。
- volitile保证数据可见性的底层实现原理
若某个线程对用volitile修饰的变量进行修改时,JVM会向处理器发送一条LOCK前缀的指令,将这个变量所在缓存行的数据写回主内存,LOCK前缀指令通过 “锁缓存” 可以确保回写主内存的操作是原子性的。但是,其它处理器的缓存中存储的仍然是 “旧值” ,并不能保证可见性,因此,还要借助缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是否过期,当处理器发现自己缓存行对应的内存地址被修改时,就会设置当前缓存行为无效,需要对数据进行修改的时候会重新从主内存中加载。如此,便保证了可见性。