「volatile」はjavaの修飾子の一つで、マルチスレッド処理で使われます。この記事では、javaのvolatile修飾の概要と使い方を解説します。
volatileの概要
Webアプリや、大量データ処理では、性能向上を図るためにマルチスレッドで処理をします。特にWebアプリは不特定多数からのリクエストを同時に処理する必要があるためマルチスレッド環境での処理は必須と言えるでしょう。
マルチスレッド環境では同時に複数処理が実行されるため、Javaのスレッドは、メインメモリ上にあるフィールド値を、スレッド固有の領域にキャッシュし、メインメモリへの読み書きを減らすことで性能向上を図っています。
このように、volatile修飾子は並列処理において変数の可視性を確保するのに用います。
マルチスレッドと言えば、Javaにはsynchronized修飾子があります。こちらはマルチスレッド環境においで、特定のメソッドや特定のコードブロックのみ排他制御を行い、synchronizedと定義された処理が同時に呼び出された時に、先に処理に入ったスレッドが、synchronizedと定義されたメソッドや、コードブロックを抜けるまで、後から来たスレッド待たせることで、スレッド間で共有する変数などのアトミック性を保ち、スレッドセーフを実現しています。
スレッド処理における変数のキャッシュとは?
volatile修飾子は、スレッドがメインメモリ上にある、変数のキャッシュコピーを取得することを禁止する機能であることは先述の通りですが、そもそも、スレッドが変数のキャッシュコピーをすると、どのようなケースで問題が発生するのでしょうか?
実際に、volatile修飾子を使わないことによって問題が発生するコードを見てみましょう。
次のサンプルコードは、並列プログラミングを実行するExecutorServiceクラスを用いて、5つのスレッドを起動し、各スレッドでは変数countの値を1ずつカウントアップしています。
import java.util.*;
import java.util.concurrent.*;
public class Main {
// マルチスレッドで共有する変数
private static int count = 0;
public static void main(String[] args) {
// スレッドプールの作成
ExecutorService pool = Executors.newFixedThreadPool(5);
// 5つのスレッドを同時に実行し、count変数をそれぞれ加算
pool.submit(new CountupThread());
pool.submit(new CountupThread());
pool.submit(new CountupThread());
pool.submit(new CountupThread());
pool.submit(new CountupThread());
// シャットダウン
pool.shutdown();
// スレッドの終了を待機
try {
pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
// count変数の値を出力
System.out.printf("count=%d", count);
}
// 変数countをカウントアップするスレッド
static class CountupThread extends Thread {
public void run() {
count++;
}
}
}
普通に考えれば、count変数を5回加算しているため、結果は当然「5」になると思いますが、実際に実行してみると、次のように結果は「4」と表示されました。
実行結果
---------------
count=4
実行環境やタイミングによっては、正しく「5」と表示されたり、さらに少ない数になることもありますが、スレッドが変数の値をキャッシュコピーすることにより、キャッシュ領域にコピーした変数の値をメインメモリに反映する前に、他のスレッドが古い値で加算処理を行なってしまったことにより、このような事象が発生します。
volatileの使いどころ
synchronizedは複数スレッド間の処理を排他的にしアトミック性を確保しているのに対し、volatileはスレッドによる変数のキャッシュコビーを禁止することにより、スレッド間で共有するリソースの「可視性」を保ちますが「アトミック性」はありません。
そのため、ArrayListなど、並列処理において同時に要素を追加すると変数の中身に不整合が生じるようなケースではsynchronizedで処理を排他的にし、ステータスフラグなど、スレッド間でリアルタイムに変更を共有しなければならない変数には、volatile修飾子を付けることを知っておきましょう。
volatileを使ってみよう
volatileは変数宣言時に、修飾子として指定します。
先ほどのサンプルコードで宣言したcount変数にvolatile修飾子を付けて、もう一度処理を実行してみましょう。
import java.util.*;
import java.util.concurrent.*;
public class Main {
// マルチスレッドで共有する変数をvolatile修飾子を付けて宣言
private static volatile int count = 0;
〜〜 以降は前のサンプルコードと同じ 〜〜
コードを実行すると、今度は結果が「5」と表示され、正しく加算処理が行われていることが分かります。
実行結果
---------------
count=5
count変数にvolatile修飾子を付けたことで、スレッドが変数のキャッシュコピーをせず、メインメモリの変数に対して読み書きするようになったため、常に最新の値に対して加算が行われ想定通りの結果になってくれました。
volatileとsynchronizedとの違い
synchronized修飾子を指定したメソッドやブロックを使うことでも、volatile修飾子と同じく変数の値は必ず元のメモリー上から読み込まれます。
synchronizedとvolatileの違いは、スレッド・セーフであるかどうかです。synchronizedはマルチスレッド環境下で同時処理を排他的に制御することで、synchronized修飾子で囲まれた処理の中はスレッド・セーフであることが保証されます。
それに対しvolatile修飾子は、変数の可視性は保証されますが、スレッド・セーフは保証されないので注意しましょう。ArrayListなどの非スレッド・セーフなクラスに対しvolatile修飾子を付けても、同時にアクセスされた時にデータが不正になる可能性があります。
まとめ
Javaのvolatile修飾子の使い方について解説してきました。
マルチスレッド処理は、最初は複雑に見えるかもしれませんが、Java8で導入されたExecutorServiceクラスなどを使用することにより、よりシンプルに並列処理が実装できるようになりました。
是非、マルチスレッドやvolatile修飾子による制御を学んで、効率がよく安全な並列処理を実装していきましょう。
一部でvolatileとsynchronized修飾子を混同し、volatileがマルチスレッドにおける排他制御で使えるという認識を持ってしまいがちですが、あくまで変数のキャッシュコピーを禁止するための修飾子であるため、volatileは排他制御には使えず、さらにvolatileを付けた変数はスレッドセーフにはならないことに注意が必要です。