スレッドセーフの設計

6か月前、クラスとオブジェクトの設計に関する一連の記事を始めました。今月の「設計手法」のコラムでは、スレッドセーフに関係する設計原則を見て、そのシリーズを続けます。この記事では、スレッドセーフとは何か、なぜそれが必要なのか、いつ必要なのか、そしてそれを取得する方法について説明します。

スレッドセーフとは何ですか?

スレッドセーフとは、複数のスレッドで同時に使用されている場合でも、他のオブジェクトやクラスで観察されるように、オブジェクトまたはクラスのフィールドが常に有効な状態を維持することを意味します。

このコラムで提案した最初のガイドラインの1つ(「オブジェクトの初期化の設計」を参照)は、オブジェクトが有効期間の最初から最後まで有効な状態を維持するようにクラスを設計する必要があるというものです。このアドバイスに従って、インスタンス変数がすべてプライベートであり、メソッドがそれらのインスタンス変数に対して適切な状態遷移のみを行うオブジェクトを作成すると、シングルスレッド環境で良好な状態になります。ただし、スレッドが増えると問題が発生する可能性があります。

多くの場合、メソッドの実行中にオブジェクトの状態が一時的に無効になる可能性があるため、複数のスレッドがオブジェクトに問題を引き起こす可能性があります。 1つのスレッドだけがオブジェクトのメソッドを呼び出している場合、一度に1つのメソッドのみが実行され、別のメソッドが呼び出される前に各メソッドを終了できます。したがって、シングルスレッド環境では、メソッドが戻る前に、各メソッドに一時的に無効な状態が有効な状態に変更されていることを確認する機会が与えられます。

ただし、複数のスレッドを導入すると、オブジェクトのインスタンス変数が一時的に無効な状態のままで、JVMが1つのメソッドを実行するスレッドを中断する場合があります。次に、JVMは別のスレッドに実行の機会を与え、そのスレッドは同じオブジェクトのメソッドを呼び出すことができます。インスタンス変数をプライベートにし、メソッドが有効な状態変換のみを実行するためのすべてのハードワークは、この2番目のスレッドが無効な状態のオブジェクトを監視するのを防ぐのに十分ではありません。

マルチスレッド環境では、オブジェクトが破損したり、無効な状態であることが確認されたりする可能性があるため、このようなオブジェクトはスレッドセーフではありません。スレッドセーフオブジェクトは、マルチスレッド環境であっても、他のクラスやオブジェクトで観察されるように、常に有効な状態を維持するオブジェクトです。

なぜスレッドセーフを心配するのですか?

Javaでクラスとオブジェクトを設計するときに、スレッドセーフについて考慮する必要がある2つの大きな理由があります。

  1. 複数のスレッドのサポートは、Java言語とAPIに組み込まれています

  2. Java仮想マシン(JVM)内のすべてのスレッドは、同じヒープとメソッド領域を共有します

マルチスレッドはJavaに組み込まれているため、設計したクラスが最終的に複数のスレッドによって同時に使用される可能性があります。スレッドセーフは無料ではないので、設計するすべてのクラスをスレッドセーフにする必要はありません(また、そうすべきではありません)。ただし、Javaクラスを設計するたびに、少なくともスレッドセーフについて考える必要があります。スレッドセーフのコストと、クラスをスレッドセーフにするタイミングに関するガイドラインについては、この記事の後半で説明します。

JVMのアーキテクチャーを考えると、スレッドの安全性について心配する場合は、インスタンス変数とクラス変数のみを考慮する必要があります。すべてのスレッドが同じヒープを共有し、ヒープはすべてのインスタンス変数が格納される場所であるため、複数のスレッドが同じオブジェクトのインスタンス変数を同時に使用しようとする可能性があります。同様に、すべてのスレッドが同じメソッド領域を共有し、メソッド領域はすべてのクラス変数が格納される場所であるため、複数のスレッドが同じクラス変数を同時に使用しようとする可能性があります。クラスをスレッドセーフにすることを選択した場合の目標は、マルチスレッド環境で、そのクラスで宣言されたインスタンス変数とクラス変数の整合性を保証することです。

ローカル変数、メソッドパラメータ、および戻り値へのマルチスレッドアクセスについて心配する必要はありません。これらの変数はJavaスタックに存在するためです。JVMでは、各スレッドに独自のJavaスタックが与えられます。スレッドは、別のスレッドに属するローカル変数、戻り値、またはパラメーターを表示または使用できません。

