1、内存模型的概述
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行。
首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
2、内存模型的示意图
主内存:
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。
工作内存:
主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
3、JMM数据原子操作
4、内存模型工作流程图
上图jmm工作流程:
步骤一
线程1:先把initFlag变量从主内存中read读取出来,再load载入自己的工作内存,use使用线程1执行代码!initFlag。
步骤二
线程2:先把initFlag变量从主内存中read读取出来,再load载入自己的工作内存,use使用线程2执行代码initFlag=true,这时候值发生了改变,线程会再assign重新赋值到工作内存,然后store存储并写入主内存,write写入赋值到主内存中的变量。
(线程2对缓存行lock加锁,write写入主内存后会解锁unlock,防止initFlag还未write写入主内存就被线程1读取为false)。
步骤三
线程1:因为initFlag被volatile修饰,使用MESI缓存一致性协议,线程1利用cpu总线嗅探机制监听到了initFlag值的修改,线程1中工作内存initFlag=false失效,这时候线程就拿不到工作内存的数据了,从而工作内存继续去读取主内存数据,变为true退出循环继续执行,体现了多线程同步运行共享变量副本的可见性。如果initFlag没有被volatile修饰,线程1将感知不到initFlag的变化,一直循环下去停止不了。
实例代码:
public class VolatileVisibilityTest {
//volatile变量,用来确保将变量的更新操作通知到其他线程。
private static volatile 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 data end...");
}
}
以上加了“volatile”关键字,实现了共享变量的可见性问题
volatile是怎么实现的呢?原理是怎样的?
volatile底层原理不是Java来编写的,而是用汇编语言的来编写
很早以前是用总线的加锁的机制来做到可见性和一致性的
lock和unlock会对主内存加锁的,总线加锁一般不使用,效率太低,跟单线程差不多。一般用MESI缓存一致性协议。