読者です 読者をやめる 読者になる 読者になる

プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

Javaでマルチスレッドするときの注意をまとめてみた

 数年ぶりとかいうレベルでJavaのスレッドに触ったもんだから色々忘れてたので、忘れてたことまとめておく。

ダブルチェックロッキングパターンって何でダメなんだっけ?

 Javaではダブルチェックロッキングパターンって場合によってはクリティカルにヤヴァい事があります。やばいってことは覚えてたんだけど、どういう理由で、どういう場面でやばいのか忘れてました。


private Object checkObj;
public void dcheck(){
  if( checkObj == null)
    synchronized{
      if(checkObj==null){
        Object o = new Object();
        checkObj = o;
      }
    }
 ………
}




 上のコードがクリティカルにヤヴァいパターンです。

 ダブルチェックロッキングパターンってのは主に、マルチスレッド環境でシングルトンなオブジェクトを作る際に、同期コストを抑える為に使われるパターンです。他言語では問題ないこともありますし、Javaでも使って問題ない場面もあります。


 Javaで上記コードが危ない原因は、checkObjのNullチェックの際に、コンストラクタが完了していない未完成なインスタンスを見てnullで無いと判断することがあり得るからです。この後でcheckObjを使って何かするとき、未完成なインスタンスを利用することでどのようなエラーが起こるか予想が付きません。
 一見すると、synchronizedブロック内部ではいったんローカル変数oに完成されたインスタンスが格納されてから、checkObjにコピーされている為、checkObjに未完成なインスタンスが入りようがないように見えます。では、何故問題になるかというと、JVMによるコードの最適化が原因です。Javaでは最適化処理の為、結果が変わらないような場面であれば、処理の順序を入れ替えても良いとなっています。つまり、実行段階において、ソースコードの額面通りの実行手順ではなく、局所変数oは要らないのですっ飛ばされて、checkObjにひとまず確保された領域のアドレスを格納→オブジェクトのコンストラクタを実行という手順になる可能性もあるのです。こういう手順に変更される理由は、キャッシュヒットとかと関係があるんだと思います。


 逆に言えば、実行手順が確定している、変更されようがない場面においてはダブルチェックロッキングパターンを利用する事に問題はありません。(変数がvolatileであるなど。)


ExecutorServiceを終了するの面倒くさい

 今時、自前でThreadを作成して、start()を呼び出すなんてナンセンスです。Executorsから各ExecutorServiceを作成するのが良いのですが、ExecutorServiceから作ったスレッドはシャットダウンを呼び出さない限り終了しません。したがって、シャットダウンを忘れると、プログラムがいつまでも終了しないという具合になってしまいます。

 それが面倒くさい場合は、ExecutorServiceのスレッドをDaemon化しましょう。

ExecutorService s = Executors.newSingleThreadScheduledExecutor(new ThreadFactory(){
    public Thread newThread(Runnable r){
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    }
});

 Javaはデーモンスレッド以外のスレッドが終了したとき、プログラムを終了します。このとき、デーモンが処理をしているかどうかは関係ありません。なので、処理が途中でぶった切られる可能性があります。それがまずい場合はやっぱり自前でシャットダウンする機構を作るしかないのですが。。。



現在のスレッドがExecutorのスレッドであるかどうかを調べる

 なぜこの機能をデフォルトで実装してくれていないのか、非常に疑問。ThreadGroupを自前で管理するのが一番楽だと思われる。

private ThreadGroup group = new ThreadGroup("group name");
private ExecutorService s = Executors.newSingleThreadScheduledExecutor(new ThreadFactory(){
    public Thread newThread(Runnable r){
        Thread t = new Thread(group,r);
        return t;
    }
});
public boolean isExecutorThread(){
  return group == Thread.currentThread().getThreadGroup();
//必要ならば return group.parentOf(Thread.currentThread().getThreadGroup());
}



スレッド固有の変数

 マルチスレッドとコストと安全を考えると、結局行き着くのが、イミュータブル化と(疑似)マルチプロセス化になるのではないかと思います。イミュータブルってのは、不変って事です。全ての変数がfinalで、一度インスタンスを作ったらインスタンスの状態を変更することが出来ないようなプログラムの書き方です。ぶっちゃけ、コストの面ではイミュータブルってどーなのよって思うけどね。
 で、マルチプロセスの様に各スレッドを扱うには他スレッドの変更とは無関係の、スレッド固有の変数が必要です。java.lang.ThreadLocalをつかうと、スレッド固有値を持つことが出来ます。

 ThreadLocalの事、綺麗さっぱり忘れて、最初HashMapで管理してた阿呆がここにおりますよ。



volatileかsynchronizedか

 volatileを選ぶべきか、synchronizedを選ぶべきかに悩むことがあります。

 私の選び方の基準は単純。排他ロックするべきか(synchronizedを選ぶ)、排他ロックはしなくても問題ないか(synchronizedを選ぶ)、排他ロックしたくないか(volatileを選ぶ)です。

 volatileとsynchronizedの同期かコストって、私が実測したわけでないので詳しくは知らないのですが、大してコストは変わらないんだそうです。というか、volatileが予想以上に重い。volatileな変数を更新した場合は、それ以前の書き込みも他スレッドから見えないといけないという制約があるのが原因だそうです。もちろん、個々のスレッドの処理が止まらないので、何度も処理がぶつかるような場面では一見してvolatileの方が軽く見えるんでしょうけど、止まらずに動く一つのスレッドで見れば、そんなに変わらないのかもね。
 というわけで、私は排他ロックしたくないという事情が無い限り、全部synchronizedを選んでいます。



synchronizedブロックを利用する

 synchronizedなメソッドを書くよりも、内部にsynchronizedブロックを持つメソッドを書いた方が色々と捗ると気がついた。

private final Object lockObj=new Object();
public void a(){
  synchronized(lockObj){
    ………

 理由は、ロックする範囲を限定できることと、ロックするオブジェクトを隠蔽できるから、wait,notifyAllあたりのメソッドの保護が出来ると言うこと。主に後者の目的で今回からなるべくsynchronizedブロックで書くようにした。まぁ、個人で書いてる限りは、普通に書いていればどっちでも何の問題も無いけどね。










 というわけで、いつも通り、落ちもなにもありませんが、以上。