Java 关键字 volatile 的作用(2/2):禁止指令重排序

我们先来看一段 Java 代码,DCL (Double Check Lock) 单例模式:

package singleton;

public class Mgr06 {

    private volatile static Mgr06 INSTANCE;

    private Mgr06() {

    }

    public static Mgr06 getInstance() {

        if (INSTANCE == null) {

            synchronized (Mgr06.class) {

                if (INSTANCE == null) {

                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    INSTANCE = new Mgr06();

                }

            }

        }

        return INSTANCE;

    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args){

        for (int i = 0; i < 100; i++) {
            new Thread(()->
                    // 输出实例的哈希值
                    System.out.println(Mgr06.getInstance().hashCode())
            ).start();
        }

    }

}

那么,在定义静态变量 INSTANCE 的时候,需不需要加 volatile 关键字呢?

我们来详细探究一下对象的创建过程:

首先,我们先例举一段简单的创建对象的代码,Java 代码如下:

package test;

public class T {

    int m = 8;

    public static void main(String args[]) {

        T t = new T();

    }

}

我们知道,Java 代码转化成二进制代码前,需要先编译成 Java字节码,如 Java 文件 T.java 会先被 JVM (Java 虚拟机) 编译成 T.class 。

汇编码如下:

0 new #2 
3 dup
4 invokespecial #3 >
7 astore_1
8 return

该代码对象创建的示意图:

0 new #2 表示在内存中为类 T 申请一块空间,此时,类 T 的成员变量 m 有一个初始值。

因为 m 为 int 类型,因此 m 的初始值为 m = 0 。该状态为半初始化状态

Java 关键字 volatile 的作用(2/2):禁止指令重排序_第1张图片

4 invokespecial #3 > :这个汇编指令的作用是初始化变量 m 的值,让 m = 8 。

Java 关键字 volatile 的作用(2/2):禁止指令重排序_第2张图片

7 astore_1 :建立对象 t 与类 T 之间的关联。

Java 关键字 volatile 的作用(2/2):禁止指令重排序_第3张图片

 

再看回原来的 DCL (Double Check Lock)单例:

Java 关键字 volatile 的作用(2/2):禁止指令重排序_第4张图片

如上图所示:

当线程 T1 拿到这把锁的时候(Java 代码如下)

synchronized (Mgr06.class)

在执行 INSTANCE = new Mgr06() 创建对象的过程中,在 Java 字节码里,创建对象的具体过程是:

Java 关键字 volatile 的作用(2/2):禁止指令重排序_第5张图片

假设不加关键字 volatile ,导致 Java 字节码的指令发生了重排序( volatile 关键字的用作之一:禁止指令重排序)

Java 关键字 volatile 的作用(2/2):禁止指令重排序_第6张图片

如上图所示,指令 astore_1 和指令 invokespecial #3 > 发生了重排序。

那么,就会造成指令执行到 astore_1 的时候,对象 INSTANCE 和类 Mgr06 已经建立了关联

Java 关键字 volatile 的作用(2/2):禁止指令重排序_第7张图片

Java 代码里面有一个非空判断:

    public static Mgr06 getInstance() {

        if (INSTANCE == null) {

            synchronized (Mgr06.class) {

                if (INSTANCE == null) {

                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    INSTANCE = new Mgr06();

                }

            }

        }

        return INSTANCE;

    }

在第一个非空判断的时候,由于 INSTANCE 和类 Mgr06 已经发生了关联,那么就会造成 INSTANCE 不为空( INSTANCE == null 返回的结果为 false )

因此函数直接 return INSTANCE 。

但是,此时 INSTANCE 指向的是一个半初始化的对象,因为由于创建 INSTANCE 对象的 Java 字节码发生了指令重排序

此时,还没有执行指令 invokespecial #3 ,该指令的作用是给类 Mgr06 的成员变量进行初始化

那么,线程 T2 在执行 INSTANCE == null 条件判断语句的时候,条件判断返回的结果就会为 false (因为线程 T1 已经对该对象进行半初始化,该对象不为空)。

因此,线程 T2 直接执行:

return INSTANCE;

这样会造成线程 T2 返回了一个半初始化的对象,最终会影响程序执行的结果。因为,正确返回的结果应当是指令不发生重排序,正常初始化的对象。

因此,在 DCL 单例模式中,需要加 volatile 关键字,被 volatile 修饰的变量禁止指令重排序,这样可以防止在创建对象的过程中发生指令重排序,从而让程序运行的结果不被指令重排序所影响。

你可能感兴趣的:(Java,java,多线程,jvm,设计模式,编程语言)