JVMの構造を考えると、ローカル変数、メソッドパラメーター、および戻り値は本質的に「スレッドセーフ」です。ただし、インスタンス変数とクラス変数は、クラスを適切に設計した場合にのみスレッドセーフになります。

RGBColor#1:シングルスレッドの準備ができました

スレッドセーフではないクラスの例として、RGBColor以下に示すクラスについて考えてみます。 :このクラスのインスタンスは3つのプライベートインスタンス変数に格納されている色を表現するrgb。以下に示すクラスを考えると、RGBColorオブジェクトは有効な状態でその寿命を開始し、その寿命の始まりから終わりまで、有効な状態の遷移のみを経験しますが、シングルスレッド環境でのみ発生します。

//ファイルthreads / ex1 / RGBColor.java//このクラスのインスタンスはスレッドセーフではありません。 public class RGBColor {private int r; private int g; private int b; public RGBColor(int r、int g、int b){checkRGBVals(r、g、b); this.r = r; this.g = g; this.b = b; } public void setColor(int r、int g、int b){checkRGBVals(r、g、b); this.r = r; this.g = g; this.b = b; } / ** * R、G、Bの3つの整数の配列で色を返します* / public int [] getColor(){int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; retValを返します。 } public void invert(){r = 255-r; g = 255-g; b = 255-b; } private static void checkRGBVals(int r、int g、int b){if(r 255 || g 255 || b <0 || b> 255){throw new IllegalArgumentException(); }}}

3つのインスタンス変数、あるためintrgとはb、プライベートで、他のクラスとオブジェクトは、これらの変数の値にアクセスしたり、影響を与えることができる唯一の方法は、経由でRGBColorのコンストラクタとメソッド。コンストラクターとメソッドの設計により、次のことが保証されます。

  1. RGBColorのコンストラクタは常に変数に適切な初期値を与えます

  2. メソッドsetColor()invert()は、これらの変数に対して常に有効な状態変換を実行します

  3. メソッドgetColor()は常にこれらの変数の有効なビューを返します

不正なデータがコンストラクターまたはsetColor()メソッドに渡された場合、それらはInvalidArgumentException。で突然完了することに注意してください。checkRGBVals()それがために何を意味するのかエフェクト定義では、この例外をスローするメソッド、RGBColorオブジェクトが有効であるとは:すべての3つの変数の値は、rg、とb、0〜255、包括的でなければなりません。さらに、有効であるためには、これらの変数によって表される色は、コンストラクターまたはsetColor()メソッドに渡されるか、invert()メソッドによって生成される最新の色である必要があります。

シングルスレッド環境で、setColor()青を呼び出して渡すと、RGBColorオブジェクトはsetColor()戻ったときに青になります。その後getColor()、同じオブジェクトを呼び出すと、青色になります。シングルスレッド社会では、このRGBColorクラスのインスタンスは正常に動作します。

コンカレントレンチを作品に投げ込む

残念ながら、行儀の良いRGBColorオブジェクトのこの幸せな写真は、他のスレッドが写真に入ると怖くなる可能性があります。マルチスレッド環境では、RGBColor上記で定義されたクラスのインスタンスは、書き込み/書き込みの競合と読み取り/書き込みの競合の2種類の不正な動作の影響を受けやすくなります。

書き込み/書き込みの競合

「赤」という名前のスレッドと「青」という名前のスレッドの2つのスレッドがあるとします。両方のスレッドが同じRGBColorオブジェクトの色を設定しようとしています。赤いスレッドは色を赤に設定しようとしています。青い糸が色を青に設定しようとしています。

これらのスレッドは両方とも、同じオブジェクトのインスタンス変数に同時に書き込もうとしています。スレッドスケジューラがこれらの2つのスレッドを正しい方法でインターリーブすると、2つのスレッドが不注意に相互に干渉し、書き込み/書き込みの競合が発生します。その過程で、2つのスレッドがオブジェクトの状態を破壊します。

非同期RGBColorアプレット

Unsynchronized RGBColorという名前の次のアプレットは、RGBColorオブジェクトが破損する可能性のある一連のイベントを示しています。赤い糸は無邪気に色を赤に設定しようとしていますが、青い糸は無邪気に色を青に設定しようとしています。結局、RGBColorオブジェクトは赤でも青でもないが、不安定な色であるマゼンタを表しています。

何らかの理由で、ブラウザではこのようにクールなJavaアプレットを表示できません。

RGBColorオブジェクトの破損につながる一連のイベントをステップスルーするには、アプレットの[ステップ]ボタンを押します。[戻る]を押してステップをバックアップし、[リセット]を押し​​て最初にバックアップします。進むにつれて、アプレットの下部にある1行のテキストで、各ステップで何が起こっているかが説明されます。

アプレットを実行できない方のために、アプレットによって示される一連のイベントを示す表を次に示します。

ステートメント r g b
なし オブジェクトは緑を表します 0 255 0  
青い 青いスレッドはsetColor(0、0、255)を呼び出します 0 255 0  
青い checkRGBVals(0, 0, 255); 0 255 0  
青い this.r = 0; 0 255 0  
青い this.g = 0; 0 255 0  
青い 青が横取りされます 0 0 0  
赤いスレッドはsetColor(255、0、0)を呼び出します 0 0 0  
checkRGBVals(255, 0, 0); 0 0 0  
this.r = 255; 0 0 0  
this.g = 0; 255 0 0  
this.b = 0; 255 0 0  
赤い糸が戻る 255 0 0  
青い 後で、青い糸が続く 255 0 0  
青い this.b = 255 255 0 0  
青い 青い糸が戻る 255 0 255  
なし オブジェクトはマゼンタを表します 255 0 255  

このアプレットとテーブルからわかるようにRGBColor、オブジェクトが一時的に無効な状態にある間にスレッドスケジューラが青いスレッドに割り込むため、が破損しています。赤い糸が入ってオブジェクトを赤く塗ると、青い糸は部分的にしかオブジェクトを青く塗り終えません。青いスレッドが戻ってジョブを終了すると、オブジェクトが誤って破損します。

読み取り/書き込みの競合

Another kind of misbehavior that may be exhibited in a multithreaded environment by instances of this RGBColor class is read/write conflicts. This kind of conflict arises when an object's state is read and used while in a temporarily invalid state due to the unfinished work of another thread.

For example, note that during the blue thread's execution of the setColor() method above, the object at one point finds itself in the temporarily invalid state of black. Here, black is a temporarily invalid state because:

  1. It is temporary: Eventually, the blue thread intends to set the color to blue.

  2. It is invalid: No one asked for a black RGBColor object. The blue thread is supposed to turn a green object into blue.

If the blue thread is preempted at the moment the object represents black by a thread that invokes getColor() on the same object, that second thread would observe the RGBColor object's value to be black.

Here's a table that shows a sequence of events that could lead to just such a read/write conflict:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes getColor() 0 0 0  
red int[] retVal = new int[3]; 0 0 0  
red retVal[0] = 0; 0 0 0  
red retVal[1] = 0; 0 0 0  
red retVal[2] = 0; 0 0 0  
red return retVal; 0 0 0  
red red thread returns black 0 0 0  
blue later, blue thread continues 0 0 0  
blue this.b = 255 0 0 0  
blue blue thread returns 0 0 255  
none object represents blue 0 0 255  

As you can see from this table, the trouble begins when the blue thread is interrupted when it has only partially finished painting the object blue. At this point the object is in a temporarily invalid state of black, which is exactly what the red thread sees when it invokes getColor() on the object.

Three ways to make an object thread-safe

There are basically three approaches you can take to make an object such as RGBThread thread-safe:

  1. Synchronize critical sections
  2. Make it immutable
  3. Use a thread-safe wrapper

Approach 1: Synchronizing the critical sections

The most straightforward way to correct the unruly behavior exhibited by objects such as RGBColor when placed in a multithreaded context is to synchronize the object's critical sections. An object's critical sections are those methods or blocks of code within methods that must be executed by only one thread at a time. Put another way, a critical section is a method or block of code that must be executed atomically, as a single, indivisible operation. By using Java's synchronized keyword, you can guarantee that only one thread at a time will ever execute the object's critical sections.

To take this approach to making your object thread-safe, you must follow two steps: you must make all relevant fields private, and you must identify and synchronize all the critical sections.

Step 1: Make fields private

同期とは、一度に1つのスレッドだけが少しのコード(クリティカルセクション)を実行できることを意味します。したがって、複数のスレッド間でアクセスを調整するのはフィールドですが、そのためのJavaのメカニズムは、実際にはコードへのアクセスを調整します。つまり、データをプライベートにした場合にのみ、データを操作するコードへのアクセスを制御することで、そのデータへのアクセスを制御できます。