内存可见性以及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指令实现的: