質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

マルチスレッド

マルチスレッドは、どのように機能がコンピュータによって実行したのかを、(一般的にはスレッドとして参照される)実行の複合的な共同作用するストリームへ区分することが出来ます。

Q&A

解決済

2回答

4542閲覧

初期化子の挙動やスレッドセーフについて

ratetail

総合スコア32

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

マルチスレッド

マルチスレッドは、どのように機能がコンピュータによって実行したのかを、(一般的にはスレッドとして参照される)実行の複合的な共同作用するストリームへ区分することが出来ます。

0グッド

3クリップ

投稿2020/03/28 01:12

編集2020/03/28 23:46

前提・実現したいこと

クラス初期化子やインスタンス初期化子は、添付コードのように
・外部のメソッドを扱わない
・初期化子内でスレッドを扱わない
・外部のスレッドセーフでない共有オブジェクトの状態を更新して利用しない
上記条件の場合は

  • 1:変数の初期化時の初期化処理については同期処理など必要なく、スレッドセーフに動作するという認識で合っていますでしょうか?
  • 2:Javaの仕様上、インスタンス初期化ブロックの途中で、同一インスタンスのインスタンス初期化ブロックに割り込むことは可能なのでしょうか?

(クラス初期化子はスレッドセーフというのは見つかるのですが
インスタンス初期化子については、明示的にそのような記載がみつからなかったため)

list1、list2共に、その後の操作においては同期処理を、使用する側の責任で実装する必要があるということは理解しています。

スレッドセーフと判断した際の、初期化処理の想定したケース
  • 1:「AA01.list1に対しマルチスレッドアクセス」の場合

マルチスレッドでアクセスしても、クラス初期化子は最初の1度しか実行されないので
他から影響を受けることがない。

  • 2:「static AA01 ins = new AA01()のように静的インスタンスにマルチスレッドアクセス」の場合

ins.list2は当インスタンス生成時に1度のみ実行されるので、マルチスレッドでアクセスしても
インスタンス初期化子内に他から割り込むことがない。
静的インスタンスを大量に作成して、list2にマルチスレッドアクセスしても
それはAA01インスタンス個々にlist2ができるので、初期化処理に他から影響を受けることがない。

間違っているということでしたら、不整合を起こす場合のサンプルコード、または参考記事を付けていただけると幸いです。
よろしくお願いします。

該当のソースコード

Java

1public class AA01 { 2 public static List<String> list1 = new ArrayList<String>(); 3 static{ 4 for(int i = 0; i < 10; ++i){ 5 list1.add(String.valueOf(i)); 6 } 7 } 8 9 public List<String> list2 = new ArrayList<String>(); 10 { 11 for(int i = 0; i < 10; ++i){ 12 list2.add(String.valueOf(i)); 13 } 14 } 15 16 public AA01(){ 17 } 18}

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

xebme

2020/03/28 14:22

「初期化子内部で、外部のスレッドセーフでない共有オブジェクトの状態を更新して利用する」シナリオは対象外ですか?
ratetail

2020/03/28 23:26

Zuishinさん、xebmeさん、ご返答ありがとうございます。 Zuishinさん 初期化順序については、一通り調べたのですが 提示頂いたURL記載の「初期化の順序」についても https://www.jpcert.or.jp/java-rules/dcl00-j.html 上記URLの「クラス内循環」のように、書き方に依存する問題ではありますが 「クラスフィールドよりもコンストラクタを先に実行されることもあり、それによる意図せぬ動作がある」 というものなどがあったため 仕様の中にまだ知らない他の手段があるかもしれない、と確証が持てなかったため 質問をさせていただいた次第になります。 xebmeさん すみません、条件として意図する内容が、質問内容から漏れていました。 確かに質問に挙げた内容に準拠した条件で、そのシナリオは当てはまり、かつ不整合を起こせますね。 その条件も除外した場合、他に何かシナリオはあったりしますでしょうか? また、Javaの仕様上、インスタンス初期化ブロックの途中で、同一インスタンスのインスタンス初期化ブロックに割り込むことは可能なのでしょうか? 上記内容は質問文に追加修正させていただきます。
guest

回答2

0

MTセーフの話と、初期化がアトミックかの話が混在して話がこんがらがってますが。

まず、AA01自体はご自身が認識されているとおり、list1、list2へのアクセスがMTセーフでないため、
AA01もMTセーフでないクラスです。

