ホームページ  >  記事  >  Java  >  Javaのvolatileキーワード実装のサンプルコードをルートから解析する(写真)

Javaのvolatileキーワード実装のサンプルコードをルートから解析する(写真)

黄舟
黄舟オリジナル
2017-03-22 10:47:231368ブラウズ

1. 分析概要

  1. メモリモデルの関連概念

  2. 3 つのことを並行して行うコンセプトのプログラミングvolatileキーワードを使用するためのvolatile

    keywords
  3. scenariosの概念モデル複数のスレッドによってアクセスされる変数は共有変数です。つまり、変数が複数の CPU にキャッシュされている場合 (通常はマルチスレッド プログラミングで発生します)、キャッシュの不整合の問題が発生する可能性があります。キャッシュの不整合の問題を解決するには、通常 2 つの解決策があります:
  4. バスに LOCK# を追加する

  5. キャッシュ整合性プロトコルを使用する

これら 2 つの方法はすべてハードウェア レベルで提供される方法です。

上記の方法 1 には、ロック期間中に他の CPU がメモリにアクセスできないため、効率が低下するという問題があります。

最も有名なものは Intel のキャッシュ コヒーレンス プロトコルであり、MESI プロトコルはコピーを保証します。各キャッシュで使用される共有変数の基本的な考え方は、CPU がデータを書き込むときに、操作された変数が共有変数であることが判明した場合、その変数のコピーがシグナルを送信するというものです。したがって、他の CPU がこの変数を読み取る必要があり、自身のキャッシュ内の変数のキャッシュ ラインが無効であることが判明すると、メモリから再読み取りされます。 3. 並行プログラミングの 3 つの概念

並行プログラミングでは、通常、原子性の問題、可視性の問題、順序付けの問題という 3 つの問題に遭遇します。

3.1 原子性
  • 原子性: つまり、1 つまたは複数の演算が完全に行われます。

    3.2 可視性
  • 可視性とは、複数のスレッドが同じ変数にアクセスするとき、1 つのスレッドが変数の値を変更する場合、他のスレッドが変更する場合を指します。スレッドは変更された値をすぐに確認できます。

    3.3 順序性: コード シーケンスから見ると、プログラムの実行順序はステートメント 2 の前にあるため、JVM がこれを実際に実行することになります。コードでは、ステートメント 1 がステートメント 2 より前に実行されることは保証されますか? なぜこれが可能になるのでしょうか?
一般的に、プログラムの動作効率を向上させるために、命令の順序変更が行われることについて説明します。入力コードを最適化する可能性がありますが、プログラム内の各ステートメントの実行順序がコード内の順序と同じであることは保証されませんが、プログラムの最終的な実行結果が次の結果と一致することが保証されます。コードの順次実行

命令の並べ替えは、単一スレッドの実行には影響しませんが、スレッドの同時実行の正確さに影響します。

言い換えれば、並行プログラムが正しく実行されるためには、原子性、可視性、および順序性が保証されなければなりません。いずれかが保証されていないと、プログラムが正しく動作しない可能性があります。

4. Java メモリ モデル

Java 仮想マシンの仕様では、さまざまなハードウェア プラットフォームとオペレーティング システム間のメモリ アクセスの違いを保護するために Java メモリ モデル (Java メモリ

モデル

、JMM) を定義する試みが行われています。 Java プログラムがさまざまなプラットフォームで一貫したメモリ アクセス効果を達成できるようにするためです。では、Java メモリ モデルは何を規定するのでしょうか? それは、プログラム内の変数のアクセス ルールを定義するものであり、より広い意味で、プログラムの実行順序を定義します。より良い実行パフォーマンスを得るために、Java メモリ モデルは、命令の実行速度を向上させるために実行エンジンがプロセッサのレジスタやキャッシュを使用することを制限したり、コンパイラが命令を並べ替えたりすることを制限しないことに注意してください。言い換えれば、Java メモリ モデルでは、キャッシュの一貫性の問題や命令の並べ替えの問題も発生します。

