- PR -

非同期下におけるダブルチェックロッキングのアンチパターンについて

1
投稿者投稿内容
よろしくお願いします。
常連さん
会議室デビュー日: 2006/01/17
投稿数: 21
投稿日時: 2009-02-28 23:24
とあることから、ある書籍※1を読んでいるのですが、その書籍の中に
 「非同期下におけるダブルチェックロッキングのアンチパターン」
ということに関して書かれている部分がありました。

書かれていた内容を要約すると、

 以下のようなクラス(「AntiDclクラス」)は、使用する側がマルチスレッドだった場合、
 「getObjectメソッド」を使用し取得した「Objectインスタンス」は、
  中途半端※2な「Objectインスタンス」
 を取得することがある。

 -マルチスレッドでは正常に動作しないクラス-
 01: public class AntiDcl {
 02:  private static Object obj;
 03:
 04:  public static Object getObject() {
 05:   if (obj == null) {
 06:    synchronized(DCL.class) {
 07:     if (obj == null) {
 08:      obj = new Object();
 09:     }
 10:     return obj;
 11:    }
 12:   }
 13:   return obj;
 14:  }
 15: }

とのことでした。

どうして、
 中途半端※2な「Objectインスタンス」を取得することがある。
となるのかを、当書籍※1を読んで自分なりにまとめてみました。


〜前提〜
・JVMは特定の条件(「事前発生のルール」)下でない限り、各スレッドを半順序※3にて処理する。
・「事前発生のルール」の1つに、
   複数のスレッドから同一のインスタンスに対してsynchronizedすると、
   ロック取得〜ロック解除までの処理が各スレッドにて全順序※4となる
 がある。
 ※以降、当ルールを『synchronizedルール』と記述

〜まとめ〜
上記例にある「AntiDclクラス」は、06〜11行目にのみsynchronizedを行っている。
 →『synchronizedルール』が適用されるのは、06〜11行目のみとなる。
その為、
 スレッドAが、完全に生成処理が完了していないが「Objectクラス」インスタンスのアドレスを
 「obj変数」へとりあえず設定。
 スレッドBは、05行目にあるif文にて、『nullではなくなった』と判断し中途半端な「Objectインスタンス」を取得。
といったことが発生してしまう。


【質問1】
以上のように理解していますが、解釈に間違いはないでしょうか?


【質問2】
上記問題を解決する方法としていくつ書かれていたのですが、その中の方法の1つに、
 『java5.0以降のJMM※5の場合、obj変数をvolatileとすることで解決する』
とありました。
volatileとは、
 複数のスレッドから参照(read/write)される場合においても、同期性を確保することができる
と理解しています。
これは、Javaのバージョンに問わずだと思っていました。
解決方法として「volatileとする」は理解できるのですが、
 『java5.0以降のJMM※5の場合』
とありました。過去のバージョンのJVM(JMM)ではそうではなかったのでしょうか?


以上、何かご存知の方がいらっしゃたらご教授の程よろしくお願いします。m(_ _)m



※1…Java 並行処理プログラミング -その「基盤」と「最新API」を究める-
※2…実際にはこのように書かれていません^^; あくまで要約ですm(_ _)m
※3…『スレッドAが処理(計算)した結果をスレッドBが確実に参照できない』と解釈しています
※4…『スレッドAが処理(計算)した結果をスレッドBが確実に参照できる』と解釈しています
※5…JavaMemoryModel
わたなべ
大ベテラン
会議室デビュー日: 2007/12/09
投稿数: 123
お住まい・勤務地: 札幌
投稿日時: 2009-03-01 00:44
Java 並行処理プログラミング -その「基盤」と「最新API」を究める-
P392 16-2-4 ダブルチェックブロッキング より引用
『排他性は理解されていましたが、可視性についてはノーでした』

入手できたことは良かったと思いますが、もう少しゆっくりと読み込んでみてはどうでしょう?
2章3章の基礎を読んでいれば、理解できると思います。

