|
|
Javaでは、マルチスレッド用のクラスとしてThreadが用意されている。[2017-04-15]
しかし新しいJavaでは、Threadを直接使う事はあまり無い。
ExecutorService/Future(JDK 1.5)やFork/Join(JDK
1.7)といったクラスが追加されているので、そちらを使う。
Java21で仮想スレッド(virtual thread)が導入された。[2023-09-23]
これにより、従来のスレッドはプラットフォームスレッド(platform thread)と呼ぶようになった。
Javaでは、マルチスレッド用のThreadクラスが用意されている。
Threadを継承して独自のクラスを作り、runメソッドをオーバーライドして処理本体を記述する。
class スレッドクラス名 extends Thread {
@Override
public void run() {
ここにプログラムを書く。
このルーチンを抜けると、スレッド処理は終了する。
}
}
スレッドを使用(開始)するには、(他のクラスから)以下のようにしてstart()を呼ぶ。するとrun()が呼ばれる。
スレッドクラス名 変数名 = new スレッドクラス名(); 変数名.start();
しかし、このやり方では他のクラスからの継承が出来ないため、マルチスレッド用のインターフェースも用意されている。
class 実行クラス名 extends 何らかの派生元クラス implements Runnable {
@Override
public void run() {
ここにプログラムを書く。
このルーチンを抜けると、スレッド処理は終了する。
}
}
このスレッドを使用するには、以下のようにする。
実行クラス名 変数名 = new 実行クラス名(); Thread スレッド用変数 = new Thread(変数名); スレッド用変数.start();
JDK1.8(Java8)で関数型インターフェースとラムダ式が使えるようになった。[2023-09-23]
Runnableは関数型インターフェースの条件を満たしているので、Runnableが渡せる箇所にはラムダ式を渡すことが出来る。
すなわち、以下のようにしてThreadインスタンスを生成できる。
Thread スレッド用変数 = new Thread(() -> { ここにプログラムを書く。 このルーチンを抜けると、スレッド処理は終了する。 }); スレッド用変数.start();
Java21(プレビュー版ではJava19)で仮想スレッドが使えるようになった。
これにより、スレッドの生成・実行方法は以下のようになった。
// 従来のスレッド(プラットフォームスレッド)
var スレッド用変数 = Thread.ofPlatform().start(() -> {
ここにプログラムを書く。
このルーチンを抜けると、スレッド処理は終了する。
});
// 仮想スレッド
var スレッド用変数 = Thread.ofVirtual().start(() -> {
ここにプログラムを書く。
このルーチンを抜けると、スレッド処理は終了する。
});
一時的にスレッドを停止するときは、sleep()を使う。
try {
Thread.sleep(数値); //ミリ秒単位
Thread.sleep(m, n); //ミリ秒+ナノ秒単位
Thread.sleep(Duration.ofSeconds(数値)); // Java19[2022-09-21]
} catch (InterruptedException e) {
}
//JDK1.5以降[2008-07-24]
try {
TimeUnit.SECONDS.sleep(数値); //秒単位
TimeUnit.MILLISECONDS.sleep(数値); //ミリ秒単位
TimeUnit.MICROSECONDS.sleep(数値); //マイクロ秒単位
TimeUnit.NANOSECONDS.sleep(数値); //ナノ秒単位
} catch (InterruptedException e) {
}
例によって、TimeUnit#sleep()も、内部ではThread#sleep(ミリ,ナノ)を呼んでるわけだが。
自分のスレッドを一時的に休止して他のスレッドを動かすにはyield()を使う。[2008-07-24]
Thread.yield();
スレッドが終了するのを待つには、join()を使う。[2006-04-15]
スレッド用変数.join();
自分自身のスレッド(カレントスレッド)を取得するには以下の様にする。[2010-01-15]
Thread t = Thread.currentThread();
マルチスレッドで処理を行うと、排他が必要な場合がある。[2004-06-12]
Javaの場合、言語仕様として排他制御が定義されている。
synchronizedを使ってブロック化すると、そのブロックを実行するときに指定されたロックオブジェクトを使ってロックされ、同じロックオブジェクトを使ったブロックは同時には実行されなくなる。(後からロックをかけようとしたブロックは、ロックが解放されるまで(先にロックされたブロックの実行が終わるまで)実行が止まる。)
ロックオブジェクトは、他の言語の場合だとライブラリによって提供されることが多いが、JavaではObjectクラスのインスタンスを使えばよい。 (これはミューテックスロックに当たるものらしい(実際は、モニターという もっと高機能なものらしい))
private Object lock = new Object(); public void func1() { synchronized(lock) { 〜 } } public void func2() { synchronized(lock) { 〜 } } //↑同一インスタンスのfunc1()とfunc2()を別スレッドから同時に呼び出しても、片方ずつしか実行されない。 // 同一インスタンスのfunc1()とfunc1()を別スレッドから呼び出すのも同様。 // 別インスタンスであれば、ロックオブジェクトが異なるので排他されず、同時に実行される。
場合によっては、特別なロックオブジェクトを用意する必要は無い。(全てのクラスがObjectを継承しているから、どのクラスのインスタンスでもロックに使える)
public void func(クラス val) { synchronized(val) { 〜 //valに対する操作(他にvalを使う箇所でも同様にロックしないと意味を為さない) } }
public void func() { synchronized(クラス.class) { 〜 //クラスに対する操作(他にクラスを使う箇所でも同様にロックしないと意味を為さない) } }
メソッドにsynchronizedを指定した場合、そのメソッド内部をブロック化したのと同じ扱いになる。
private static synchronized void func() {
〜
//複数スレッドから自分のクラスのfunc()が呼ばれた場合に排他され、
//一度に1スレッド分しか実行されない
}
↑↓同じ
private static void func() {
synchronized(自分のクラス.class) {
〜
}
}
private synchronized void func() {
〜
//複数スレッドから自分のインスタンス(this)のfunc()が呼ばれた場合に排他され、
//一度に1スレッド分しか実行されない
}
↑↓同じ
private void func() {
synchronized(this) {
〜
}
}
なお、これらは動作上は同じ働きをするが、コンパイルされたバイトコード上は同一ではない。[2008-05-23]
Javaソース | バイトコード |
---|---|
synchronized void func1() {
System.out.println("abc");
}
|
0: getstatic #5; //Field
java/lang/System.out:Ljava/io/PrintStream; |
void func2() {
synchronized(this) {
System.out.println("def");
}
}
|
0: aload_0 |
synchronizedブロックにした方(func2())は、モニターの処理が明示的に入ってきている。
つまりコンパイルされたファイルのサイズが大きくなるということか。
メソッド全体で排他するなるなら、メソッドの属性としてsynchronizedを付ける方が良さそうだ。
※synchronizedを付けたメソッドはリフレクションのisSynchronized()
がtrueになるので、そこで区別しているのだろう。
メソッドにsynchronizedを付けた場合、それはそのメソッドのみに適用される。[2008-05-23]
何が言いたいかと言うと、そのメソッドをオーバーライドした場合、オーバーライドしたメソッドは各々でsynchronizedを付けない限り、排他対象にならない。
(synchronizedは継承されない)
class SyncSuper { protected int counter; public synchronized inc() { //排他される counter++; } } class SyncSub extends SyncSuper { @Override public synchronized inc() { //synchronizedを付けないと、排他されない super.counter++; } }
したがって、抽象メソッドにはsynchronizedを付けられない。(コンパイルエラー)
×protected synchronized abstract void method();
同一ロックオブジェクトに対してsynchronizedをネストすることが出来る。[2006-04-15]
すなわち、同一スレッドであれば、内側のsynchronizedで排他待ちになることは無い。
public void func1() { synchronized(this) { func2(); } } private void func2() { synchronized(this) { 〜 //func1()でロックされているが、ちゃんと実行される } }
ちなみに、synchronized()に指定するロックオブジェクトがnullだと、NullPointerExceptionが発生する。[2008-05-12]
private static Object lock = null;
synchronized(lock) { //NullPointerExceptionが発生する
〜
}
まぁ、そんなところでNullPointerExceptionになる事は滅多に無いだろうけど…。
Java16以降では、synchronized()に指定するロックオブジェクトが値ベース・クラス(Integer等)だと、コンパイル時に警告が出る。[2021-03-21]
Integer i = Integer.valueOf(1); synchronized(i) { System.out.println(i); }
synchronizedは負荷が高いと言われる。[2008-05-12]
そりゃ何も排他処理をしないよりは、排他処理をする(synchronizedを使う)方が負荷が高いに決まっている。
なのでsynchronizedは敬遠される傾向にある。
しかし、
あるオブジェクトが排他しないと使えない場合、
インスタンスを1つ作っておいて使う度に排他するのと
毎回インスタンスを生成する(排他しない)のとでは、
コンストラクターの負荷によっては、synchronizedで排他する方が実行速度は速いことがある。
→インスタンス生成にかかる時間とsynchronizedにかかる時間の比較
一番シンプルなクラスを生成して複数スレッド並行で実行し、実行時間を測ってみた。(WindowsXP、JDK1.6)
class Data { public void m(){ /*何も処理しない*/ } }
並行数 | パターン1 | パターン2 |
---|---|---|
class Worker1 extends Thread {
private static final Data data
= new Data();
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
synchronized(data) {
data.m();
}
}
}
}
|
class Worker2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
Data data = new Data();
data.m();
}
}
}
|
|
2 | 約870ms | 約70ms |
3 | 約1300ms | 約80ms |
10 | 約4200ms | 約140ms |
この例では、インスタンス生成もメソッド呼び出しも最小限の処理しか行っていない。
この場合、毎回インスタンスを生成していても、排他なんぞしない方が圧倒的に高速。
しかしSimpleDateFormatでは逆転し、synchronizedを使った方が速い。
(SimpleDateFormatはスレッドセーフではないので、排他する必要がある)
→SimpleDateFormatの測定結果
synchronizedによるロックは、いずれか1つのみが動くことを保証する。逆に、どれが動くかは不定となる。
順序を保証したい場合には、Object#wait(),notify()が使えるかもしれない。[2006-04-15]
wait()はsynchronizedブロックの中で使用する。
wait()を呼び出すと、呼び出したスレッドはそこで実行を停止し、他のスレッドがsynchronizedの中に入って実行できる。
後から実行されたスレッドがnotify()を呼び出すと、そのスレッドがsynchronizedブロックから抜けた後にwait()を呼び出したスレッドが再実行される。
notify()を呼ばずにsynchronizedブロックを抜けると、wait()は永久に待機状態のままとなる!(Object#wait(long)ならタイムアウトするけど)
↓例
public void thread1() { System.out.println("W1 start"); synchronized (lock) { System.out.println("W1 lock start"); msleep("W1",200); try { System.out.println("W1 wait start"); lock.wait(); System.out.println("W1 wait end"); } catch (InterruptedException e) { } msleep("W1",1000); System.out.println("W1 lock end"); } System.out.println("W1 end"); } |
public void thread2() { System.out.println("W2 start"); synchronized (lock) { System.out.println("W2 lock start"); msleep("W2",100); System.out.println("W2 notify start"); lock.notify(); System.out.println("W2 notify end"); msleep("W2",1000); System.out.println("W2 lock end"); } msleep("W2",1000); System.out.println("W2 end"); } |
↓実行結果
W1 start W1 lock start ←スレッド1がロック獲得。 W1 sleep200 start W2 start ←スレッド2はロック待ち。 W1 sleep200 end W1 wait start ←スレッド1がwaitに入ると W2 lock start ←スレッド2がロック獲得。 W2 sleep100 start W2 sleep100 end W2 notify start W2 notify end ←スレッド2がnotifyを呼んだが W2 sleep1000 start … W2 sleep1000 end … W2 lock end ←スレッド2がロック終了するまで W2 sleep1000 start … W1 wait end ←スレッド1のwaitは終わらない。 W1 sleep1000 start W2 sleep1000 end W1 sleep1000 end W2 end W1 lock end W1 end
排他に関して普通はsynchronizedを使えば充分だが、JDK1.5からは新しいクラスも用意された。[2007-03-26]
排他クラス | 概要 | 参考 |
---|---|---|
ReentrantReadWriteLock | 読み書きロック(読み込み頻度が多くて書き込み頻度が少ないデータ用のロック) | JavaWorldの新たな同期メカニズム |
Semaphore | セマフォ | JavaWorldの新たな同期メカニズム |
でも こういうクラスの形で提供されるんじゃ、「Javaでは排他制御が言語仕様(synchronized)でサポートされている」という宣伝文句が色褪せるよね〜。
今回のクラスもsynchronizedの文法を拡張すればよかったのに。synchronized_readとかsynchronized_writeとか(爆)
ま、そういう事すると どんどんクドイ言語になるだろうけど(苦笑)
リードライトロックのサンプル:
class RWCounter { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private volatile int value = 0; public void add(int n) { WriteLock w = lock.writeLock(); try { w.lock(); value += n; } finally { w.unlock(); } } public int get() { ReadLock r = lock.readLock(); try { r.lock(); return value; } finally { r.unlock(); } } }
JDK1.5で、ArrayListやHashMapに対するMTセーフなコレクションクラスが追加された。[2007-03-26]
MTセーフなクラス | 該当クラス | 参考 | 更新日 | |
---|---|---|---|---|
ConcurrentHashMap |
1.5 | HashMap | JavaWorldの並行処理に対応したコレクション | |
ConcurrentSkipListMap |
1.6 | TreeMap | 2011-04-02 | |
CopyOnWriteArrayList |
1.5 | ArrayList | JavaWorldの並行処理に対応したコレクション | |
CopyOnWriteArraySet |
1.5 | JavaWorldの並行処理に対応したコレクション | ||
Collections.newSetFromMap(new ConcurrentHashMap<E, Boolean>()) |
1.6 | HashSet | 『Java本格入門』p.145 ConcurrentHashSet相当 |
2017-04-15 |
ConcurrentLinkedQueue |
1.5 | LinkedList | ITproの両端キュー:デック | 2007-12-07 |
Hashtableも 機能的にはHashMapのMTセーフ版(メソッドがことごとくsynchronizedで排他されている)らしい。
(Vectorも ArrayListをsynchronizedで同期化したようなクラス)
また、HashMapのJavadocには 同期化する為にCollections.synchronizedMap()を使うよう書かれている。
だが、上記の新規クラスの方が高速(になるように設計されている)らしい。
→HashMapを同期化しなかった場合の障害の例: @ITのThreadとHashMapに潜む無限回廊は実に面白い? (2008-05-17)
ThreadLocalは、スレッド毎の値を保持する為のクラス。[2008-07-10]
あるクラス(のインスタンス)がマルチスレッドで呼ばれる際に、スレッド毎に異なる値(インスタンス)を使いたい場合に使用する。
自分でThread(やRunnable)を使ってマルチスレッド化しているなら
スレッド毎の値なんて簡単に保持できるので、ThreadLocalには意味が無い。
しかしウェブアプリ(サーブレット)は1つのインスタンスがマルチスレッドで呼ばれるので、そのスレッド毎に別のインスタンスを保持したい時にはとても便利。
ThreadLocalを継承したサブクラスを作り、そのinitialValue()をオーバーライドして実装しておく。
このメソッドは、スレッド内で値を取得する際に“そのスレッド専用の値”を生成する為に呼ばれる。(そのスレッド用の値をクリアしない限り、一スレッドにつき一回だけ呼ばれる)
/** SimpleDateFormatはスレッドセーフでないので、ThreadLocalを使ってスレッドセーフにしてみる例 */
public class DateFormatLocal extends ThreadLocal<DateFormat> {
private String pattern;
/** コンストラクター */
public DateFormatLocal(String pattern) {
this.pattern = pattern;
}
@Override
protected DateFormat initialValue() {
//スレッド毎の初期化(1スレッドにつき1回だけ呼ばれる)
return new SimpleDateFormat(pattern);
}
}
private static DateFormatLocal dflocal = new DateFormatLocal("yyyy/MM/dd"); public void method() { //マルチスレッドで呼んでも平気 DateFormat df = dflocal.get(); Date date = df.parse("2008/07/10"); }
上記のpatternが固定でいいなら、無名クラスを利用してシンプルにすることも出来る。
private static ThreadLocal<DateFormat> dflocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
//スレッド毎の初期化(1スレッドにつき1回だけ呼ばれる)
return new SimpleDateFormat("yyyy/MM/dd");
}
}
public void method() { //マルチスレッドで呼んでも平気
DateFormat df = dflocal.get();
Date date = df.parse("2008/07/10");
}
※マルチスレッド(ウェブ)ではSimpleDateFormatはThreadLocalを使うのが最も高速
※そもそもJava8以降ではSimpleDateFormatでなく(スレッドセーフな)DateTimeFormatterを使えばThreadLocalにする必要は無い
Java8では、ThreadLocalのinitialValueメソッドをオーバーライドする代わりに、初期値を生成する処理をラムダ式で指定することが出来る。[2022-10-22]
private static ThreadLocal<DateFormat> dflocal
= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy/MM/dd"));
ThreadLocalは、スレッドIDをキーにして生成した値を保持するMapのようなもの。
スレッドが終了すると ThreadLocalに保持していた“そのスレッド用の値”が自動的に消去される(GCの対象になる)ので、いつまでもゴミが残りっぱなしになったりはしない。
(これには、弱参照という仕組み(WeakReferenceクラス)が使われている)
なので逆に、スレッドが毎回生成されて破棄されるような作りだと、ThreadLocalは値を保持せず毎回生成することになるので、意味が無い。
ウェブアプリ(サーブレットコンテナ)だと
たぶんスレッド数の上限が決まっていてスレッドプール内のスレッドが使い回される為、ThreadLocalにとっても効率がいいだろう。
クラスのフィールド(メンバー変数)に対して付けることが可能な修飾子。[2008-04-17]
JavaVMのバイトコードよりさらに下のレベル(CPUが実行するマシン語のレベル)では、変数の値は(スレッド毎の)キャッシュメモリーに格納されて、変数の参照・更新はキャッシュメモリーに対して行われる事がある。(その方が高速化できるから)
もちろんある時点でキャッシュメモリーから実メモリー(変数の本来の格納場所)へ格納されて各スレッド共通の値となる。
これがスレッド毎に行われる為、マルチスレッドにおいては、同じ変数なのに自分のスレッドの値と他スレッドの値が異なることが起こり得る。
で、volatileを付けると、「あるスレッドで更新された値が別スレッドで読み込まれる」ことが保証される。
volatileを付けていないと、以下のような状況が発生する可能性がある。
(フィールド「int value;
」と各スレッドのローカル変数nがあるものとする)
時系列 | 処理 | 説明 | 処理後のvalueの値 | ||||
---|---|---|---|---|---|---|---|
スレッドAの処理 | スレッドBの処理 | 理論的な (期待した)値 |
スレッドA用 キャッシュ |
スレッドB用 キャッシュ |
フィールド値 | ||
初期 | 0 | 0 | |||||
1 | int n = this.value; |
フィールドからキャッシュにコピーされる。 | 0 | 0← | 0 | ||
2 | this.value = n + 1; |
valueが更新されたが、キャッシュの中のみ。 | →1 | →1 | 0 | ||
3 | int n = this.value; |
フィールドからキャッシュにコピーされる。 | 1 | 1 | 0← | 0 | |
4 | this.value = n + 1; |
valueが更新されたが、キャッシュの中のみ。 | →2 | 1 | →1 | 0 | |
5 | 変更されたスレッドAのキャッシュの内容がフィールドに反映される。 | 2 | 1→ | 1 | 1 | ||
6 | 変更されたスレッドBのキャッシュの内容がフィールドに反映される。 | 2 | 1 | 1→ | 1 |
valueが「volatile int value;
」なら、自スレッドが値を取得する際に、別スレッドの変更内容が必ずフラッシュ(本来のフィールド値のエリアに格納)される。
時系列 | 処理 | 説明 | 処理後のvalueの値 | ||||
---|---|---|---|---|---|---|---|
スレッドAの処理 | スレッドBの処理 | 理論的な (期待した)値 |
スレッドA用 キャッシュ |
スレッドB用 キャッシュ |
フィールド値 | ||
初期 | 0 | 0 | |||||
1 | int n = this.value; |
フィールドからキャッシュにコピーされる。 | 0 | 0← | 0 | ||
2 | this.value = n + 1; |
valueが更新されたが、キャッシュの中のみ。 | →1 | →1 | 0 | ||
3 | int n = this.value; |
他スレッドのキャッシュの中に変更があると、その内容がフィールドに反映される。 | 1 | 1→ | 1 | ||
フィールドからキャッシュにコピーされる。 | 1 | 1 | 1← | 1 | |||
4 | this.value = n + 1; |
valueが更新されたが、キャッシュの中のみ。 | →2 | 1 | →2 | 1 | |
5 | 変更されたスレッドBのキャッシュの内容がフィールドに反映される。 | 2 | 1 | 2→ | 2 |
volatileは他スレッドで値が更新されていないかチェックする為、多少実行が遅くなる。
ただし、volatileを付けても原子性が担保されるわけではない。すなわち、valueの取得とvalueへの設定が不可分に行われるわけではない。
(スレッドセーフになるわけではない)
時系列 | 処理 | 説明 | 処理後のvalueの値 | ||||
---|---|---|---|---|---|---|---|
スレッドAの処理 | スレッドBの処理 | 理論的な (期待した)値 |
スレッドA用 キャッシュ |
スレッドB用 キャッシュ |
フィールド値 | ||
初期 | 0 | 0 | |||||
1 | int n = this.value; |
フィールドからキャッシュにコピーされる。 | 0 | 0← | 0 | ||
2 | int n = this.value; |
フィールドからキャッシュにコピーされる。 | 0 | 0 | 0← | 0 | |
3 | this.value = n + 1; |
valueが更新されたが、キャッシュの中のみ。 | →1 | →1 | 0 | ||
4 | this.value = n + 1; |
valueが更新されたが、キャッシュの中のみ。 | →2 | 1 | →1 | 0 | |
5 | 変更されたスレッドAのキャッシュの内容がフィールドに反映される。 | 2 | 1→ | 1 | 1 | ||
6 | 変更されたスレッドBのキャッシュの内容がフィールドに反映される。 | 2 | 1 | 1→ | 1 |
「this.value++;
」にしたところで、コンパイルしたバイトコードのレベルでは(ですら)以下のように複数ステップに分かれている。
10: getfield #20; //Field jp/hishidama/sample/thread/Data.value:I 13: iconst_1 14: iadd 15: putfield #20; //Field jp/hishidama/sample/thread/Data.value:I
●マルチスレッドプログラミングでは、ソースコードのレベルではなく、CPUが実行するマシン語のレベルで物を考えなければならない。
(ちなみに、バイトコードのレベルでは、volatile付きとそうでないフィールドの取得・設定方法には違いが無い模様(上記のバイトコードと全く同じ)。
ただし実行速度は異なるので、get/put内ではちゃんと何かやっているのだろう)
なお、排他した(同期をとった)カウンターに関しては、JDK1.5で導入されたAtomicIntegerが便利。
volatileの使い道は、IBMの『Javaの理論と実線:
volatileを扱う』が詳しい。
(難しくて半分以上よく分からない(爆)けど、リスト6の安価な読み書きロックは目からウロコ)
JDK1.5で、原子性を担保するクラスが導入された。[2008-04-17]
原子性(アトミックである)とは、値の取得と更新が不可分である(その間に別スレッドからの割り込みが入らない)のが保証されていること。
単純な話、synchronizedを使って排他すれば原子性が確保される。
class Atomic { private int value = 0; public synchronized void add(int n) { value += n; } public int get() { return value; } }
しかしシンプルなカウンターなら、synchronizedは負荷が高い。JDK1.5で導入されたAtomicIntegerやAtomicLongを使うとよい。
class Atomic { private AtomicInteger value = new AtomicInteger(0); public void add(int n) { value.addAndGet(n); } public int get() { return value.get(); } }
addAndGet()メソッドは、最終的にnativeメソッドを呼び出している。
原子性の担保はCPUの“テスト&セット命令”を使わないと実現できないので、それを呼び出す実装になっているのだろう。
実験:(WindowsXP・JDK1.6)
以下のようなDataクラスを1つだけインスタンス化し、2スレッドから100万回ずつinc()を呼び出して加算してみた。
class Data { int n = 0; volatile int vol = 0; AtomicInteger atom = new AtomicInteger(0); int s = 0; RWCounter rw = new RWCounter(); public void inc() { n++; vol++; atom.addAndGet(1); // synchronized (this) { s++; } rw.add(1); } @Override public String toString() { return "int=" + n + ", volatile=" + vol + ", atom=" + atom.intValue() + ", sync=" + s + ", rw=" + rw.get(); } }
結果:
int=1999194, volatile=1999132, atom=2000000, sync=0, rw=2000000 int=1998732, volatile=1998050, atom=2000000, sync=0, rw=2000000 int=1999192, volatile=1999162, atom=2000000, sync=0, rw=2000000
※synchronizedはコメントアウトしているが、それを有効にするとnもvolも影響を受けて排他された状態に近い値になる。
int=1999998, volatile=1999998, atom=2000000, sync=2000000, rw=2000000
なお、個別に実行して時間を測ったら、おおよそ以下のようになった。 (100万回ループ×2スレッドが完了する時間)
修飾子なし | volatile | AtomicInteger | synchronized | 読み書きロック |
---|---|---|---|---|
30ms | 60ms | 180〜200ms | 900ms | 1000ms |
※修飾子なしとvolatileは速いが、計算結果が正しくないので論外。
しかしこの実験は書き込みしか行っていないので、読み書きロックにとっては非常に不利。
(なお、RWCounterのvolatileを外すと700〜800ms程度になってsynchronizedより速くなったが、
たぶん外すとまずいだろう)
なので、読み込みのみも実験してみた。[2008-04-18]
上記のinc()の代わりに、下記のget()を呼び出した。
public int get() { return n; // return vol; // return atom.get(); // synchronized (this) { return s; } // return rw.get(); }
修飾子なし | volatile | AtomicInteger | synchronized | 読み書きロック |
---|---|---|---|---|
2.9ms | 3.1ms | 3.1ms | 840〜860ms | 460〜560ms |
※修飾子なし・volatile・AtomicIntegerは速すぎるので、1スレッド当たり1億回ループさせて100で割った時間(他は100万回ループの時間)
読み書きロックの方がsynchronizedより速くなって面目を保ったとは言え、さらにAtomicIntegerの方が圧倒的に高速。
単純な排他(同期)カウンターならAtomicIntegerを使うのが断然良さそうだ。
この調子なら、AtomicBoolean(のcompareAndSet()メソッド)なんかも便利に使えるかもしれない。