その話とは別に端的に、

AA01.list1、AA01.list2ともに、アクセスできる状態であれば、初期化途中の半端な状態で
インスタンスが公開されることがあるか?

であれば。無いでいいと思います。

javaの仕様では

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.
「構築中のオブジェクトをロックするため、コンストラクターを同期する必要はありません。通常、オブジェクトのすべてのコンストラクターが作業を完了するまで、他のスレッドからは利用できません。」

となっていて他のスレッドからアクセスする場合は構築済みのインスタンスかnullになります。
よくあるLazyLoadの場合nullなので、nullの場合に、オブジェクト生成して設定したあとに返すような実装の場合、異なるインスタンスを返して想定外の挙動になる可能性があります。

TSM03-J. 初期化が完了していないオブジェクトを公開しない

の話がこれ。

  • AA01.list1

staticイニシャライザはインスタンス生成前に行われるのでnullということはありません。
またマルチスレッドだろうが、クラスロード時に行われ1度しか初期化されません

  • AA01.list2

同様に、当該インスタンスへアクセスできる時点では初期化が終わっています。

以上から、

AA01のインスタンスへアクセスできる状態であれば、list1、list2ともに初期化が終わっている状態
であり中途半端な割り込みは起こらない。

と思います。

1:「AA01.list1に対しマルチスレッドアクセス」の場合

マルチスレッドでアクセスしても、クラス初期化子は最初の1度しか実行されないので
他から影響を受けることがない。

上記で述べたように問題ない。

2:「static AA01 ins = new AA01()のように静的インスタンスにマルチスレッドアクセス」の場合

たぶん、説明不足かと思いますが、異なる複数のクラスにstatic AA01と宣言しても
AA01のインスタンは複数あるので、list2は関係ないよね?
という意味であれば、その通り問題ないです。

静的インスタンスを大量に作成して

異なるインスタンスの話であれば、そもそもMTセーフ関係ないです。

今回のケースのように単純な初期化であれば、たぶん問題にならないと思いますが
初期化の中で別のオブジェクト参照が起こるようなケースは同期の必要があるはずです。(後述の記事参照)

一方、コンストラクタでインスタンスを生成する場合は、nullになる可能性があります。
また、コンストラクタはスレッドセーフではありません。

投稿2020/04/01 03:12

編集2020/04/01 03:21
momon-ga

総合スコア4826

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

xebme

2020/04/01 03:30

JPCERTの記事とJava言語仕様が矛盾しているように思います。Java言語仕様が正しいのであれば、JPCERTの記事をどう読むべきでしょうか? 「コンストラクタはスレッドセーフではありません」は読みました。すでにこの質問の前提から除外されていると思います。
momon-ga

2020/04/01 03:37

lazyLoadをしてるから予期せぬ状態になるって記事だと思いますけど。 JPCERTの記事を、ちゃんと把握できていないかもですが・・・ どこを矛盾と感じました?
xebme

2020/04/01 04:04

こちらの読み間違いです。「コンストラクターはアトミックである(言語仕様の通り)」を読み間違えていて、コンストラクターの中間状態が見える、と誤解していました。正しくは「コンパイラーがステップを入れ替えたとしても、未初期化(0)か、初期化完了した状態のいずれかが公開される」です。(未初期化の場合、そのあとでロックが発生するのか?は課題です) 事前に調査した情報の中に、コンストラクターが長い初期化処理をする前に、thisを外部公開する例を読み、中間状態を見せられると思いこんでいました。コンストラクターがロックされるなら公開待ちとなるはずなので実験してみます。 以上です。わたしの回答を修正します。
xebme

2020/04/01 06:04

実験しました。「コンストラクターが初期化処理をする前に、thisを外部公開すると、中間状態を見せられる」は正しい。これをthisの「逸出(escape)」というそうです。 むしろ、言語仕様の「コンストラクターはアトミックである」の意味がわからなくなりました。
yohhoy

2020/04/01 07:11 編集

JLSは「コンストラクターはアトミックである」と強くは定義しておらず、「(this escapeのような特殊処理を明示的に行わない)通常ケースではsynchronized修飾しなくても問題ない(なのでコンストラクタではsynchronized修飾を禁止している)」と言ってるだけではないでしょうか。 > Unlike methods, a constructor cannot be abstract, static, final, native, strictfp, or synchronized: > * 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.
xebme