volatileについて
http://www.itarchitect.jp/technology_and_programming/-/24161-4.html
よろしくお願いします。
常連さん
会議室デビュー日: 2006/01/17
投稿数: 21
投稿日時: 2009-03-02 01:12
〜わたなべ様〜
度々のご教授ありがとうございます。
ご指摘頂いた通り、再度、2〜3章を読み返しております。
また、
http://www.ibm.com/developerworks/jp/java/library/j-jtp02244/
http://www.ibm.com/developerworks/jp/java/library/j-jtp03304/
http://www.ibm.com/developerworks/jp/java/library/j-dcl/
のサイトもあわせて勉強を進めております。

そこでなのですが、上記URLの2番目
 『Javaの理論と実践: Javaメモリ・モデルを修正する 第2回』
の記事中に、
--------------------------------------------------------------------------
新しいJMMは、非公式に、happens-before(事前発生)と呼ばれる配列を定義していますが、これは次に示すようにプログラム内で行われる全操作の部分配列です。
@同じスレッド内で、プログラム配列の前の方に出てくる操作は、後に出てくる操作よりもhappens-beforeである
A同じモニター上で、モニターのアンロックはロックよりもhappens-beforeである
Bvolatileフィールドへの書き込みは、それに続く、同じvolatileの読み込みよりもhappens-beforeである
CあるスレッドのThread.start()へのコールは、開始されたそのスレッドでのどの操作よりもhappens-beforeである
Dスレッドでの全操作は、そのスレッドのThread.join()から成功(success)して戻る他のどのスレッドよりもhappens-beforeである
--------------------------------------------------------------------------
と、半順序/全順序及びその可視性について書かれている箇所がありました。
今回に関連するところは、ABの部分だと思うのですが、この文章を意味は、
--------------------------------------------------------------------------
A同じモニター上で、モニターのアンロックはロックよりも事前発生(happens-before)である
Bvolatileフィールドへの書き込みは、それに続く、同じvolatileの読み込みよりも事前発生(happens-before)である
--------------------------------------------------------------------------
となり、
--------------------------------------------------------------------------
A同じモニター上で、モニターのアンロックの前に行われていた処理はロックよりも必ず先に実行され、メインメモリにその結果が格納される。
Bvolatileフィールドへの書き込みを行うと、書き込みの前に行われていた処理は、それに続く、同じvolatileの読み込みよりも必ず先に実行され、メインメモリにその結果が格納される。
--------------------------------------------------------------------------
と、解釈しておりますが問題ないでしょうか?


また、ご教授くださったサイト(volatileについて)を拝見させて頂いたところ、
--------------------------------------------------------------------------
JSR 133ではより制限を強め、作業メモリとメイン・メモリの同期に関して、volatile変数とロックが同じように動作するように定めている。すなわち、volatile変数に書き込むと作業メモリの内容をメイン・メモリに書き出し、volatile変数を読み出すと作業メモリの内容を無効にする。
--------------------------------------------------------------------------
との記述がありました。上記、文章の
 volatile変数を読み出すと作業メモリの内容を無効にする。
部分に関しては、先に記述した@〜Dのルールとは関係ないと解釈しておりますが、問題ないでしょうか?

〜余談〜
どうも、synchronizedとvolatileにおける
・可視性
・アトミック性
・全順序(プログラム配列保証)
がごちゃごちゃになってきて…。
まず、それぞれ(可視性、アトミック性、全順序)の定義を明確にし、その後、synchronizedとvolatileにおける上記3項目についてどうなのかをまとめたいと思っています。
これを理解しまとめることができたら、自分のものになると考えているのですが…
これもずれているのかな…
よろしくお願いします。
常連さん
会議室デビュー日: 2006/01/17
投稿数: 21
投稿日時: 2009-03-04 01:53
個々の定義を自分なりにまとめてみました。
※「全順序(プログラム配列保証)」を「順序変更」と変更しました。

