インタビューアサルト51:なぜシングルトンに揮発性物質を追加しなければならないのですか?

創造を続け、成長を加速させましょう!「ナゲッツデイリーニュープラン・6月アップデートチャレンジ」に参加して3日目です。クリックしてイベントの詳細をご覧ください。

シングルトンモードを実装するには、空腹モード、レイジーモード、静的内部クラス、列挙型など、さまざまな方法があります。インタビュアーが「シングルトンモードで揮発性を追加する必要があるのはなぜですか」と尋ねると、なぜ揮発性である必要があるのか​​を意味します。レイジーモードでプライベート変数に追加されましたか?

遅延モードとは、遅延読み込みによってオブジェクトが作成される方法を指します。オブジェクトは、プログラムの起動時には作成されませんが、実際に初めて使用されるときに作成されます。

なぜ揮発性を追加するのか説明するには?まず、レイジーモードの特定の実装コードを見てみましょう。

public class Singleton {
    // 1.防止外部直接 new 对象破坏单例模式
    private Singleton() {}
    // 2.通过私有变量保存单例对象【添加了 volatile 修饰】
    private static volatile Singleton instance = null;
    // 3.提供公共获取单例对象的方法
    public static Singleton getInstance() {
        if (instance == null) { // 第 1 次效验
            synchronized (Singleton.class) {
                if (instance == null) { // 第 2 次效验
                    instance = new Singleton(); 
                }
            }
        }
        return instance;
    }
}
复制代码

上記のコードからわかるように、スレッドの安全性と高性能を確保するために、コードは2つのifを使用し、同期してプログラムの実行を保証します。スレッドセーフを確保するためにすでに同期されているので、なぜ変数に揮発性を追加するのですか?この問題を説明する前に、まず前提条件を理解する必要があります。揮発性物質の使用は何ですか?

1.不安定な役割

Volatileには2つの主要な機能があります。1つはメモリの可視性の問題を解決するため、もう1つは命令の並べ替えを防ぐためです。

1.1メモリの可視性の問題

いわゆるメモリ可視性の問題とは、複数のスレッドによる変数の同時操作を指します。1つのスレッドが変数の値を変更した後、他のスレッドは変数の変更を認識できません。これがメモリ可視性の問題です。 また、volatileを使用すると、次のコードなどのメモリの可視性の問題を解決できます。volatileが追加されていない場合、その実装は次のようになります。

private static boolean flag = false;
public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            // 如果 flag 变量为 true 就终止执行
            while (!flag) {

            }
            System.out.println("终止执行");
        }
    });
    t1.start();
    // 1s 之后将 flag 变量的值修改为 true
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("设置 flag 变量的值为 true!");
            flag = true;
        }
    });
    t2.start();
}
复制代码

上記プログラムの実行結果は以下のとおりです。image.pngただし、上記プログラムを長時間実行しても実行は終了しません。つまり、スレッド2がフラグ変数を変更した後、スレッド1は変更を認識しません。変数のすべて。次に、フラグにvolatileを追加しようとします。実装コードは、次のとおりです。

public class volatileTest {
    private static volatile boolean flag = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 如果 flag 变量为 true 就终止执行
                while (!flag) {

                }
                System.out.println("终止执行");
            }
        });
        t1.start();
        // 1s 之后将 flag 变量的值修改为 true
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("设置 flag 变量的值为 true!");
                flag = true;
            }
        });
        t2.start();
    }
}
复制代码

以上程序的执行结果如下: image.png 从上述执行结果我们可以看出,使用 volatile 之后就可以解决程序中的内存可见性问题了。

1.2 防止指令重排序

指令重排序是指在程序执行过程中,编译器或 JVM 常常会对指令进行重新排序,已提高程序的执行性能。 指令重排序的设计初衷确实很好,在单线程中也能发挥很棒的作用,然而在多线程中,使用指令重排序就可能会导致线程安全问题了。

所谓线程安全问题是指程序的执行结果,和我们的预期不相符。比如我们预期的正确结果是 0,但程序的执行结果却是 1,那么这就是线程安全问题。

而使用 volatile 可以禁止指令重排序,从而保证程序在多线程运行时能够正确执行。

2.为什么要用 volatile?

回到主题,我们在单例模式中使用 volatile,主要是使用 volatile 可以禁止指令重排序,从而保证程序的正常运行。这里可能会有读者提出疑问,不是已经使用了 synchronized 来保证线程安全吗?那为什么还要再加 volatile 呢?看下面的代码:

public class Singleton {
    private Singleton() {}
    // 使用 volatile 禁止指令重排序
    private static volatile Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) { // ①
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // ②
                }
            }
        }
        return instance;
    }
}
复制代码

注意观察上述代码,我标记了第 ① 处和第 ② 处的两行代码。给私有变量加 volatile 主要是为了防止第 ② 处执行时,也就是“instance = new Singleton()”执行时的指令重排序的,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:

  1. 创建内存空间。
  2. 在内存空间中初始化对象 Singleton。
  3. 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。

揮発性が追加されていない場合、スレッド1は上記のコードの2番目のポイントを実行するときに命令の並べ替えを実行し、1、2、および3の実行順序を1、3、および2に並べ替えることができます。ただし、特別な場合、スレッド1がステップ3を実行した後、スレッド2が来て上記のコードの最初のステップを実行すると、インスタンスオブジェクトはヌルではないと判断されますが、この時点でスレッド1はオブジェクトのインスタンス化を完了していません。 、次に、スレッド2はインスタンス化された「half」オブジェクトを取得し、プログラムが正しく実行されないようにします。これが、volatileがプライベート変数に追加される理由です。

要約する

volatileを使用すると、メモリの可視性の問題を解決し、命令の並べ替えを防ぐことができます。シングルトンモードでvolatileを使用するのは、主にvolatileの後者の機能(命令の並べ替えを防ぐ)を使用するためです。これにより、並べ替えによるマルチスレッド実行のケースを回避できます。また、一部のスレッドで完全にインスタンス化されていないオブジェクトを取得すると、プログラムの実行エラーが発生します。

何が正しく何が間違っているかを判断し、他の人の話を聞き、利益と損失を数えるのは自分次第です。

公式アカウント:Javaインタビューの質問の分析

インタビューコレクション:gitee.com/mydb/interv…

おすすめ

転載: juejin.im/post/7102222154518757383