2020/04/01 08:28 編集

it would lock の would がすばらしい。了解しました。 アトミックであるべきはメモリーアロケートのnew(ステップ1)ですね。 「コンストラクターはアトミックである」の結論を書きます。 would はロックしているかのようなものと読める。これは、コンストラクターを実行中のスレッドがインスタンスのオブジェクトリファレンスを占有していて、他のスレッドに露出させないので、実質的にロックしているのと同じであるから。 この状況は、逸出(escape)によって、あるいは、JPCERTの記事にあるようにコンパイラのステップ変更によっても、破られる。
xebme

2020/04/01 15:34

momon-gaさん、yohhoyさん、参考になりました。補足説明(追記)にもツッコミ歓迎します。
guest

0

ベストアンサー

クラス初期化子

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の記事で説明されています。

バイトコードのステップごとの状態

バイトコードのステップごとにオブジェクトの状態を考えます。

  • ステップ1〜ステップ2

オブジェクトの領域をアロケート済み。変数にはすべてビット0が設定されている。参照値は取得済。

  • ステップ3

コンストラクターメソッド内でメンバ変数の初期値を設定。完了するまでに中間状態がある。

  • ステップ4

オブジェクトのメンバ変数は完全に設定済み。

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}

投稿2020/03/30 11:19

編集2020/04/01 15:18
xebme

総合スコア1090

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ratetail

2020/04/01 01:00 編集

メンバー変数にアクセスするためには、ステップ4完了後でないと不可であり 「コンストラクターの実行」はステップ3で行われるため 「コンストラクターの実行内部に含まれるインスタンス初期化子の実行」もステップ3であるため ステップ3で行われるものに対し、ステップ4以降で動作を行うものがアクセスは出来ない。 「一方のスレッドが(ステップ3で行われる)インスタンス初期化子実行中に  他方のスレッドが同一インスタンスに(ステップ4以降で行われる)アクセスすることは原理上不可能」 という理解であっていますでしょうか? 1点確認したいのですが「コンストラクターはスレッドセーフではない」とは どのような意味を指しているのでしょうか? 「コンストラクターはスレッドセーフ」と認識していたため。 「コンストラクターはスレッドセーフではない」は、例外として 今回上げている条件、提示していただいた条件などのようなものを 入れている場合なのかと思っておりました。 //サンプルで表示されるP157の最後の2行からP158にかけて https://books.google.co.jp/books?id=MAGhYlDuNrUC&pg=PA157&lpg=PA157#v=onepage&q&f=false https://thinkit.co.jp/cert/tech/15/7/3.htm
xebme

2020/04/01 02:12

>という理解であっていますでしょうか? まず、ステップ1から4までの処理を理解してくださり、ありがとうございます。(訂正:ステップ4でistore先を他のスレッドに公開するには前提違反の可能性あり。putfieldだけに限定します) 「原理上不可能」ではありません。リンク先のJPCERTの記事は、「コンパイラがステップ4と3を入れ替えてもよい」という知識を前提にしています。
xebme

2020/04/01 04:14 編集

>「コンストラクターはスレッドセーフではない」とはどのような意味を指しているのでしょうか? これは説明不足ですね。オブジェクトの初期化が完了するまでオブジェクトをロックしたいのですが、する手段がないという意味です。thisを公開すれば、初期化中にgetter/setterを呼べてしまう。そのため、初期化前、初期化中、初期化完了のいずれかの状態をさらすだけでなく、更新も許す。<-- この説明は、コンストラクターはロックされるという言語仕様と矛盾しています。調べ直したうえで正しい説明を追記します。 解決策がリンク先のJPCERTの記事に載っています。
xebme

2020/04/01 15:27

<-- この説明は、コンストラクターはロックされるという言語仕様と矛盾しています。調べ直したうえで正しい説明を追記します。 コンストラクターはロックされません。「アトミック」であると早とちりをしましたが、ロックできないが一般的なシナリオでは、コンストラクターが完了するまで、インスタンスが公開されないので実質ロックと同じである、と解釈し直しました。 当初の説明に戻ります。補足説明を追記しました。
momon-ga

2020/04/02 02:00