1. 順序変更
javaコンパイラ及びjavavmは、セマンティクス上の依存関係がない場合、コーディングされているプログラムの処理の順序を変更することがある。
例えば、
 01: int a,b;
 02: a = 1;
 03: b = 2;
 04: System.out.println("a=[" + a "]");
 05: System.out.println("b=[" + b "]");
とコーディングされていても、変数a,bへの値設定は、
 「aに1を設定」→「bに2を設定」
の順ではなく、
 「bに2を設定」→「aに1を設定」
ということが発生しえる。
これを『順序変更』という。


2. アトミック性(操作)
いくつかの処理を組み合わせたもので、それらがひとつの処理となる。
また、その処理が全て完了するまで、他のスレッドからは、処理中のいかなる途中結果も参照することができない。
このような一連の処理を『アトミック性(操作)』という。


3. 可視性
各スレッドから参照/設定される領域(変数)は、「メインメモリ」※1にある領域(変数)へ直接アクセスしている訳ではない。
「メインメモリ」にある領域(変数)を「ローカルメモリ」※2に複製したものへ参照/設定を行う。
また、各スレッドから設定(更新)された変数は、「任意のタイミング」 or 「ある一定のルール」に従って、「メインメモリ」へ書き戻される。

これは、
 マルチスレッドにて実行されている複数のスレッドが同一の領域(変数)を参照していても、
 それぞれのスレッドで値が異なることがる。
ということである。

『可視性』とは、
 各スレッドから「メインメモリ」に対して参照/設定する。
ということである。


※1…各スレッドから、
    参照要求があった場合に複製元となる
    設定要求があった場合に最終的な設定先となる
   となる場所(装置)。
※2…各スレッド毎にある、「メインメモリ」の複製が格納される場所(装置)。



以上を踏まえた上で、Java(JMM)では上記3点についてどのような処置が施されているかをまとめてみたいと思います。
よろしくお願いします。
常連さん
会議室デビュー日: 2006/01/17
投稿数: 21
投稿日時: 2009-03-04 05:15
Java1.5以降のJMMは、happens-before(事前発生)と呼ばれるルールが存在する。
 @同じスレッド内で、プログラム配列の前の方に出てくる操作は、後に出てくる操作よりもhappens-before(事前発生)である
 A同じモニター上で、モニターのアンロックはロックよりもhappens-before(事前発生)である
 Bvolatileフィールドへの書き込みは、それに続く、同じvolatileの読み込みよりもhappens-before(事前発生)である
 CあるスレッドのThread.start()へのコールは、開始されたそのスレッドでのどの操作よりもhappens-before(事前発生)である
 Dスレッドでの全操作は、そのスレッドのThread.join()から成功(success)して戻る他のどのスレッドよりもhappens-before(事前発生)である
上記、Aのルールを具体的に説明する。

A同じモニター上で、モニターのアンロックはロックよりもhappens-before(事前発生)である
これは、

 ・複数のスレッドにて同じインスタンスに対してsynchronaizeしていた場合、
   スレッドAがsynchronaizeブロック(メソッド)を出る その後、 スレッドBがsynchronaizeブロック(メソッド)を入る
  という順に処理が実行される事を保障する。
 ・複数のスレッドにて同じインスタンスに対してsynchronaizeしていた場合、
   スレッドAがsynchronaizeブロック(メソッド)を出るまでに行った操作※1は、その後、実行される
   スレッドBがsynchronaizeブロック(メソッド)へ入ると、全て可視(性)となる
  という事を保障する。
  ※1…スレッドAがsynchronaizeブロック(メソッド)に入る前に行われていた操作も含む

ということです。

<例>
---テスト用クラス---
01: import static java.lang.System.out;
02: public class SynchronaizeTest {
03:  public static Object lock = new Object();
04:  public static int x = 0;
05:  public static int y = 0;
06:  
07:  static public void main(String args[]) {
08:   new Thread(new Runnable() {
09:    public void run() {
10:     out.println(getName() + " start");
11:     
12:     x = 1;
13:     
14:     synchronized(lock) {
15:      out.println(getName() + " enter_block");
16:      
17:      new Thread(new Runnable() {
18:       public void run() {
19:        out.println(" " + getName() + " start");
20:        
21:        synchronized(lock) {
22:         out.println(" " + getName() + " enter_block");
23:         
24:         out.println(" " + getName() + " y=" + y);
25:         
26:         out.println(" " + getName() + " exit_block");
27:        }
28:        
29:        out.println(" " + getName() + " x=" + x);
30:        
31:        out.println(" " + getName() + " end");
32:       }
33:      }).start();
34:      
35:      y = 2;
36:      
37:      sleep(5000);
38:      
39:      out.println(getName() + " exit_block");
40:     }
41:     
42:     sleep(5000);
43:     
44:     out.println(getName() + " end");
45:    }
46:   }).start();
47:  }
48:  
49:  static public void sleep(int time) {
50:   try { Thread.sleep(time); } catch (InterruptedException e){out.println(e);}
51:  }
52:  
53:  static public String getName() {
54:   return Thread.currentThread().getName();
55:  }
56: }


---実行結果---
01: Thread-0 start
02: Thread-0 enter_block
03: Thread-1 start
04: Thread-0 exit_block
05: Thread-1 enter_block
06: Thread-1 y=2
07: Thread-1 exit_block
08: Thread-1 x=1
09: Thread-1 end
10: Thread-0 end


---考察1---
@2つのスレッド(最初に起動されるスレッドt0。t0から起動されるスレッドt1)は、共に[lock変数]にてsynchronaizeを行っている。
 →テスト用クラス 14行目、21行目
At1は、t0のsynchronaizeブロック内から起動されてる。
 →テスト用クラス 17行目
B実行結果として「Thread-1 start」が表示された後、しばらくしてから「Thread-0 exit_block」が表示され、
 「Thread-1 enter_block」が表示された。
 →実行結果 03〜05行目

t1が起動され「Thread-1 start」が表示された後すぐに、「Thread-1 enter_block」が表示されていません。
これは、t0が[lock変数]のロックを解除せずに、37行目にてスリープしている為に、t1が21行目にてロック解除待ちとなっている為です。

以上のことが、
 複数のスレッドにて同じインスタンスに対してsynchronaizeしていた場合、
  スレッドAがsynchronaizeブロック(メソッド)を出る その後、 スレッドBがsynchronaizeブロック(メソッド)を入る
 という順に処理が実行される事を保障する。
の具体的な例です。
※今回の場合、「スレッドA=t0」、「スレッドB=t1」となります。

また、この動きは、
 複数のスレッドにて同じインスタンスに対してsynchronaizeしていた場合、
 synchronaizeブロック(メソッド)内のアトミック性(操作)も保障する
ということになります。
※synchronaizeブロックの外にて操作されている[変数x]への操作はアトミック性(操作)となっていません。


---考察2---
@考察1の@〜B
At0にてsynchronaizeブロックの外から[変数x]へ"1"を設定している。
 →テスト用クラス 12行目
Bt0にてsynchronaizeブロックの内から[変数y]へ"2"を設定している。
 →テスト用クラス 35行目
Ct0が終了していない状態にて、t1から[変数x]、[変数y]を表示。
 →テスト用クラス 24行目、29行目
D実行結果として「Thread-1 y=2」、「Thread-1 x=1」が表示される。
 →実行結果 06行目、08行目

t0にて
 synchronaizeブロックの外から[変数x]へ"1"を設定
 synchronaizeブロックの内から[変数y]へ"2"を設定
されている。
その後、
 t0はsynchronaizeブロックを出る(「Thread-0 exit_block」が表示される)が、
 t0が終了していない(「Thread-0 end」が表示されない)
という状態になる。
この状態中に、t1は、[変数lock]のロックを取得しsynchronaizeブロックに入る。その後、t1にて
 [変数x]には"1"(実行結果 08行目)
 [変数y]には"2"(実行結果 06行目)
と設定されている値が表示される。

