1.java内存模型
java内存模型(Java Memory Model, JMM)中规定了所有变量都存贮到主内存中。每个线程都有一个自己的工作内存(如cpu中的高速缓存)。线程中的工作内存保存了该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。不同线程之间无法直接访问对方工作内存中的变量。线程间变量的值传递均需要通过主内存来完成。
Java内存模型的8大原子操作:
①lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
②unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
③read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
④load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
⑤use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
⑥assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
⑦store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
⑧write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
注意:
①如果需要把变量从主内存赋值给工作内存,read和load必须是连续。read只是把主内存的变量值从主内存加载到工作内存中,而load是真正把工作内存的值放到工作内存的变量副本中。
②如果需要把变量从工作内存同步回主内存,就需要顺序执行store跟write操作。store作用于工作内存,将工作内存变量值加载到主内存中,write是将主内存里面的值放入主内存的变量中。
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
①不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起回写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
②不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
③不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
④一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
⑤一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
⑥如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
⑦如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
⑧对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
代码实例:
public class VolatileTest2 {
static boolean flag = false;
public void refresh(){
this.flag = true;
String threadName = Thread.currentThread().getName();
System.out.println("线程: " + threadName + " 修改共享变量flag为"+flag);
}
public void load(){
String threadName = Thread.currentThread().getName();
while (!flag){
//flag为false时,进入死循环,这时不会打印下面的log
}
System.out.println("线程: "+threadName+" 嗅探到flag状态的改变"+" flag:"+flag);
}
public static void main(String[] args) {
//创建两个线程
VolatileTest2 obj = new VolatileTest2();
Thread thread1 = new Thread(() -> {
obj.refresh();
}, "thread1");
Thread thread2 = new Thread(() -> {
obj.load();
}, "thread2");
thread2.start();
try {
//确保线程2先执行
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
thread1.start();
}
}
上面代码数据结果为:
线程: thread1 修改共享变量flag为true
并且主线程不会退出,说明有用户线程在runnable运行中,说明线程2一直在运行,也说明线程2获取的变量值先从主内存read到工作内存,然后load给线程2里面工作内存里面变量,然后线程2一直是从自己工作内存获取数据,并且线程2是while的空转,抢占cpu时间多,所以一直不退出。
2.基于8大原子操作程序数据加载回写流程
8大原子操作是怎样做的?变量是如何读取、如何赋值的?
上面是线程2执行后的结果,所以线程2先读取到flag=false,所以先不会退出。接着线程1会执行修改flag的操作,将flag修改成true,具体修改操作如下:
①第1步:read变量到线程1的工作内存中
②第2步: load到工作内存的变量副本中
③第3步: use传递给执行引擎做赋值操作
④第4步: 将修改后的值assign到工作内存,这个值会从false变成true
那么工作内存里面的新值flag=true会立马同步到主内存里面去吗?答案是不会的!更新后的新值不会立马同步到主内存里面去,他需要等待一定的时机,时机到了之后才会同步到主内存中去。同步的时候也需要分为执行两步骤:store和write操作。
但是更新到主内存为true之后,为什么线程2没有感知到呢?原因是线程2在while进行循环判断的时候,执行引擎一直判断的是线程2自己的工作内存里面的值。
然后修改代码如下:在while循环判断里面加一个i++的话,那么线程2能不能及时感知到flag变化的值呢?
因为工作内存中已经存在这个值的话,就不会从主内存去加载。
再修改代码如下:线程3去读取主内存flag的值,因为线程3是从主内存加载的线程1已经写入的值,此时这个值是flag=true,所以ok。
然后在线程2的while循环里加上一个同步代码块之后的效果呢?
此时,线程2已经感知到了flag数据的变化。 加了同步块之后,线程2就能够读取到线程1修改的数据,这个是为什么呢?
原因:之前没有加同步代码块之前,程序指令一直在循环/或者一直在做i++操作,循环是空的,可以理解为其近似在自旋跑。此时此线程对cpu的使用权限是特别高的,别的线程压根就抢不到cpu的时间片。加了同步块之后,此时线程会产生阻塞(cpu的使用权限被别的线程抢去了)。产生阻塞之后会发生线程上下文切换。如下:
3.可见性
可见性是指一个线程对某个共享主内存变量进行修改之后,其他与该共享变量相关的线程会立马感知到这个数据的更改,即其他线程可以看到某个线程修改后的值。
上面例子中的两个线程,线程1修改了flag的值之后,线程2是load读取不到修改后的值的,最简单的修改方式是使用volatile关键字修改这个多线程共享的变量。
static volatile boolean flag = false;
输出结果如下:
线程: thread1 修改共享变量flag为true
线程: thread2 嗅探到flag状态的改变 flag:true
volatile底层原理:
volatile是Java虚拟机提供的轻量级的同步机制。
volatile语义有如下两个作用:
①可见性:保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了被volatile修饰的共享变量的值后,新值总是可以被其他线程立即得知。
②有序性:禁止指令重排序优化:内存屏障。
volatile缓存可见性实现原理:
JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步到主内存,使用时必须从主内存刷新,由此保证volatile可见性。
底层实现:通过汇编lock前缀指令,他会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器缓存失效。
汇编代码查看:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
缓存一致性原理再次剖析:
线程1跟线程2都已经将flag=false的值加载到各自的工作内存,此时flag的状态都是S状态(共享状态)。当线程2修改flag的值为true的时候,其状态变成了M状态,这个时候线程1所在的cpu嗅探到flag值修改后,将flag对应的缓存行状态设置为I(无效状态),然后线程1需要使用的时候由于值无效,需要重新加载,此时需要重新加载的话,需要线程2将修改的值添加到主内存,然后线程1才能够加载到正确的值。
Java内存模型内存交互操作:
把一个变量从主内存中复制到工作内存中,就需要按顺序执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按照顺序执行store和write操作。但是Java内存模型只要求上述操作必须按照顺序执行,而没有保证必须是连续执行的。
以上是顺序的而不是连贯的,注意read跟load必须成对出现,store跟write必须成对出现。