Java メモリ モデルでは、すべての変数がメイン メモリ (前述の物理メモリと同様) に格納され、各スレッドが独自の作業メモリ (前のキャッシュと同様) を持つことが規定されています。スレッドによる変数の操作はすべて作業メモリ内で実行する必要があり、メインメモリ上で直接操作することはできません。また、各スレッドは他のスレッドの作業メモリにアクセスできません。

4.1 アトミック性

Java では、基本的な

データ型

の変数への読み取りおよび代入操作はアトミック操作です。つまり、これらの操作は中断できず、実行されるか実行されないかのどちらかです。

次のどの操作がアトミック操作であるかを分析してください:

x = 10; //ステートメント 2

x++; //ステートメント 3

x = x + 1 //ステートメント 4

;

実際、ステートメント 1 だけがアトミック操作であり、他の 3 つのステートメントはアトミック操作ではありません。

言い換えれば、単純な読み取りと代入 (そして数値は変数に代入する必要があり、変数間の相互代入はアトミック操作ではありません) のみがアトミック操作です。

上記からわかるように、Java メモリ モデルは、基本的な読み取りと割り当てがアトミックな操作であることのみを保証します。より広範囲の操作でアトミック性を実現したい場合は、同期化とロックを使用してそれを実現できます。

4.2 可視性

可視性のために、Java は可視性を確保するために volatile キーワードを提供します。

共有変数が volatile に変更されると、他のスレッドがそれを読み取る必要があるときに、変更された値が直ちにメイン メモリに更新され、メモリから新しい値が読み取られます。

通常の共有変数は、変更された後、いつメインメモリに書き込まれるかが不確実であるため、他のスレッドがそれを読み取るときに、その時点では元の古い値がメモリ内に残っている可能性があるため、可視性を保証できません。 . なので視認性は保証されません。

さらに、同期とロックによって可視性も確保できます。同期とロックを使用すると、1 つのスレッドのみがロックを取得して同期コードを実行し、変数への変更が前にメイン メモリにフラッシュされます。ロックを解除します。したがって、視認性は保証されます。

4.3 順序性

Java メモリ モデルでは、コンパイラとプロセッサは命令を並べ替えることができますが、並べ替えプロセスはシングルスレッド プログラムの実行には影響しませんが、マルチスレッドの同時実行の正しい実行には影響します。 .セックス。

Java では、 volatile キーワードを使用して、特定の「順序性」を確保できます (命令の並べ替えを禁止できます)。さらに、synchronized と Lock によって順序性を確保できます。 synchronized と Lock は、各瞬間に 1 つのスレッドが同期コードを実行することを保証します。これは、スレッドに同期コードを順番に実行させることと同じであり、自然に順序性が保証されます。

さらに、Java メモリ モデルには、何らかの固有の「順序性」、つまり、手段を選ばずに保証できる順序性があり、これはよく「happens-before-principle」と呼ばれます。 2 つの操作の実行順序が前発生の原則から推定できない場合、それらの順序は保証されず、仮想マシンはそれらを自由に並べ替えることができます。

以下は、事前発生の原則の詳細な紹介です:

  1. プログラムシーケンス規則: スレッド内では、コードの順序に従って、前に書かれた操作が後ろに書かれた操作よりも先に発生します

  2. ロック ルール: ロック解除操作は、同じロックによる後続のロック操作の前に発生します

  3. volatile変数ルール: 変数に対する書き込み操作は、この変数に対する後続の読み取り操作の前に発生します

  4. 渡しルール:オペレーション A がオペレーション B の前に最初に発生し、オペレーション B がオペレーション C の前に最初に発生した場合、オペレーション A がオペレーション C の前に最初に発生すると結論付けることができます

  5. スレッド起動ルール: Thread オブジェクトの start() メソッドが最初に発生します。このスレッドのアクション

  6. スレッド中断ルール: スレッドのinterrupt()メソッドの呼び出しは、中断されたスレッドのコードが割り込みイベントの発生を検出する前に最初に発生します

  7. スレッド終了ルール: スレッド内のすべてのスレッドすべての操作は、スレッドが終了するときに最初に行われます。スレッドが終了したことは、Thread.join() メソッドと Thread.isAlive() の戻り値によって検出できます。つまり、オブジェクトの初期化が最初に完了します。彼の Finalize() メソッドの最初に発生します

  8. これら 8 つのルールのうち、最初の 4 つのルールがより重要であり、最後の 4 つのルールは明らかです。

  9. 最初の 4 つのルールを説明しましょう:

プログラム順序のルールについては、プログラム コードの一部の実行が単一のスレッド内で順序正しく実行されているように見える、というのが私の理解です。このルールでは、「前に書かれた操作は後ろに書かれた操作の前に最初に発生する」と述べていますが、これは、プログラムが実行されるように見える順序がコードの順序であることを意味する必要があることに注意してください。プログラムコードの変更を実行し、順序を変更します。並べ替えは実​​行されますが、最終的な実行結果はプログラムの順次実行の結果と一致します。並べ替えられるのは、データ依存関係のない命令のみです。したがって、単一スレッドでは、プログラムの実行は順番に実行されるように見えますが、これを理解する必要があります。実際、このルールは単一スレッドでのプログラム実行結果の正確性を保証するために使用されますが、複数スレッドでのプログラム実行の正確性は保証できません。

  1. 2 番目のルールも理解しやすいです。つまり、シングル スレッドでもマルチスレッドでも、同じロックがロック状態にある場合は、ロックを続行する前にロックを解除する必要があります。手術。

  2. 3 番目のルールはより重要なルールであり、次の記事の焦点でもあります。直感的に説明すると、スレッドが最初に変数を書き込み、次にスレッドがそれを読み取る場合、読み取り操作の前に書き込み操作が確実に発生します。

  3. 4 番目のルールは、実際には、前に発生する原則の推移的な性質を反映しています。

5. volatile キーワードの詳細な分析

5.1 Volatile キーワードの 2 レベルのセマンティクス

共有変数 (クラスのメンバー変数、クラスの静的メンバー変数) が変更されると、 volatile には 2 つのレベルのセマンティクスがあります。 セマンティクス:

  1. は、さまざまなスレッドがこの変数を操作するときの可視性を保証します。つまり、1 つのスレッドが変数の値を変更すると、新しい値は他のスレッドからすぐに可視になります。

  2. 命令の再注文は禁止されています。

可視性に関して、最初にスレッド 1 が実行され、その後スレッド 2 が実行される場合のコードを見てみましょう。

//线程1
boolean stop = false;
while(!stop){
doSomething();
}

//线程2
stop = true;

このコードは、次の場合にこのマーキング方法を使用する可能性があります。スレッドを中断します。しかし実際、このコードは完全に正しく実行されるでしょうか?つまり、スレッドは中断されますか?必ずしもそうとは限りませんが、ほとんどの場合、このコードはスレッドを中断する可能性がありますが、スレッドを中断できなくなる可能性もあります (この可能性は非常に低いですが、一度これが発生すると、無限ループが発生します)。

このコードによりスレッドが中断できなくなる理由を以下に説明します。前に説明したように、各スレッドは実行中に独自の作業メモリを持っているため、スレッド 1 の実行中に stop 変数の値をコピーし、それを独自の作業メモリに置きます。

つまり、スレッド 2 が stop 変数の値を変更すると、それをメインメモリに書き込む前に、スレッド 2 は他の処理に切り替わり、スレッド 1 はスレッド 2 による stop 変数の変更を知りません。 、したがって、それはまだそのサイクルが続きます。

しかし、volatile で変更された後は異なります。

  • 1 つ目: volatile キーワードを使用すると、変更された値が直ちにメイン メモリに書き込まれます

  • 2 つ目: volatile キーワードが使用された場合、スレッド 2 が変更を行うと、スレッド 1 の作業メモリ内のキャッシュ変数 stop のキャッシュ ラインが無効になります (ハードウェア層に反映されると、CPU の L1 または L2 キャッシュ内の対応するキャッシュ ラインが無効になります)。

  • 3 番目: スレッド 1 の作業メモリ内のキャッシュ変数 stop のキャッシュ ラインが無効であるため、スレッド 1 はメイン メモリに移動して変数 stop の値を再度読み取ります。