以上のことが、
 複数のスレッドにて同じインスタンスに対してsynchronaizeしていた場合、
  スレッドAがsynchronaizeブロック(メソッド)を出るまでに行った操作※1は、その後、実行される
  スレッドBがsynchronaizeブロック(メソッド)へ入ると、全て可視(性)となる
 という事を保障する。
 ※1…スレッドAがsynchronaizeブロック(メソッド)に入る前に行われていた操作も含む
の具体的な例です。


---まとめ---
○synchronaizeにおける『順序変更』について
synchronaizeを用いても、『順序変更』は発生します。しかしながら、同一モニターでのロック/アンロックにて処理の同期化(他スレッドの排他)を行うことが可能な為、マルチスレッド化において『順序変更』が発生しても期待した結果を得ることができます。
※先に示した自分なりの定義にて書いてる『順序変更』は、発生すると思っています。でも…あぁ〜違うかも…

○synchronaizeにおける『アトミック性(操作)』について
同一モニターでのロック/アンロックを用いることで、他のスレッドに対して排他をかけることが出来るので、synchronaizeブロック(メソッド)中の処理は完全に『アトミック性(操作)』となります。

○synchronaizeにおける『可視性』について
モニターロック解除までに行った全ての操作はロック解除後、同一のモニターに対してロックを取得したスレッドでは全て『可視性』となります。

◎余談
当初、『順序変更』、『アトミック性(操作)』、『可視性』を分けて考えていこうと思っていました。
しかし、いろいろわかって来た結果、この3つは、
 それぞれがそれぞれとしてあるためにそれぞれを利用し成り立っている
 (うまく表現できませんorz...)
のだと気付きました。




かなり長文になってしまいましたが、ご意見・ご指摘頂ければ大変ありがたいです。

※次は、
  Bvolatileフィールドへの書き込みは、それに続く、同じvolatileの読み込みよりもhappens-before(事前発生)である
 について今回と同様、まとめてみたいと思います。
よろしくお願いします。
常連さん
会議室デビュー日: 2006/01/17
投稿数: 21
投稿日時: 2009-03-04 22:09
Bvolatileフィールドへの書き込みは、それに続く、同じvolatileの読み込みよりもhappens-before(事前発生)である
を具体的に説明する。


volatileフィールド(変数)とは、

 volatile対象となるフィールド(変数)(以降、[変数y])1に対して、対となる自動的にモニター
 (以降、[変数auto_lock])が1つ生成され、[変数y]にアクセス(参照/更新)する際は、
 自動生成された[変数auto_lock]へsynchronaizeが行われる。

ということです。

<例>
---テスト用クラス---
01: import static java.lang.System.out;
02: public class VolatileTest {
03:  public static int x = 0;
04:  public static volatile int y = 0;
05:  
06:  static public void main(String args[]) {
07:   new Thread(new Runnable() {
08:    public void run() {
09:     out.println(getName() + " start");
10:     
11:     x = 1;
12:     
13:     y = 2;
14:     
15:     sleep(5000);
16:     
17:     new Thread(new Runnable() {
18:      public void run() {
19:       out.println(" " + getName() + " start");
20:       
21:       out.println(" " + getName() + " y=" + y);
22:       
23:       out.println(" " + getName() + " x=" + x);
24:       
25:       out.println(" " + getName() + " end");
26:      }
27:     }).start();
28:     
29:     sleep(1000);
30:     
31:     out.println(getName() + " end");
32:    }
33:   }).start();
34:  }
35:  
36:  static public void sleep(int time) {
37:   try { Thread.sleep(time); } catch (InterruptedException e){out.println(e);}
38:  }
39:  
40:  static public String getName() {
41:   return Thread.currentThread().getName();
42:  }
43: }


---実行結果---
01: Thread-0 start
02: Thread-1 start
03: Thread-1 y=2
04: Thread-1 x=1
05: Thread-1 end
06: Thread-0 end


