JMMデザインアイデア

JMMデザインアイデア

(1)なぜJMMが必要なのか

以下は、最新のプロセッサアーキテクチャモデルの図です。

ここに画像の説明を挿入

現在のプロセッサアーキテクチャのほとんどは、マルチコア+マルチレベルキャッシュ+メインメモリモードです。このように、マルチスレッドシナリオではデータの競合が発生し、キャッシュの不整合が発生します。さらに、CPUはプログラムを最適化して命令を並べ替える場合があります。命令の並べ替え後にプログラムのセマンティクスが変更されない限り、命令の再配置が可能です(命令の再配置はコンパイラとJVMにも存在します)。マルチスレッド実行の結果は予期しないものになります。

プロセッサの場合、メモリモデルは、他のプロセッサのメモリへの書き込みが現在のプロセッサから見えるようにし、現在のプロセッサの書き込みが他のプロセッサから見えるようにするために十分かつ必要な条件を定義します。一部のプロセッサは強力なメモリモデルを使用します。つまり、すべてのプロセッサは特定のメモリ位置で常に正確に同じ値を参照できますが、これは絶対的なものではなく、完了するために特別な命令(メモリバリア)が必要になる場合があります。 。

一部のプロセッサは弱いメモリモデル使用し、他のプロセッサの書き込み操作を表示したり、このプロセッサの書き込み操作を他のプロセッサから見えるようにするために、ローカルプロセッサキャッシュを更新または無効にするメモリバリアを必要とします。これらのメモリバリアは通常、ロックおよびロック解除中に実行され、高級言語を使用するプログラマには見えません。プロセッサーの設計傾向は、その仕様がよりスケーラブルであるため、弱いメモリーモデルの使用を奨励することです。

ここに画像の説明を挿入

Javaスレッド間の通信は、Javaメモリモデル(JMM)によって制御されます。JMMは、1つのスレッドによる共有変数の書き込みが別のスレッドにいつ表示されるかを決定します。抽象的観点から、JMMはスレッドとメインメモリ間の抽象的な関係を定義します。スレッド間の共有変数はメインメモリに格納され、各スレッドにはプライベートローカルメモリ(ローカルメモリ)があります。 、シェア変数を読み書きするスレッドのコピーはローカルメモリに保存されます。ローカルメモリはJMMの抽象的な概念であり、実際には存在しません。キャッシュ、書き込みバッファ、レジスタ、およびその他のハードウェアとコンパイラの最適化について説明します。Javaメモリモデルの抽象図は次のとおりです。

ここに画像の説明を挿入
簡単に言うと、当面は知っておく必要があります。JMMは仕様です。目的は、複数のスレッドが共有メモリを介して通信し、コンパイラがコード命令を並べ替え、プロセッサがコードを乱す場合のローカルメモリデータの不整合を解決することです。実装等によるトラブル 簡単に言えば、JMMの目的は、並行プログラミングシナリオにおける原子性(プロセッサ最適化問題)、可視性(キャッシュ整合性問題)、順序(命令再配置問題)の3つの主要な問題を解決することです。

JMMの役割を要約すると、Javaメモリモデル(JMM)は、すべての変数がメインメモリに格納され、各スレッドが独自の作業メモリを持つことを規定しています。スレッドで使用される変数のメインメモリのコピーは、スレッドのワーキングメモリに格納されます。スレッドによる変数のすべての操作は、ワーキングメモリで実行する必要があり、メインメモリを直接読み書きすることはできません。異なるスレッドが互いのワーキングメモリ内の変数に直接アクセスすることはできません。スレッド間での変数の転送には、独自のワーキングメモリとメインメモリ間のデータ同期が必要です。JMMは、ワーキングメモリとメインメモリ間のデータ同期プロセスに作用します。データ同期を行う方法とデータ同期を行うタイミングを指定します。

JMMの3つの主要な問題を見て、これらの問題に対するJMMのソリューションの設計アイデアについて考えてみましょう。特定のソリューションについては、後の記事で個別に説明します。

