内存可见性以及synchronized实现可见性
一、内存可见性
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."":()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指令实现的: