複数のスレッドがブロックされて互いを待機するようになると、デッドロックが発生します。たとえば、最初のスレッドが 2 番目のスレッドでブロックされ、2 番目のスレッドが保持するリソースを待機する場合を考えます。2 番目のスレッドは、最初のスレッドが保持するリソースを獲得するまで、このリソースを解放しません。最初のスレッドは 2 番目のスレッドからリソースを獲得するまで自分のリソースを解放できず、2 番目のスレッドは最初のスレッドからリソースを獲得するまで自分のリソースを解放できないので、2 つのスレッドはデッドロックの状態になります。
デッドロックは、マルチスレッドのコードで処理する最も難しい問題の 1 つです。これは、最も予想外の位置で発生することがあるため、デッドロックを見付けて正すことは、時間のかかる大変な作業です。たとえば、複数のオブジェクトをロックする以下のコードについて考えます。
public int sumArrays(int[] a1, int[] a2)
{
int value = 0;
int size = a1.length;
if (size == a2.length) {
synchronized(a1) { //1
synchronized(a2) { //2
for (int i=0; i<size; i++)
value += a1[i] + a2[i];
}
}
}
return value;
} |
このコードは、合計操作でアクセスする前に 2 つの配列オブジェクトを正しくロックします。このコードは、短く単純で、実行する作業に適するように作成されていますが、残念なことに問題が生じる可能性があります。問題は、異なるスレッドから同じオブジェクトに対してこのメソッドを呼び出す場合について別途の注意を払わない限り、潜在的なデッドロック状態が発生する、ということです。潜在的なデッドロックを理解するには、以下の一連のイベントについて考えてください。
- 2 つの配列オブジェクト、
ArrayAおよびArrayBが作成されます。 - スレッド 1 は、以下の呼び出しで
sumArraysメソッドを呼び出します。
sumArrays(ArrayA, ArrayB); - スレッド 2 は、以下の呼び出しで
sumArraysメソッドを呼び出します。
sumArrays(ArrayB, ArrayA); - スレッド 1 は、
sumArraysメソッドの実行を開始して、//1 でパラメーター a1 のロック (この呼び出しでは、ArrayAオブジェクトのロック) を獲得します。 - スレッド 1 は、次に //2 で
ArrayBのロックを獲得する前に占有されます。 - スレッド 2 は、
sumArraysメソッドの実行を開始して、//1 でパラメーター a1 のロック (この呼び出しでは、ArrayBオブジェクトのロック) を獲得します。 - スレッド 2 は、次に //2 でパラメーター a2 のロック (
ArrayAオブジェクトのロック) を獲得しようとします。このロックは現在スレッド 1 によって保持されているので、スレッド 2 はブロックされます。 - スレッド 1 は、実行を開始して、//2 でパラメーター a2 のロック (
ArrayBオブジェクトのロック) を獲得しようとします。このロックは現在スレッド 2 によって保持されているので、スレッド 1 はブロックされます。 - これで、両方のスレッドがデッドロックの状態になります。
コードでこの問題を避ける 1 つの方法は、固定されたグローバルな順序でロックを獲得することです。この例では、スレッド 1 とスレッド 2 がsumArrays メソッドを呼び出す際のパラメーターの順序が同じであれば、デッドロックは発生しません。ただし、この技法の場合、マルチスレッドのコードのプログラマーは、パラメーターとして渡されたオブジェクトをロックするメソッドを呼び出す際に注意する必要があります。このタイプのデッドロックに遭遇して、デバッグが必要になるまで、このような技法の適用は不合理に思えるかもしれません。
別の方法として、ロックの順序をオブジェクトに組み込むことができます。この場合、コードはロックを獲得する対象のオブジェクトに照会して、適切なロック順序を判別することができます。ロックされるすべてのオブジェクトがロック順序規則をサポートし、ロックを獲得するコードがこの戦略に従っている限り、潜在的なデッドロック状態を避けることができます。
ロックの順序をオブジェクトに組み込むことの欠点は、インプリメンテーションに余分なメモリーとランタイム・コストが必要になることです。前述の例でこの技法を適用するには、配列上にロック順序情報を入れるためのラッパー・オブジェクトが必要になります。たとえば、前述の例を変更してロック順序の技法をインプリメントしたコードは、以下のようになります。
class ArrayWithLockOrder
{
private static long num_locks = 0;
private long lock_order;
private int[] arr;
public ArrayWithLockOrder(int[] a)
{
arr = a;
synchronized(ArrayWithLockOrder.class) {
num_locks++; //Increment the number of locks.
lock_order = num_locks; //Set the unique lock_order for
} //this object instance.
}
public long lockOrder()
{
return lock_order;
}
public int[] array()
{
return arr;
}
}
class SomeClass implements Runnable
{
public int sumArrays(ArrayWithLockOrder a1,
ArrayWithLockOrder a2)
{
int value = 0;
ArrayWithLockOrder first = a1; //Keep a local copy of array
ArrayWithLockOrder last = a2; //references.
int size = a1.array().length;
if (size == a2.array().length)
{
if (a1.lockOrder() > a2.lockOrder()) //Determine and set the
{ //lock order of the
first = a2; //objects.
last = a1;
}
synchronized(first) { //Lock the objects in correct order.
synchronized(last) {
int[] arr1 == a1.array();
int[] arr2 == a2.array();
for (int i=0; i<size; i++)
value += arr1[i] + arr2[i];
}
}
}
return value;
}
public void run() {
//...
}
} |
ArrayWithLockOrder クラスは、最初の例で使用した配列のラッパーとして提供されています。このクラスは、このクラスのオブジェクトが新規作成されるたびにstatic num_locks 変数を増分します。別個のlock_order インスタンス変数は、num_locks
static 変数の現行値に設定されます。これにより、このクラスの各オブジェクトがlock_order 変数に固有の値を持つことが保証されます。lock_order インスタンス変数は、このクラスの他のオブジェクトとの関連でこのオブジェクトをロックする順序を示す標識となります。
このstatic
num_locks 変数の操作は、synchronized ステートメント内から行われます。これが必要なのは、オブジェクトの各インスタンスがstatic 変数を共用するためです。したがって、2 つのスレッドがArrayWithLockOrder クラスのオブジェクトを同時に作成すると、操作元のコードが同期していないためにstatic
num_locks 変数が異常になる可能性があります。このコードを同期すれば、ArrayWithLockOrder クラスの各オブジェクトがlock_order 変数に固有の値を持つことが保証されます。
sumArrays メソッドも更新され、正しいロック順序を判別するコードが組み込まれています。ロックが要求される前に、各オブジェクトにロック順序が照会されます。値の小さい方のオブジェクトからロックされます。このコードにより、オブジェクトがメソッドに渡される順序に関係なく、常に同じ順序でロックされることが保証されます。
static num_locks フィールドとlock_order フィールドは、両方ともlong としてインプリメントされています。long データ・タイプは、64 ビットで符号の付いた 2 の補数整数としてインプリメントされます。つまり、num_locks とlock_order の値は、9,223,372,036,854,775,807 個のオブジェクトが作成された後に一巡します。この限界に達することは考えられませんが、状況によっては起こり得ます。
ロック順序の組み込みには、余分な作業、メモリー、および実行時間がいくらか必要になります。しかし、これらのタイプのデッドロック状態がコードで起こり得る場合、それだけの価値があります。余分のメモリーや実行オーバーヘッドの余裕がない場合や、num_locks またはlock_order フィールドが一巡する可能性がない場合は、オブジェクトをロックする順序を注意して定義しておく必要があります。
Peter Haggarは、アメリカのノースカロライナ州にあるResearch Triangle Parkに勤務するIBMシニア・ソフトウェア・エンジニアであり、 「 Practical Java Programming Language Guide」 (Addison-Wesley 社発行) の著者です。彼はまた、Javaプログラミングに関する多数の記事を発表しています。彼は、開発ツール、クラス・ライブラリー、およびオペレーティング・システムなど、幅広いプログラミング経験を持っています。IBMでは、未来のインターネット・テクノロジーに携わり、現在は高性能なWebサービスを中心に研究を行っています。Peterは、多くの業界の会議で頻繁に技術的なスピーチを行っています。彼はIBMに14年以上にわたって勤務しており、また、Clarkson Universityよりコンピューター・サイエンスのB.S. を取得しています。Peterの連絡先はhaggar@us.ibm.com です。