すごく詳細な解説、勉強になります。念のため確認させてください。 元の質問では、 > static AA01 ins = new AA01() のようにとあったのですが、説明されている”thisの「逸出(escape)」”とは異なるケースだと思っています。 私の理解は(ごめんなさい、変数名は元の質問にあわせて変えています) > Escaped ins = new Escaped(); > System.out.println("ins : i = " + ins.i + " s = '" + ins.s + "'"); ins.i、ins.sのようなアクセスをする際に不完全でことはあるか? という趣旨だと思っていました。 > 一般的なシナリオでは、コンストラクターが完了するまで、インスタンスが公開されない なので、ins.i、ins.sのようなアクセスをする際に不完全でことはあるか? としては無いと思ってよいでしょうか。
momon-ga

2020/04/02 02:04 編集

とても興味深いのですが、元の質問者さんの質問趣旨とことなる話題で混乱しないかなと思ったしだいです。 ちなみに、AA01Holderに対してthisを戻すような実装をすると問題になるということなら了解です。
xebme

2020/04/02 03:23

>> static AA01 ins = new AA01() > のようにとあったのですが、説明されている”thisの「逸出(escape)」”とは異なるケースだと思っています。 スタティック初期化子の動作なのでクラスがロックされインスタンスが生成されます。しかし逸出はコンストラクター内部の問題なので可能だろうと思います。
xebme

2020/04/02 03:31

>> 一般的なシナリオでは、コンストラクターが完了するまで、インスタンスが公開されない >なので、ins.i、ins.sのようなアクセスをする際に不完全でことはあるか? >としては無いと思ってよいでしょうか。 ないと思います。理由はシングルスレッドの実行なので、System.ou.println()の直前にステップ3が完了しているから(ステップ4-3、3-4どちらの場合でも)。問題が起きるためには、他のスレッドが登場することと、変数insがそのスレッドに見えること、の2つの条件が必要です。
xebme

2020/04/02 03:36

> とても興味深いのですが、元の質問者さんの質問趣旨とことなる話題で混乱しないかなと思ったしだいです。 > ちなみに、AA01Holderに対してthisを戻すような実装をすると問題になるということなら了解です。 アドバイスありがとうございます。質問趣旨から逸脱しているか?してるでしょうね。
ratetail

2020/04/03 01:03 編集

xebmeさん、momon-gaさん、yohhoyさん ご回答ありがとうございます。 まずはすみません 質問内容が「適切に局所的な区域に限定した話」に絞り込めない内容なために いろいろと想定されたようで、ご不便をおかけしました。 本題 thisというものがいましたね・・・ this参照逸出については以前目を通したことがあったのですが、思いっきり忘れておりました・・・。 xebmeさん >ステップ4が先なら、変数にすべてビット0が設定されている状態を他のスレッドに見せることができます。これは質問の反例になりませんか。 おっしゃるように、なりますね。 >this参照の逸出 こちらも、なりますね。そして質問でお聞きしたかった内容に沿った破壊でした。 「EscapedHolder」の「Thread t」内の「doneSignal.countDown」実行前の段階でいくらでも書き換え可能ですね。 丁寧に待ち合わせをして破壊してますね。 質問内容に合わせた表現にすれば 「Escaped」の「i」がListであれば、要素の追加削除によって、要素数が不定の状態が作り出せる、と。 試したところエラーも起こせて、エラーがないときでも要素数は不定でした。 テストコード概要 ・EscapedのiをList<Integer>に変更 ・Escapedのインスタンス初期化子内「i = 100」直後の位置で要素数を確認する事で  外部からアクセスを受けて変更されていることを確認 momon-gaさん いろいろ質問内容や追記でうまく説明できず、すみません。 yohhoyさん JLSの説明について補足していただきありがとうございます。 xebmeさん、momon-gaさん 質問内容の回答部分の参考になり、関連する部分はあると思っていますし 興味深い内容なので、そのあたりの話は追記なりコメントなりで 追加していただくのも良いと考えています。
xebme

2020/04/03 23:03

『Java 並行処理プログラミング』ブライアン・ゲーツ他著 ソフトバンククリエイティブ株 2006年 『逸出』の解説があります。スレッドがオブジェクトを占有する概念は『スレッド拘束』と呼ばれます。『メモリー整合性』も重要。マルチコア環境では、CPUキャッシュ上のデータをメモリにライトバックしなければなりませんが、synchronizedによって保証することが書かれています。
ratetail

2020/04/05 19:53

xebmeさん 参考書籍の紹介ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問