---まとめ---
以下の「VolatileToSynchronizedTestクラス」と上記、「VolatileTestクラス」は等価である。
01: import static java.lang.System.out;
02: public class VolatileToSynchronizedTest {
03:  public static Object auto_lock = new Object();
04:  public static int x = 0;
05:  public static int y = 0;
06:  
07:  static public void main(String args[]) {
08:   new Thread(new Runnable() {
09:    public void run() {
10:     out.println(getName() + " start");
11:     
12:     x = 1;
13:     
14:     synchronized(auto_lock){y = 2;}
15:     
16:     sleep(5000);
17:     
18:     new Thread(new Runnable() {
19:      public void run() {
20:       out.println(" " + getName() + " start");
21:       
22:       synchronized(auto_lock){out.println(" " + getName() + " y=" + y);}
23:       
24:       out.println(" " + getName() + " x=" + x);
25:       
26:       out.println(" " + getName() + " end");
27:      }
28:     }).start();
29:     
30:     sleep(1000);
31:     
32:     out.println(getName() + " end");
33:    }
34:   }).start();
35:  }
36:  
37:  static public void sleep(int time) {
38:   try { Thread.sleep(time); } catch (InterruptedException e){out.println(e);}
39:  }
40:  
41:  static public String getName() {
42:   return Thread.currentThread().getName();
43:  }
44: }

「VolatileToSynchronizedTestクラス」にある[変数auto_lock]は、「VolatileToSynchronizedTestクラス」にある[変数y]の専用モニターと言い換えることができます。
「VolatileTestクラス」では、「VolatileToSynchronizedTestクラス」にある[変数auto_lock]が登場しませんが、「VolatileTestクラス」の[変数y]にアクセス(参照/更新)する際、自動的に、「VolatileToSynchronizedTestクラス」にあるような[変数auto_lock]が生成され、その自動生成されたモニターに対してSynchronaizeされているのです。


○volatileにおける『順序変更』について
synchronaizeと同。

○volatileにおける『アトミック性(操作)』について
volatileフィールド(変数)への単一操作については、『アトミック性(操作)』となります。
※本来、『アトミック操作』とは、
  いくつかの処理を組み合わせたもので、それらがひとつの処理となる。
 なので、正確には、『アトミック性(操作)』ということになりませんが…。

○volatileにおける『可視性』について
synchronaizeと同。

◎余談
今回まとめた内容は、あくまで私の想像です。しかし、このように解釈するといい感じになりました。

ご意見・ご指摘頂ければ大変ありがたいです。

※次は、
  A同じモニター上で、モニターのアンロックはロックよりもhappens-before(事前発生)である
  Bvolatileフィールドへの書き込みは、それに続く、同じvolatileの読み込みよりもhappens-before(事前発生)である
 のまとめから、当スレッドの本来の議題である
  非同期下におけるダブルチェックロッキングのアンチパターンについて
 について、再考慮しその結果をまとめたいと思います。
よろしくお願いします。
常連さん
会議室デビュー日: 2006/01/17
投稿数: 21
投稿日時: 2009-03-04 23:52
 以下のようなクラス(「AntiDclクラス」)は、使用する側がマルチスレッドだった場合、
 「getObjectメソッド」を使用し取得した「Objectインスタンス」は、
  中途半端な「Objectインスタンス」
 を取得することがある。

 -マルチスレッドでは正常に動作しないクラス-
 01: public class AntiDcl {
 02:  private static Object obj;
 03:
 04:  public static Object getObject() {
 05:   if (obj == null) {
 06:    synchronized(AntiDcl.class) {
 07:     if (obj == null) {
 08:      obj = new Object();
 09:     }
 10:     return obj;
 11:    }
 12:   }
 13:   return obj;
 14:  }
 15: }


〜中途半端な「Objectインスタンス」が取得されてしまう理由〜
上記例にある「AntiDclクラス」は、
 ・05行目→synchronizedではない
 ・06〜11行目→synchronizedである
となる。

1.スレッドAが、08行目まで進み、完全に生成処理が完了していないが
  「Objectクラス」インスタンスのアドレスを「obj変数」へとりあえず設定。
