「こんにちは、アミーゴ! 複数のスレッドが共有リソースに同時にアクセスしようとしたときに生じる問題についてエリーが話してくれたことを覚えていますか?」

"はい。"

「問題は、それだけではありません。別の小さな問題があります。」

ご存知のとおり、コンピューターには、データとコマンド (コード) が保存されるメモリーと、これらのコマンドを実行してデータを操作するプロセッサーがあります。プロセッサはメモリからデータを読み取り、変更し、メモリに書き戻します。計算を高速化するために、プロセッサには独自の「高速」メモリ、つまりキャッシュが内蔵されています。

プロセッサは、最も頻繁に使用される変数とメモリ領域をキャッシュにコピーすることにより、より高速に実行されます。次に、この高速メモリにすべての変更が加えられます。そして、データを「遅い」メモリにコピーして戻します。この間ずっと、遅いメモリには古い (変更されていない!) 変数が含まれています。

ここで問題が発生します。1 つのスレッドは変数(上の例の isCancel や isInterrupted など) を変更しますが、2 番目のスレッドはこの変更を認識しません。これは高速メモリ内で発生したためです。これは、スレッドが相互のキャッシュにアクセスできないという事実の結果です。(多くの場合、プロセッサーには複数の独立したコアが含まれており、スレッドは物理的に異なるコアで実行できます。)

昨日の例を思い出してみましょう。

コード 説明
class Clock implements Runnable
{
private boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
スレッドは他のスレッドが存在することを「知りません」。

run メソッドでは、isCancel 変数は初めて使用されるときに子スレッドのキャッシュに置かれます。この操作は次のコードと同等です。

public void run()
{
boolean isCancelCached = this.isCancel;
while (!isCancelCached)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}

別のスレッドからcancelメソッドを呼び出すと、通常の (低速な) メモリ内のisCancelの値が変更されますが、他のスレッドのキャッシュ内の isCancel の値は変更されません。

public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

「おお! それから、彼らはこれに対しても、同期などの美しい解決策を思いついたのでしょうか ?」

「信じられないでしょう!」

最初に考えられたのはキャッシュを無効にすることでしたが、これによりプログラムの実行が数倍遅くなってしまいました。その後、別の解決策が現れました。

不安定なキーワードが誕生しました。このキーワードを変数宣言の前に置き、その値をキャッシュに入れてはならないことを示します。より正確に言えば、キャッシュに入れられないのではなく、常に通常の (遅い) メモリから読み書きする必要があるというだけです。

すべてが正常に動作するようにソリューションを修正する方法は次のとおりです。

コード 説明
class Clock implements Runnable
{
private volatile boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
volatile 修飾子を使用すると、変数は常にすべてのスレッドで共有される通常のメモリから読み取られ、そこに書き込まれるようになります。
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

"それでおしまい?"

「それだけです。シンプルで美しいです。」