(2)原子性

CPUが操作を実行するとき、共有変数を共有メモリからCPUキャッシュに最初にロードし、操作が完了した後で最新のデータを共有メモリに書き戻す必要があることは誰もが知っています。実際、この一見完璧に機能する方法には問題があります。この問題について以下で説明します。

現在のシステム環境がシングルコアCPU +マルチスレッドの作業モードの場合、共有変数の初期値は1で、スレッド1とスレッド2はそれぞれこの共有変数に1を加算します。理論上、この共有変数の最終値は3です。プログラムの実行動作が私たちの期待と一致するかどうか見てみましょう。

スレッドによって共有変数に1を追加するプロセスには、3つのステップが必要です。

  1. シェア変数を作業メモリに読み込む
  2. +1から共有変数
  3. シェア変数をメインメモリに書き戻す

ただし、上記の3つのステップがアトミック操作ではないことは明らかです。つまり、途中で中断される可能性があります。ここで、スレッド1がステップ1の実行を終了したが、CPUタイムスライスが使い果たされた場合、スレッド2は実行する機会を得て、共有変数の値をメモリからロードし(この時点で共有変数の値はまだ1です)、最後の2つのスレッドがステップ2を終了します。 step3の後の共有変数の値は3ではなく2です。

上記の問題の原因は、共有変数に1を追加する操作がアトミック操作ではないことです。いわゆるアトミック操作は、1つ以上の操作を指し、すべて実行され、実行中に何らかの要因によって中断されないか、すべて実行されません。マルチスレッド環境では、アトミック性の問題により、実行結果が不正確になる場合があります。

JMMの設計者であれば、原子性の問題を解決する方法を考えることができます。データベースシステムについて詳しく知っている友人は、すぐにロックの使用を考えなければなりません。ロックの機能は、変数またはオブジェクトを一時的にロックして、同時に1つのスレッドのみが操作できるようにすることです。スレッドがロックを解除した後でのみ、他のスレッドが変数またはオブジェクトを操作できます。これは、同期されたキーワードの機能ですが、原子性を実際に保証することはできません。コード実行の最終結果が正しいことだけが保証されます。

(3)可視性の問題

マルチコアCPUがマルチスレッドプログラミングを実行するときに存在するもう1つの重要な問題は、キャッシュの一貫性です。例を見てみましょう:

ここに画像の説明を挿入

上の図は一例であり、マルチコアCPUマルチスレッド環境では、2つのスレッドが共有変数aに1を加算します。上の図に示すように、両方のスレッドがメモリ内の共有変数aの値を作業メモリにロードします。しかし、この時点でスレッド2はCPUタイムスライスを失い、スレッド1は実行を継続し、変数を1ずつ正常にインクリメントします。スレッド1を実行すると、メモリ内の値は次の図のようになります。

ここに画像の説明を挿入

スレッド2の変数aの値は、変数aの最新の値ではなく、期限切れの値になっていることがわかったため、スレッド2を実行すると、変数aは必要な値3ではありません。この問題は、マルチコアCPUのキャッシュコヒーレンシの問題であり、可視性の問題とも呼ばれます。この問題は、あるスレッドによる変数の変更が別のスレッドからは見えないことが原因です。アトミック性の問題とは異なり、キャッシュコヒーレンシの問題はマルチコアおよびマルチスレッド環境でのみ発生しますが、アトミック性の問題は、マルチスレッド環境で発生する限り発生します。

私たちがJMMデザイナーであれば、可視性の問題を解決する方法を考えることができます。別のスレッドからは見えない1つのスレッドによる変数の変更の根本的な原因は、キャッシュとメモリの値が同期していないためであることがわかります。したがって、キャッシュとメモリの一貫性を確保する方法を考える必要があります。実際、同期を確実にするために必要なことは2つだけです。

  1. スレッドが変数のコピーを変更すると、すぐに最新の値がメインメモリに更新されます。
  2. スレッドが変数のコピーを変更すると、他のスレッドのこの変数のコピー値は無効になります。他のスレッドがこの共有変数を読み書きする必要がある場合は、メインメモリから再読み込みする必要があります。