2.スレッドBは、05行目にあるif文にて、
   『nullではなくなった』
  と、判断し中途半端な「Objectインスタンス」を取得。

これは、先にも示したとおり、05行目がsynchronizedで無い為、起こってしまう。


〜volatile指定することで回避できる理由〜
『java5.0以降のJMM※5の場合、obj変数をvolatileとすることで解決する』理由は、以下の通り。

 01: public class AntiDclByVolatile {
 02:  private static volatile Object obj;
 03:
 04:  public static Object getObject() {
 05:   if (obj == null) {
 06:    synchronized(AntiDclByVolatile.class) {
 07:     if (obj == null) {
 08:      obj = new Object();
 09:     }
 10:     return obj;
 11:    }
 12:   }
 13:   return obj;
 14:  }
 15: }

上記、「AntiDclByVolatileクラス」と以下の「AntiDclBySynchronizedクラス」は等価である。
※概念的に等価ということで、お願いしますm(_ _)m

01: public class AntiDclBySynchronized {
02:  
03:  private static Object lock = new Object();
04:  private static Object obj;
05:  
06:  public static Object getObject() {
07:   if (chkNull()) {
08:    synchronized(AntiDclBySynchronized.class) {
09:     if (chkNull()) {
10:      synchronized(lock) {
11:       obj = new Object();
12:      }
13:     }
14:     return obj;
15:    }
16:   }
17:   return obj;
18:  }
19:  
20:  private static boolean chkNull() {
21:   synchronized(lock) {
22:    return (obj == null);
23:   }
24:  }
25: }

その為、
 『java5.0以降のJMM※5の場合、obj変数をvolatileとすることで解決する』
となる。


以上が、私なりに考え、まとめた結果です。

ご意見・ご指摘頂ければ大変ありがたいです。

◎余談
今回のことを調べていて、finalについてもvoratileと似たルールがあることがわかりました。
※あくまで、「似たルール」です。

それは、『初期化の安全性』といわれているルールです。

以下、http://www.javareading.com/bof/cookbook-J20060917.htmlより抜粋
--------------------------------------------------------------------
(コンストラクタ中での)finalフィールドへのストアと,finalフィールドが参照型の時にこのfinalが参照可能な全てのストアは,その後に続く(コンストラクタ外の), そのfinalフィールドを保持するオブジェクトの参照の,他のスレッドよりアクセス可能な変数へのストアと,順序変更してはならない.例えば,以下の例では順序変更は許されない.
  x.finalField = v; ... ; sharedRef = x;
例えば"..."が,コンストラクタの論理的な終端にまで及ぶコンストラクタのインライン化の際に,これが関係してくる.コンストラクタ中のfinalへのストアは,そのオブジェクトを他のスレッドから見える様にするかもしれないコンストラクタ外にあるストアの後に,移動してはならない.(以下で見られるように,これはバリアの発行が必要になるかもしれない.)同様に以下の例において,三つ目の割り当ては,前の二つのいずれかと順序変更することは許されない.
  v.afield = 1; x.finalField = v; ... ; sharedRef = x;

finalフィールドの初期化ロード(例えば,本当に最初にスレッドに遭遇した時)finalフィールドを保持するオブジェクトへの参照の初期化ロードと順序変更してはならない.これは次の例で意味を持つ.
  x = sharedRef; ... ; i = x.finalField;
これらには依存関係があるため,コンパイラはこれらを順序変更しようとはしないだろう.しかしある種のプロセッサではこのルールの結果,順序変更されるかもしれない.(?)
--------------------------------------------------------------------
『初期化の安全性』があるおかげで、
 完全に生成されたインスタンスへの参照であれば、マルチスレッド化においても、
 そのインスタンスへのfinalフィールドの可視性は保障される
とわかりました。


最後まで読んでくださった方、本当にありがとうございました。
これまでにまとめた内容には、間違いが沢山あるかと思います。
もしよければ、どなたかご指摘頂けたらありがたいです。
m(_ _)m
1

スキルアップ/キャリアアップ(JOB@IT)