クラス初期化子
1:「AA01.list1に対しマルチスレッドアクセス」の場合
マルチスレッドでアクセスしても、クラス初期化子は最初の1度しか実行されないので他から影響を受けることがない。
クラス初期化子は、スレッドセーフであり、一度しか実行されないことが保証されます。(クラスローダー単位の動作)
インスタンス初期化子
2:「static AA01 ins = new AA01()のように静的インスタンスにマルチスレッドアクセス」の場合
ins.list2は当インスタンス生成時に1度のみ実行されるので、マルチスレッドでアクセスしてもインスタンス初期化子内に他から割り込むことがない。
「割り込む」は、別スレッドが初期化子実行中に「インスタンス変数にアクセス可能」の意味だとします。
インスタンス初期化子の処理は、すべてのコンストラクターの先頭(super();の直後。this();は除く)に、同じ処理がコピーされます。従って、初期化子の動作はコンストラクターの動作に含まれます。「コンストラクターはスレッドセーフではない」 インスタンスの生成とメンバ変数への代入をバイトコードで示します。
Java
11: new #nn // class AA01
22: dup
33: invokespecial #mm // Method AA01."<init>":()V
44: putfield #ll // Field <メンバ変数名>:AA01;
- newは、ヒープメモリにオブジェクトを割り付ける。
newの戻り値は参照値(オブジェクトリファレンス)である。
- 参照値を複製する。
- コンストラクターメソッド<init>:()vを、参照値(this)に対して実行。オブジェクトを初期化する。
この処理過程で、super()の連鎖によってクラスごとにインスタンスの初期値が設定される。
- メンバ変数への参照値の格納 (putfieldでなくローカル変数への格納istoreでもよい)
ステップ3が完了する前に、参照値(this)が別のスレッドから利用できる状況が生じると矛盾が起きる。
TSM03-J. 初期化が完了していないオブジェクトを公開しない
補足説明(追記)
訂正:(putfieldでなくローカル変数への格納istoreでもよい)は取り下げます。putfieldだけに限定します。
コンストラクターの動作
コンストラクターはスレッドセーフではないの意味は、まず言語仕様を引用します。
There is no practical need for a constructor to be synchronized, because it would lock the object under construction, which is normally not made available to other threads until all constructors for the object have completed their work.
コンストラクターにsynchronized
修飾が必要ない理由説明。インスタンスが生成される一般的なシナリオでは、コンストラクターを実行するスレッドは、当該オブジェクトの参照値を占有しており、コンストラクターが完了するまで他のスレッドに公開することがないから。(synchronized
できないが、)ロックしているのと同じ意味だと解釈します。
上のバイトコードにあてはめると、ステップ1〜4までが同一スレッドで実行されるため、ステップ4で他のスレッドに公開される時は、ステップ3が完了してることが保障される。と読むことができます。
しかし、一般的なシナリオからの逸脱があり、ステップ3の途中を他のスレッドから観ることができることがJPCERTの記事で説明されています。
バイトコードのステップごとの状態
バイトコードのステップごとにオブジェクトの状態を考えます。
オブジェクトの領域をアロケート済み。変数にはすべてビット0が設定されている。参照値は取得済。
コンストラクターメソッド内でメンバ変数の初期値を設定。完了するまでに中間状態がある。
オブジェクトのメンバ変数は完全に設定済み。
JPCERTの記事
「コンパイラがバイトコードのステップ3と4を入れ替えてもよい」との記述があります。ステップ4が先なら、変数にすべてビット0が設定されている状態を他のスレッドに見せることができます。これは質問の反例になりませんか。
JPCERTの記事(逸出)
TSM01-J. オブジェクトの構築時にthis参照を逸出させない これも質問の反例になりうると思います。
わたしの確認コード(逸出)
static フィールドへの参照値の逸出。CountDownLatch
を利用した逸出確認。メモリ整合性が保証されるので初期化前後のオブジェクトの内部状態を確実に見ることができます。以下は逸出させる側のオブジェクト。
Java
1public class Escaped {
2
3 final int i;
4 final String s;
5
6 {
7 EscapedHolder.escaped = this; // thisの逸出(escape)
8 EscapedHolder.startSignal.countDown(); // 待ちスレッドを解放
9 try {
10 EscapedHolder.doneSignal.await(); // 待機
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 i = 100;
15 s = "Escape_";
16 }
17
18 public Escaped() {
19 super();
20 }
21
22}
staticフィールドで参照値の逸出をうける。逸出したオブジェクトの内容をスレッドで確認する。
Java
1import java.util.concurrent.CountDownLatch;
2
3public class EscapedHolder {
4
5 static Escaped escaped; // 逸出先
6 static final CountDownLatch startSignal = new CountDownLatch(1);
7 static final CountDownLatch doneSignal = new CountDownLatch(1);
8
9 public static void main(String[] args) {
10
11 Thread t = new Thread(() -> {
12 try {
13 startSignal.await();
14 } catch (InterruptedException e) {
15 e.printStackTrace();
16 }
17 System.out.println("escaped : i = " + EscapedHolder.escaped.i + " s = " + EscapedHolder.escaped.s);
18 doneSignal.countDown();
19 });
20 t.setDaemon(true);
21 t.start();
22
23 System.out.println(">>>>>> before construction. >>>>>>");
24 Escaped instance = new Escaped();
25 System.out.println(">>>>>> after construction. >>>>>>");
26
27 System.out.println("instance : i = " + instance.i + " s = '" + instance.s + "'");
28 System.out.println("escaped : i = " + EscapedHolder.escaped.i + " s = '" + EscapedHolder.escaped.s + "'");
29
30 }
31
32}