一、内存可见性
1、内存可见性介绍
可见性: 一个线程对共享变量值的修改,能够及时的被其他线程看到
共享变量: 如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:
(1)、首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
(2)、然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。
如上图所示,本地内存 A 和 B 有主内存中共享变量 x 的副本。假设初始时,这三个内存中的 x 值都为0。线程 A 在执行时,把更新后的 x 值(假设值为 1)临时存放在自己的本地内存 A 中。当线程 A 和线程 B 需要通信时,线程 A 首先会把自己本地内存中修改后的 x 值刷新到主内存中,此时主内存中的 x值变为了 1。随后,线程 B 到主内存中去读取线程 A 更新后的 x 值,此时线程 B 的本地内存的 x 值也变为了 1。
从整体来看,这两个步骤实质上是线程 A 在向线程 B 发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证。
JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。
2、可见性问题
前面讲过多线程的内存可见性,现在我们写一个内存不可见的问题。
案例如下:
public class Demo1Jmm { public static void main(String[] args) throws InterruptedException { JmmDemo demo = new JmmDemo(); Thread t = new Thread(demo); t.start(); Thread.sleep(100); demo.flag = false; System.out.println("已经修改为false"); System.out.println(demo.flag); } static class JmmDemo implements Runnable { public boolean flag = true; public void run() { System.out.println("子线程执行。。。"); while (flag) { } System.out.println("子线程结束。。。"); } } }
结果:
子线程执行。。。 已经修改为false false
按照 main方法的逻辑,我们已经把flag设置为false,那么从逻辑上讲,子线程就应该跳出while死循环,因为这个时候条件不成立,但是我们可以看到,程序仍旧执行中,并没有停止。
原因:线程之间的变量是不可见的,因为读取的是副本,没有及时读取到主内存结果。
解决办法:强制线程每次读取该值的时候都去“主内存”中取值
二、synchronized实现可见性
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个线程执行synchronized声明的代码块。还可以保证共享变量的内存可见性。
同一时刻只有一个线程执行,这部分代码块的重排序也不会影响其执行结果。也就是说使用了synchronized可以保证并发的原子性,可见性,有序性。
1、解决可见性问题
JMM关于synchronized的两条规定:
线程解锁前(退出同步代码块时):必须把自己工作内存中共享变量的最新值刷新到主内存中
线程加锁时(进入同步代码块时):将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁)
做如下修改,在死循环中添加同步代码块
while (flag) { synchronized (this) { } }
代码如下:
public class Demo1Jmm { public static void main(String[] args) throws InterruptedException { JmmDemo demo = new JmmDemo(); Thread t = new Thread(demo); t.start(); Thread.sleep(100); demo.flag = false; System.out.println("已经修改为false"); System.out.println(demo.flag); } static class JmmDemo implements Runnable { public boolean flag = true; public void run() { System.out.println("子线程执行。。。"); while (flag) { synchronized (this){ } } System.out.println("子线程结束。。。"); } } }
结果如下:
子线程执行。。。 已经修改为false 子线程结束。。。 false
synchronized实现可见性的过程
(1)、获得互斥锁(同步获取锁)
(2)、清空本地内存
(3)、从主内存拷贝变量的最新副本到本地内存
(4)、执行代码
(5)、将更改后的共享变量的值刷新到主内存
(6)、释放互斥锁
2、同步原理
synchronized的同步可以解决原子性、可见性和有序性的问题,那是如何实现同步的呢?
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
(1)、普通同步方法,锁是当前实例对象this
(2)、同步方法块,锁是括号里面的对象
(3)、静态同步方法,锁是当前类的class对象
当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁。
synchronized的同步操作主要是monitorenter(监听进入)和monitorexit(监听退出)这两个jvm指令实现的,先写一段简单的代码:
public class Demo2Synchronized { public void test2() { synchronized (this) { } } }
在 cmd命令行执行javac编译和javap -c Java 字节码的指令
D:\project\prism\java9-test\src\test\java\com\zwh>javac Demo2Synchronized.java D:\project\prism\java9-test\src\test\java\com\zwh>javap -c Demo2Synchronized.class Compiled from "Demo2Synchronized.java" public class com.zwh.Demo2Synchronized { public com.zwh.Demo2Synchronized(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void test2(); Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_1 5: monitorexit 6: goto 14 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow 14: return Exception table: from to target type 4 6 9 any 9 12 9 any }
从结果可以看出,同步代码块是使用monitorenter和monitorexit这两个jvm指令实现的:
三、Volatile实现可见性
通过前面内容我们了解了synchronized,虽然JVM对它做了很多优化,但是它还是一个重量级的锁。而接下来要介绍的volatile则是轻量级的synchronized。
如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。
Java语言规范对volatile的定义如下:
Java允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是内存可见性。
volatile虽然看起来比较简单,使用起来无非就是在一个变量前面加上volatile即可,但是要用好并不容易。
1、解决内存可见性问题
如果不加volatile
public class Demo1Jmm {
public static void main(String[] args) throws InterruptedException {
JmmDemo demo = new JmmDemo();
Thread t = new Thread(demo);
t.start();
Thread.sleep(100);
demo.flag = false;
System.out.println("已经修改为false");
System.out.println(demo.flag);
}
static class JmmDemo implements Runnable {
public boolean flag = true;
public void run() {
System.out.println("子线程执行。。。");
while (flag) { // 如果flag为true就会一直循环,不会结束
}
System.out.println("子线程结束。。。");
}
}
}
执行后,打印如下:
发现并没有停止。
原因:默认情况下,内存是不可见的,即线程之间的数据是不可见的,因为使用的是自己本地内存的数据,别的线程是看不到自己本地内存的数据的,故子线程一直把flag当作true不断的循环而不会停止。
在可见性问题案例中进行如下修改,添加volatile关键词,变成volatile变量:
private volatile boolean flag = true;
代码如下:
public class Demo1Jmm {
public static void main(String[] args) throws InterruptedException {
JmmDemo demo = new JmmDemo();
Thread t = new Thread(demo);
t.start();
Thread.sleep(100);
demo.flag = false;
System.out.println("已经修改为false");
System.out.println(demo.flag);
}
static class JmmDemo implements Runnable {
public volatile boolean flag = true;
public void run() {
System.out.println("子线程执行。。。");
while (flag) { // 如果步修改flag的值为false,就会一直循环,不会停止
}
System.out.println("子线程结束。。。");
}
}
}
结果如下:
子线程执行。。。
已经修改为false
子线程结束。。。
false
Volatile实现内存可见性的过程
线程写Volatile变量的过程:
(1)、改变线程本地内存中Volatile变量副本的值;
(2)、将改变后的副本的值从本地内存刷新到主内存;
线程读Volatile变量的过程:
(1)、从主内存中读取Volatile变量的最新值到线程的本地内存中;
(2)、从本地内存中读取Volatile变量的副本
Volatile 实现内存可见性原理(了解):
写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中;
读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值
PS: 内存屏障(Memory Barrier)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序
volatile的底层实现是通过插入内存屏障,但是对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM采用了保守策略。如下:
(1)、StoreStore 屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
(2)、StoreLoad 屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
(3)、LoadLoad 屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
(4)、LoadStore 屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
java内存模型为了实现volatile
可见性和禁止指令重排两个语义,使用如下内存屏障插入策略:
(1)、每个volatile
写操作前边插入Store-Store
屏障,后边插入Store-Load(全能)
屏障;
(2)、每个volatile
读操作前边插入Load-Load
屏障和Load-Stroe
屏障;
如图所示:写volatile
屏障指令插入策略可以保证在volatile
写之前,所有写操作都已经刷新到主存对所有处理器可见了。其后全能型屏障指令为了避免写volatile
与其后volatile
读写指令重排序。
读volatile
时,会在其后插入两条指令防止volatile
读操作与其后的读写操作重排序。