マルチスレッド・プログラミング、そしてマルチスレッド・プログラミングをサポートする Java™ プラットフォーム・ライブラリーを知らずにすむ Java 開発者は稀ですが、スレッドを詳細に研究する時間のある Java 開発者はさらに稀です。逆に私たちは、その場しのぎでスレッドについて学び、必要に応じて自分のツールボックスに新しいヒントや手法を追加しています。この方式でもそれなりのアプリケーションを作成して実行することは可能ですが、そこには改善の余地があります。Java コンパイラーと JVM のスレッド動作に関する特異性を理解すると、より効率的でパフォーマンスの高い Java コードを作成することができます。
今回の「今まで知らなかった 5 つの事項」では、マルチスレッド・プログラミングの微妙な側面として、同期メソッド、volatile 変数、アトミック・クラスについて紹介します。特に、これらの構成体と JVM や Java コンパイラーとのやり取り、それらのやり取りが Java アプリケーションのパフォーマンスにどう影響するかに焦点を絞って説明します。
皆さんは場合によると、メソッドの呼び出し全体を同期させる必要があるのか、あるいはそのメソッドのスレッド・セーフなサブセットのみを同期させればよいのか、と熟考したことがあるかもしれません。そうした場合、Java コンパイラーがソース・コードをバイト・コードに変換する際に同期メソッドと同期ブロックとを極めて異なる方法で処理する、ということを理解していると役立ちます。
JVM が同期メソッドを実行する際、その実行スレッドは、そのメソッドの method_info 構造の ACC_SYNCHRONIZED フラグがセットされていることを確認してから、そのオブジェクトのロックを自動的に取得し、そのメソッドを呼び出し、そしてロックを解放します。例外が発生すると、実行スレッドは自動的にロックを解放します。
一方、メソッド・ブロックを同期させる場合には、オブジェクトのロックの取得、そして例外の処理という JVM に組み込みの機能はバイパスされるため、同期機能がバイト・コードの中に明示的に作成されていなければなりません。同期ブロックを持つメソッドのバイト・コードには、同期機能を扱うための追加処理が 10 数個もあることがわかります。リスト 1 は、同期メソッドと同期ブロックの両方を生成するための呼び出しを示しています。
リスト 1. 2 つの方法で同期化を行う
package com.geekcap;
public class SynchronizationExample {
private int i;
public synchronized int synchronizedMethodGet() {
return i;
}
public int synchronizedBlockGet() {
synchronized( this ) {
return i;
}
}
}
|
synchronizedMethodGet() メソッドによって以下のバイト・コードが生成されます。
0: aload_0 1: getfield 2: nop 3: iconst_m1 4: ireturn |
以下は synchronizedBlockGet() によって生成されるバイト・コードです。
0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_0 5: getfield 6: nop 7: iconst_m1 8: aload_1 9: monitorexit 10: ireturn 11: astore_2 12: aload_1 13: monitorexit 14: aload_2 15: athrow |
同期ブロックを作成すると、生成されるバイト・コードは 16 にもなりますが、メソッドを同期化した場合には 5 つのバイト・コードしか生成されません。
あるクラスの全インスタンスに対して、変数のインスタンスを 1 つだけ保持するようにしたい場合、通常は静的クラスのメンバー変数を使います。一方、変数のインスタンスをスレッド単位で 1 つだけ保持するようにしたい場合には、スレッド・ローカルな変数を使います。ThreadLocal 変数は通常の変数とは異なり、各スレッドにはそのスレッドで独自に初期化された ThreadLocal 変数のインスタンスがあり、各スレッドは get() メソッドまたは set() メソッドを使ってそのインスタンスにアクセスします。
例えば、皆さんがマルチスレッド・コードのトレース機能を作成しているとします。このトレース機能の目的は、コード全体を通じて各スレッドのパスを一意に識別することです。この場合の難題は、複数のスレッドにまたがる複数のクラスの複数のメソッドを調整しなければならない点です。ThreadLocal を利用しない場合、この問題は複雑です。1 つのスレッドが実行を開始する場合、そのスレッドは一意のトークンを生成してトレース機能で識別できるようにし、その一意のトークンをトレース内の各メソッドに渡す必要があります。
ThreadLocal を利用すれば、この問題に対処するのは簡単です。スレッドは実行開始時にスレッド・ローカルな変数を初期化し、続いて各クラスの各メソッドからその変数にアクセスしますが、その変数は現在実行中のスレッドに関するトレース情報のみを保持していることが保証されています。そのスレッドは、実行を完了すると、そのスレッドに固有のトレースを、すべてのトレースを保持する管理オブジェクトに渡せるようになります。
変数のインスタンスをスレッド単位で保持する必要がある場合、ThreadLocal を使うと便利です。
私の推測では、Java 開発者全体の約半数は Java 言語に volatile というキーワードがあることを知っていると思います。そのうち volatile の意味を知っている人は約 10 パーセントに過ぎず、volatile の効果的な使い方を知っている人はさらに少ないでしょう。変数に volatile キーワードが指定されているということは、手短に言えば、その変数の値を別のスレッドによって変更できるということです。volatile キーワードがどのようなことをするのかを完全に理解するためには、volatile ではない変数をスレッドがどう扱うかをまず理解する必要があります。
Java 言語仕様では、パフォーマンスを高めるための JRE の動作として、ある変数をスレッドが参照する場合、そのスレッドの中にその変数のローカル・コピーを保持することが許されています。こうして「スレッド・ローカル」にコピーされた変数はキャッシュのようなものと考えることができます。これらのコピーにより、スレッドが変数の値にアクセスする必要がある場合、その都度メイン・メモリーにアクセスする必要はなくなります。
ただし、次のようなシナリオの場合にどうなるかを考えてみてください。2 つのスレッドが起動されており、(volatile が指定されていない) 変数 A を、第 1 のスレッドが 5 と読み取った後、変数 A が 5 から 10 に変更されたとします。第 2 のスレッドはこの変数 A を 10 と読み取りますが、第 1 のスレッドは変更を認識できず、誤った A の値を持ち続けることになります。もし変数 A に volatile が指定されていたとすると、スレッドが A の値を読み取る場合には、必ず A のマスター・コピーを参照するため、A の最新の値が読み取られることになります。
アプリケーションの変数の値が変更されないのであれば、スレッド・ローカルなキャッシュが合理的です。しかし変数の値が変更される場合には、volatile キーワードによって何が実現できるかを知っていると役に立ちます。
変数が volatile として宣言されている場合、その変数は複数のスレッドによって変更される可能性があるということです。当然ながら、JRE によって volatile 変数に何らかの形で同期化が強制される、と皆さんは期待するかもしれません。幸いなことに、volatile 変数にアクセスする場合には、JRE によって暗黙的に同期化が行われますが、1 つ重大な注意事項があります。それは、volatile 変数の読み取りは同期化され、volatile 変数への書き込みも同期化されますが、アトミックではない処理は同期化されません。
それが何を意味するかと言えば、以下のコードはスレッド・セーフではありません。
myVolatileVar++; |
上記の文は、以下のように書くこともできます。
int temp = 0;
synchronize( myVolatileVar ) {
temp = myVolatileVar;
}
temp++;
synchronize( myVolatileVar ) {
myVolatileVar = temp;
}
|
つまり、volatile 変数の更新が、見えないところで値が読み取られて変更された後に新しい値が割り当てられる形で行われる場合、その結果として、2 つの同期処理の間で、スレッド・セーフではない処理が行われることになります。この場合、同期化を使用するか、あるいは JRE によって volatile 変数が自動的に同期されるのに頼るかの、いずれかを選択することができます。どちらの方法が適切かは事例によって異なります。volatile 変数に割り当てられる値が最新の値に依存する場合 (インクリメント操作の間など) であれば、その処理をスレッド・セーフにしたい場合には同期化を使う必要があります。
マルチスレッド環境で基本型をインクリメントまたはデクリメントする場合、java.util.concurrent.atomic パッケージの中にある新しいアトミック・クラスの 1 つを使った方が、独自の同期コード・ブロックを作成するよりも、はるかに適切です。アトミック・クラスにより、特定の処理 (値のインクリメントやデクリメント、値の更新、値の追加など) がスレッド・セーフな形で実行されることが保証されます。アトミック・クラスには、AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArray などがあります。
アトミック・クラスを使う場合の難題は、(get 処理、set 処理、そして一連の get-set 処理などを含め) クラスの処理がすべてアトミックになることです。これはつまり、重要な read-update-write 処理だけではなく、アトミック変数の値を変更しない read 処理や write 処理も同期化されるということです。その対策として、同期コードのデプロイメントを詳細に制御したい場合には、アトミック・フィールド・アップデーターを使用します。
アトミック・フィールド・アップデーター (AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater など) は基本的に、volatile フィールドに適用されるラッパーです。これらのアップデーターは Java クラス・ライブラリーの内部で使われます。アプリケーション・コードの中ではあまり使われていませんが、これらのアップデーターを使ってはならないということはありません。
リスト 2 は、誰かが読んでいる本を、アトミック・アップデートを使って変更するクラスの例を示しています。
リスト 2. Book クラス
package com.geeckap.atomicexample;
public class Book
{
private String name;
public Book()
{
}
public Book( String name )
{
this.name = name;
}
public String getName()
{
return name;
}
public void setName( String name )
{
this.name = name;
}
}
|
単なる POJO にすぎないこの Book クラスには、1 つだけフィールド (name) があります。
リスト 3. MyObject クラス
package com.geeckap.atomicexample;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
*
* @author shaines
*/
public class MyObject
{
private volatile Book whatImReading;
private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =
AtomicReferenceFieldUpdater.newUpdater(
MyObject.class, Book.class, "whatImReading" );
public Book getWhatImReading()
{
return whatImReading;
}
public void setWhatImReading( Book whatImReading )
{
//this.whatImReading = whatImReading;
updater.compareAndSet( this, this.whatImReading, whatImReading );
}
}
|
リスト 3 の MyObject クラスは、ご想像のとおり get メソッドと set メソッドを使って whatAmIReading プロパティーを公開しますが、set メソッドは少し違うことをします。指定された Book に内部の Book 参照を単純に割り当てる (この割り当ては、リスト 3 でコメント・アウトされたコードを使えば実現されます) 代わりに、MyObject クラスは AtomicReferenceFieldUpdater を使っています。
Javadoc によれば、AtomicReferenceFieldUpdater は以下のように定義されています。
リフレクション・ベースのユーティリティーであり、指定されたクラスの指定された volatile 参照フィールドをアトミックに更新することができます。このクラスは、アトミックなデータ構造の中で使用するように設計されており、この構造の中では、同じノードの複数の参照フィールドが独立してアトミックに更新されます。
リスト 3 で、AtomicReferenceFieldUpdater は、AtomicReferenceFieldUpdater の静的な newUpdater メソッドを呼び出すことで作成されています。newUpdater メソッドは以下の 3 つの引数を取ります。
- フィールドを含むオブジェクトのクラス (この場合は
MyObject) - アトミックに更新されるオブジェクトのクラス (この場合は
Book) - アトミックに更新されるフィールドの名前
この場合の真の価値は、まったく同期化されずに getWhatImReading メソッドが実行される一方、setWhatImReading はアトミックな処理として実行される点です。
リスト 4 には、setWhatImReading() メソッドの使い方と、値が適切に変更されることが示されています。
リスト 4. アトミックな更新を実行するテスト・ケース
package com.geeckap.atomicexample;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class AtomicExampleTest
{
private MyObject obj;
@Before
public void setUp()
{
obj = new MyObject();
obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );
}
@Test
public void testUpdate()
{
obj.setWhatImReading( new Book(
"Pro Java EE 5 Performance Management and Optimization" ) );
Assert.assertEquals( "Incorrect book name",
"Pro Java EE 5 Performance Management and Optimization",
obj.getWhatImReading().getName() );
}
}
|
アトミックなクラスについて学ぶための資料は「参考文献」を参照してください。
マルチスレッド・プログラミングはいつでも困難なものですが、Java プラットフォームが進化するにつれ、マルチスレッド・プログラミングのタスクが少し単純になってきています。この記事では、Java プラットフォームでマルチスレッド・アプリケーションを作成する場合に関し、あまり知られていない 5 つの事項として、メソッドの同期とコード・ブロックの同期の違い、スレッド単位で保持するために ThreadLocal 変数を使用する価値、正しく理解されていないことが多い volatile キーワード (そして同期化が必要な場合に volatile に頼ることの危険性)、アトミック・クラスの難解さの概略などを説明しました。さらに学ぶためには「参考文献」セクションを参照してください。
学ぶために
- 「今まで知らなかった 5 つの事項」を読み、Java プラットフォームについて皆さんがどれほど知らなかったかを学んでください。この連載は、Java 技術に関する些細な知識をプログラミングのための実用的なヒントに変えることに焦点を絞っています。
- 『Java並行処理プログラミング ―その「基盤」と「最新API」を究める』(Brian Goetz らの共著、2006年、ソフトバンククリエイティブ刊) は Java 開発者に必読の本です。著者の Brian は複雑な概念を驚くほどわかりやすく解説しています。
- 「Code Tracing」(Steven Haines 著、InformIT、2010年8月) を読み、ThreadLocal 変数を使ってコードをトレースする方法を学んでください。
- 「Java bytecode: Understanding bytecode makes you a better programmer」(Peter Haggar 著、developerWorks、2001年7月) はバイト・コードの内部を紹介したチュートリアルとして、同期メソッドと同期ブロックの違いを説明した初期の例を含めて解説しています。
- 「Javaの理論と実践: アトミックで行く」(Brian Goetz 著、developerWorks、2004年11月) は、アトミック・クラスを利用することによって非常にスケーラブルなノンブロッキング・アルゴリズムを Java 言語で作成できることを解説しています。
- 「Javaの理論と実践: (若干) シンプルになった並行性」(Brian Goetz 著、developerWorks、2002年11月) は java.util.concurrent パッケージの概要を解説しています。
- 「今まで知らなかった 5 つの事項: java.util.concurrent 第 1 回」(Ted Neward 著、developerWorks、2010年5月) は 5 つの並行コレクション・クラスを紹介し、それらのクラスによって標準的なコレクション・クラスを置き換え、並行プログラミングの要求に応える方法について解説しています。
- developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した技術記事が豊富に用意されています。
議論するために
- My developerWorks に参加し、開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。

Steven Haines は ioko の技術アーキテクトであり、GeekCap Inc. の設立者でもあります。彼は Java プログラミングとパフォーマンス分析に関する本を 3 冊執筆しており、また数百本の記事や 10 本を超えるホワイトペーパーも執筆しています。また彼は JBoss World や STPCon などの業界のカンファレンスでの講演経験もあり、以前はカリフォルニア大学アーバイン校 (University of California, Irvine) と Learning Tree University で Java プログラミングを教えていました。彼はフロリダ州オーランドの近郊に住んでいます。