これら2つのポイントを実行している限り、キャッシュとメインメモリの整合性を確保できることは容易に想像できます。JMMも同じことを行いますが、その最下層ではメモリバリアと呼ばれるものを使用しますが、これについては今後の記事で説明します。

(4)秩序性

順序の問題は、主に命令の並べ替えが原因で発生します。いわゆる命令の並べ替えとは、CPUがセマンティクス(シングルスレッド)を変更せずにコードのアウトオブオーダー実行を実行して、内部プロセッサユニットを完全に使用できるようにする動作を指します。この命令の順序変更動作は、シングルスレッド環境では問題を引き起こしませんが、マルチスレッド環境ではプログラムの実行結果が正しくない場合があります。問題を引き起こす可能性のある例を見てみましょう:

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

上記のコードはシングルトンモードを実装するコードの一部ですが、コードのInstance変数はvolatileキーワードで変更されていません。したがって、このような問題が発生します。スレッドが実行してinstance == nullを判断すると、コードはインスタンスがnullではないことを読み取りますが、インスタンスによって参照されるオブジェクトはまだ初期化されていない可能性があります。

ステートメントinstance = new Singleton()に3つのプロセスが含まれていることは誰でも知っています。

  1. オブジェクトのメモリ空間を割り当てる
  2. 初期化オブジェクト
  3. 新しく割り当てられたメモリ空間を指すようにインスタンスを設定します

これら3つのプロセスを並べ替えることができるかどうか見てみましょう。まず、並べ替えによってセマンティクスを変更することはできません。明らかに、1と3の2つのプロセスでは、1は3の前でなければなりません。そうでない場合、セマンティクスは正しくありません。次に、1と2の2つのプロセスでは、メモリが割り当てられていないとオブジェクトを初期化できないため、1は2の前になければなりません。この観点から、2と3の順序を交換できます。つまり、プロセッサーはパフォーマンスを改善するために2と3の順序を変更できます。したがって、プロセスは次のようになります。

  1. オブジェクトのメモリ空間を割り当てる
  2. 新しく割り当てられたメモリ空間を指すようにインスタンスを設定します
  3. 初期化オブジェクト

この時点で、プログラムが実行を開始したばかりで、オブジェクトがまだ作成されていないとします。スレッド1がオブジェクトの作成を開始します。2番目のステップが実行されると、インスタンスはnullではなくなりますが、オブジェクトは初期化されていません。たまたま、このときスレッド2もオブジェクトの作成を開始します。instance== nullと判断された場合、instanceはnullではないため、初期化されていないオブジェクトの参照を直接返します。これは非常に致命的なエラーです。

順序の問題を解決する方法も非常に単純で、問題が発生する可能性のある場所での再順序付けを停止します。具体的な方法については、後の記事で説明します。

(5)まとめ

Javaのマルチスレッディングは共有メモリを介して通信します。通信に共有メモリを使用するため、通信プロセスでは原子性、可視性、順序などの一連の問題が発生します。JMMは、これらの問題を解決するように見えました。このモデルは、マルチコアCPUマルチスレッドプログラミング環境で共有変数のアトミック性、可視性、および読み取りと書き込みの順序を保証するための仕様を確立しました。簡単に言うと、JMMは、マルチコアCPUマルチスレッドプログラミング環境での共有変数アクセスの原子性、可視性、および順序付けを解決するための仕様です。JMMの主な構造を理解するために、下の写真を入れてください。

ここに画像の説明を挿入

この記事では、JMMの概念とそれが解決する問題について簡単に説明します。具体的には、JMMが原子性、可視性、順序の問題、およびJMMに関するその他の詳細をどのように解決するかについては、引き続き分析されます。

2020年7月7日

おすすめ

転載: blog.csdn.net/weixin_43907422/article/details/106983311