こんにちは!フリーランスの長野です。
volatileって使ってますか?
volatileは修飾子の一つで、マルチスレッド処理で使われます。
この記事では、volatile修飾子について
・volatileとは
・volatileの使い方
という基本的な内容から、
・synchronizedとの違い
・Threadについて
など応用的な内容についても解説していきます。
今回はvolatile修飾子について、使い方をわかりやすく解説します!
なお、Javaの記事については、こちらにまとめています。
volatileとは?
volatileはJavaの修飾子の一つで、マルチスレッド処理で使われます。
フィールドの値は通常メモリから呼び出され、更新されたりします。
ただこれはシングルスレッドの場合で、マルチスレッドの場合はこれとは違ってきます。
スレッドが一列で順番に処理されていくことを、「シングルスレッド」と言います。
これに対して、スレッドの列が2つ以上複数あり並列に同時進行で処理されていくことを「マルチスレッド」と言います。
マルチスレッド処理の場合、それぞれのスレッドが同じフィールドの値を別々にキャッシュすることがあります。
キャッシュとはメモリとは違う場所に値を保持することで、一般的にメモリに比べて保存容量は少ないですが、高速に値の呼び出しや更新ができます。
マルチスレッドの場合、フィールドの値をメモリより高速に呼び出し・更新するためにキャッシュすることがあるのです。
ここで注意しなければならないのが、それぞれのスレッドが同じフィールドの値を別々にキャッシュすると、同じフィールド名であっても値が違ってくる危険性があるということです。
次のようなwhile文を記述し、コンパイルのあとに実行する場合を考えてみましょう。
while(flag == 0) { // flagが0の間行われる処理 }
このwhile文のブロック内でflagの値を操作しない場合、while文の条件式を毎回評価する必要はありません。
このような場合にwhile文はコンパイラによって以下のように最適化することが可能になります。
if(flag == 0) { while(1) { // flagが0の間行われる処理 } }
シングルスレッドの場合はwhile文のブロック内でflagの値を操作しない場合、このように最適化されても問題ありません。
ただし、マルチスレッドの場合は前述のとおり他のスレッドからflagの値を操作されることがあります。
もし他のスレッドからflagの値を操作され変更された場合、最適化によってループから抜けられなくなる不具合が発生してしまうのです。
そこで、このような処理を行う場合はvolatile修飾子を使用します。
volatileの使い方
volatile修飾子の使い方をサンプルコードで確認しましょう。
public class VolatileSample { private static volatile int count = 0; //private static int count = 0; public static void main(String[] args) { new MultiThread1().start(); new MultiThread2().start(); } static class MultiThread1 extends Thread { public void run() { int val = count; while(count < 3) { if (val != count) { String message = getName() + ": val = " + val + ", count = " + count; System.out.println(message + " 更新"); val = count; } } } } static class MultiThread2 extends Thread { public void run() { int val = count; while(count < 3) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } String message = getName() + ": val = " + val + ", count = " + count; System.out.println(message); count = ++val; } } } }
実行結果:
Thread-1: val = 0, count = 0 Thread-0: val = 0, count = 1 更新 Thread-1: val = 1, count = 1 Thread-0: val = 1, count = 2 更新 Thread-1: val = 2, count = 2 Thread-0: val = 2, count = 3 更新
このサンプルコードではフィールドcountの値が更新された場合に出力表示するスレッドクラスMultiThread1と約500ミリ秒ごとにcountの値を1ずつ増やすスレッドクラスMultiThread2を並列で処理しています。
MultiThread1クラスのwhile文の条件式にはフィールドcountが使われていますが、while文のブロック内ではcountの値は操作されていません。
この場合コンパイラで最適化される可能性がありますが、volatile修飾子を使って最適化を抑止しています。
以下の実行結果はvolatile修飾子なしの場合の実行結果です。
実行結果(volatile修飾子なしの場合):
Thread-1: val = 0, count = 0 Thread-1: val = 1, count = 1 Thread-1: val = 2, count = 2
このサンプルコードを実行すると、上記の実行結果を表示したあと無限にループし続けます。
注意してください。
この場合volatile修飾子を使っていませんので、コンパイラで最適化され、意図した処理とは違う処理が実行されていると考えられます。
synchronizedとの違い
ここまではコンパイラの最適化の抑止について説明しました。
その他にもvolatile修飾子をフィールドにつけることで、マルチスレッド処理でメモリとキャッシュで値が違ってしまう危険性を避けるためにフィールドの値がキャッシュされるのを抑止することができます。
同じようにメモリとキャッシュで値が違ってしまう危険性を避けるためにsynchronized修飾子が使われます。
synchronized修飾子の付いたブロック内で扱うフィールドの値は、必ず元のメモリー上から呼び出し・更新されますので、キャッシュによる値の不整合を避けることができます。
volatile修飾子は簡単な値の読み書き操作で、synchronized修飾子を付けて扱うまでもない場合に使います。
synchronized修飾子ではスレッド・セーフが保証されていますが、volatile修飾子はスレッド・セーフは保証していませんので注意しましょう!
volatile修飾子とsynchronized修飾子のその他の違いについてはこちらでも詳しく解説しています。
https://www.javamex.com/tutorials/synchronization_volatile.shtml
Threadについて
マルチスレッドおよびThreadクラスについてはこちらで詳しく解説していますので、ぜひ参考にしてください。
まとめ
ここでは、volatile修飾子について説明しました。
マルチスレッド処理でフィールドの値が他のスレッドで変更される場合、コンパイルの最適化によって無限ループにならないか注意する必要があります。
マルチスレッドの記述は最初は複雑だと感じるかもしれませんが、慣れて使いこなすことができるようにこの記事を何度も参考にして下さいね!