Java の変数は、ローカル変数とクラス変数の 2 つのカテゴリに分類されます。ローカル変数は、run メソッドで定義された変数など、メソッド内で定義された変数を指します。これらの変数については、スレッド間での共有の問題はありません。したがって、データの同期は必要ありません。クラス変数はクラス内で定義された変数であり、そのスコープはクラス全体です。このような変数は複数のスレッドで共有できます。したがって、そのような変数に対してデータ同期を実行する必要があります。
データの同期とは、同期されたクラス変数に同時にアクセスできるのは 1 つのスレッドだけであることを意味します。現在のスレッドがこれらの変数にアクセスした後、他のスレッドはそれらの変数に引き続きアクセスできます。ここで言うアクセスとは、書き込み操作を伴うアクセスを指します。クラス変数にアクセスするすべてのスレッドが読み取り操作である場合、通常はデータの同期は必要ありません。では、共有クラス変数に対してデータ同期が実行されない場合はどうなるのでしょうか?まず、次のコードで何が起こるかを見てみましょう。
次のようにコードをコピーします。
パッケージテスト。
パブリッククラスMyThreadはThreadを拡張します
{
パブリック静的 int n = 0;
public void run()
{
int m = n;
収率();
m++;
n = m;
}
public static void main(String[] args) が例外をスローする
{
MyThread myThread = new MyThread();
スレッド thread[] = 新しいスレッド [100];
for (int i = 0; i < thread.length; i++)
スレッド[i] = 新しいスレッド(myThread);
for (int i = 0; i < thread.length; i++)
スレッド[i].start();
for (int i = 0; i < thread.length; i++)
スレッド[i].join();
System.out.println("n = " + MyThread.n);
}
}
上記のコードを実行すると考えられる結果は次のとおりです。
次のようにコードをコピーします。
n=59
この結果を見て驚いた読者も多いだろう。このプログラムは明らかに 100 個のスレッドを開始し、各スレッドが静的変数 n を 1 ずつインクリメントします。最後に、join メソッドを使用して 100 スレッドすべてを実行し、n の値を出力します。通常、結果は n = 100 になるはずです。しかし、結果は100未満です。
実際、この結果の原因は、よく言及される「ダーティ データ」です。 run メソッドの yield() ステートメントは、「ダーティ データ」のイニシエーターです (yield ステートメントを追加しなくても、「ダーティ データ」も生成される可能性がありますが、それほど明白ではありません。100 をより大きな数値に変更することによってのみ、 「ダーティ データ」の生成 (この例では yield を呼び出します) は、「ダーティ データ」の影響を増幅するために発生します)。 yield メソッドの機能は、スレッドを一時停止することです。つまり、yield メソッドを呼び出すスレッドに一時的に CPU リソースを放棄させ、CPU に他のスレッドを実行する機会を与えます。このプログラムがどのように「ダーティ データ」を生成するかを説明するために、thread1 と thread2 の 2 つのスレッドのみが作成されると仮定します。 thread1 の start メソッドが最初に呼び出されるため、通常は thread1 の run メソッドが最初に実行されます。 thread1 の run メソッドが最初の行まで実行されると (int m = n;)、n の値が m に代入されます。 2 行目の yield メソッドが実行されると、スレッド 1 は一時的に実行を停止します。スレッド 1 が一時停止されると、スレッド 2 は CPU リソースを取得した後に実行を開始します (スレッド 2 が最初の行を実行するときは、以前は準備完了状態になっていました)。 m = n;)、thread1 が yield するために実行されるとき、n はまだ 0 であるため、thread2 の m によって取得される値も 0 になります。これにより、thread1 と thread2 の m 値が両方とも 0 になります。 yield メソッドを実行すると、すべて 0 から始まり 1 が加算されます。したがって、誰が最初に実行しても、n の最終値は 1 になりますが、この n には、それぞれ thread1 と thread2 によって値が割り当てられます。 n++ しかない場合、「ダーティ データ」が生成されるのではないかと疑問に思う人もいるかもしれません。答えは「はい」です。つまり、n++ は単なるステートメントなので、実行中に CPU を他のスレッドに渡すにはどうすればよいでしょうか?実際、これは表面的な現象にすぎません。n++ は、Java コンパイラーによって中間言語 (バイトコードとも呼ばれます) にコンパイルされた後は、言語ではありません。次の Java コードがどの Java 中間言語にコンパイルされるかを見てみましょう。
次のようにコードをコピーします。
public void run()
{
n++;
}
コンパイルされた中間言語コード
次のようにコードをコピーします。
public void run()
{
aload_0
ダップ
ゲットフィールド
iconst_1
iadd
プットフィールド
戻る
}
run メソッドには n++ ステートメントしかありませんが、コンパイル後は 7 つの中間言語ステートメントがあることがわかります。これらのステートメントの機能を知る必要はありません。行 005、007、および 008 のステートメントを見てください。 005 行目は getfield で、英語の意味からすると、ここには n が 1 つしかないので、n の値を取得したいことは間違いありません。 007 行目の iadd が、取得した n の値に 1 を加算するものであることは推測に難しくありません。 008 行目の putfield の意味はもうお分かりいただけたかと思います。この関数は、クラス変数 n に 1 を追加して n を更新する役割を果たします。そういえば、n++ を実行するときは n を 1 足すだけで済むのに、なぜこんなに手間がかかるのかという疑問が残るかもしれません。実際、これには Java メモリ モデルの問題が関係しています。
Java のメモリ モデルは、メイン ストレージ領域と作業ストレージ領域に分かれています。メインストレージ領域には、Java のすべてのインスタンスが保存されます。つまり、 new でオブジェクトを作成すると、オブジェクトとその内部メソッド、変数などがこの領域に保存され、MyThread クラスの n もこの領域に保存されます。メインストレージはすべてのスレッドで共有できます。作業領域とは先ほど説明したスレッドスタックのことで、ここにはrunメソッド内で定義された変数やrunメソッドから呼び出されるメソッド、つまりメソッド変数が格納されます。スレッドがメイン記憶域の変数を変更する場合、これらの変数は直接変更されませんが、変更が完了した後、変数値が対応する変数で上書きされ、現在のスレッドの作業記憶域にコピーされます。主記憶域の変数値。
Java のメモリ モデルを理解すると、n++ がアトミック操作ではない理由を理解するのは難しくありません。コピーして1を加えて上書きするというプロセスを経る必要があります。このプロセスは、MyThread クラスでシミュレートされるプロセスと似ています。ご想像のとおり、getfield の実行時に thread1 が何らかの理由で中断された場合、MyThread クラスの実行結果と同様の状況が発生します。この問題を完全に解決するには、何らかの方法を使用して n を同期する必要があります。つまり、同時に 1 つのスレッドだけが n を操作できます。これは、n に対するアトミック操作とも呼ばれます。