次に、スレッド 2 が停止値を変更すると (もちろん、これには、スレッド 2 の作業メモリ内の値を変更し、変更された値をメモリに書き込むという 2 つの操作が含まれます)、変数がキャッシュされます。スレッド 1 の作業メモリ内で停止キャッシュ ラインが無効であるため、スレッド 1 がそれ​​を読み取ると、そのキャッシュ ラインが無効であることがわかり、キャッシュ ラインに対応するメイン メモリ アドレスが更新されるまで待機します。対応するメインメモリに移動して最新の値を読み取ります。

その場合、スレッド 1 が読み取るのは最新の正しい値です。

5.2 volatile はアトミック性を保証しますか?

Volatile はアトミック性を保証しません。以下の例を見てみましょう。

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

皆さん、このプログラムの出力について考えてください?おそらく友人の中には10,000だと思っている人もいるでしょう。しかし、実際に実行してみると、結果は毎回一貫性がなく、常に 10,000 未満の数値であることがわかります。

ここで誤解があります。 volatile キーワードは可視性を保証できますが、上記のプログラムのエラーは原子性を保証できないことです。可視性は常に最新の値が読み取られることを保証することしかできませんが、揮発性は変数に対する操作のアトミック性を保証できません。

前述したように、自動インクリメント操作には、変数の元の値の読み取り、1 の加算、作業メモリへの書き込みが含まれます。これは、自動インクリメント操作の 3 つのサブ操作が個別に実行される可能性があることを意味し、次のような状況が生じる可能性があります:

ある時点で変数 inc の値が 10 である場合。

スレッド 1 は変数の自動インクリメント操作を実行します。スレッド 1 は最初に変数 inc の元の値を読み取り、次にスレッド 1 がブロックされます。次に、スレッド 2 が変数の自動インクリメント操作を実行します。 2 は変数 inc も読み取ります。スレッド 1 は変数 inc を読み取るだけで変数を変更しないため、スレッド 2 の作業メモリ内の変数 inc のキャッシュ ラインは無効になりません。そのため、スレッド 2 はメインに直接進みます。メモリ inc の値を読み取って、inc の値が 10 であることを確認し、次に 1 を加算して 11 を作業メモリに書き込み、最後にそれをメイン メモリに書き込みます。

その後、スレッド 1 が 1 を加算します。 inc の値が読み取られているため、この時点ではスレッド 1 の作業メモリ内の inc の値はまだ 10 であることに注意してください。そのため、スレッド 1 の後の inc の値は inc に 1 を加算します。 . が 11 の場合、11 は作業メモリに書き込まれ、最後にメインメモリに書き込まれます。

その後、2 つのスレッドがそれぞれ自動インクリメント操作を実行した後、inc は 1 だけ増加しました。

これを説明すると、質問する人もいるかもしれません。変数が volatile 変数を変更すると、キャッシュ ラインは無効になることが保証されているのではありませんか?その後、他のスレッドが新しい値を読み取ります。はい、これは正しいです。これは、上記の事前発生ルールの揮発性変数ルールですが、スレッド 1 が変数を読み取ってブロックされた後は、inc 値が変更されないことに注意してください。この場合、volatile はスレッド 2 がメモリから変数 inc の値を確実に読み取ることができますが、スレッド 1 は変数 inc の値を変更しないため、スレッド 2 には変更された値がまったく表示されません。

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

把上面的代码改成以下任何一种都可以达到效果:

采用synchronized:

public class Test {
    public  int inc = 0;

    public synchronized void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();

    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();

    public  void increase() {
        inc.getAndIncrement();
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

5.3 volatile能保证有序性吗?

volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

举个例子:

//x、y为非volatile变量
//flag为volatile变量

x = 2;         //语句1
y = 0;         //语句2
flag = true;   //语句3
x = 4;         //语句4
y = -1;        //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

5.4 volatile的原理和实现机制

这里探讨一下volatile到底如何保证可见性和禁止指令重排序的。

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

6、使用volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

  1. 对变量的写操作不依赖于当前值(比如++操作,上面有例子)

  2. 该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

下面列举几个Java中使用volatile的几个场景。

状态标记量

volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

double check

class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

至于为何需要这么写请参考:


以上がJavaのvolatileキーワード実装のサンプルコードをルートから解析する(写真)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。