woshidan's blog

あいとゆうきとITと、とっておきの話。

「Java並行処理プログラミング ―その「基盤」と「最新API」を究める」を読みました

去年の11月はブログを一切書けていなかったのですが、その間*1何をしていたかというと主に下記の本を読んでいました。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

読むのにかけた時間としては、だいたい11月の余暇をまるまる使って1回読んで、正月休みに缶詰になってもう一回読み進めた感じです。

この本の1章で触れられているのですが、この本が書かれた2006年ごろは「一昔前は並行処理が"前衛的な"話題だったけれど、普通のプログラマが(並行処理における)スレッドの安全性を意識しなければいけない」だったそうです。

不勉強なのであれですが個人的な肌感としては、クライアントサイドでは並行処理をそれとして意識しなくていいようなReactiveの考え方や、MVP, MVVMといったGUIシステムなど特定の条件下の作法に注目が集まっています。一方Railsなどのサーバサイドにおいては並行処理といわずにバッチの処理などもっと具体的な問題にフォーカスして対応していたり。。

そういう状況なので、一周回った2018年現在も自分が書いているコードを並行処理として考えることは、普通のプログラマにとって "前衛的"、あるいは"オタク的"なような気配があります。このため、1章のこの辺の話は分量として3行程度で短いのですが時代が変わった感じがあって興味深かったです*2

この本全体の内容としては、そういう特殊な話題となったかもしれない並行処理に対して普通のデベロッパが取り組むときに生じる、なんとなくロックを獲得したりしているけれど、これが正しいかどうやってチェックすればよいのだろう、といった疑問にコードレベルで答えるものになっています。一つ一つのトピックによくないコード、あまり良くないコード、良いコードの事例が並べられ、それら一つ一つに解説が加えてあるため、あまり詳しくない人でも丁寧に読んでけばそれ相応につかめていく感触があってよかったなと思いました。

基本的な部分は変わらないような気がしているものの、具体的なライブラリクラスの詳細やイディオム、パフォーマンスの兼ね合いなどはJava6.0くらいまでの話となっているので、せめてJava7, Java8くらいまでは適宜Javadocなどで確認した方がよいです。

それなりに胃もたれ感はありましたが、新しいことに少し手がとどきそうな感覚はワクワクですね。結城先生の下記の本を手にとって面白いな、と思った方は本書も楽しいと思います。

以下、簡単なメモを残して締めます。現場からは以上です。

  • 競り合い状態と複合アクション
    • 複数のスレッドから都合が悪いようなタイミングでアクセスされる可能性があり、悪い方の順序でのアクセスの結果、特定のステート変数の値がおかしくなるような状態を 競り合い状態 という
    • 競り合い状態が起きるような典型的な実装事例として、 check-then-act のような確認して、操作する、というような複数の操作がスレッドから見てアトミックにアクセスされる単位になっていないというのがある
      • 複合アクションには put-if-absent read-modify-write などがある。全部 check-then-act でいいのでは、といえなくもないですが、自分で一回書いてみると put-if-absent 見逃したりする...[感想]
    • 複合アクション全体をロックを使って一度に一つのスレッドしかアクセスできないようにする
  • ロックの粒度とデッドロックの話
    • 「このステート変数は~~でなければならない」といった制約を不変項といい、1つの不変項に関わるステート変数への操作を同一のロックでガードする
      • 別の不変項で制限されているステート変数(不変項Aでのみ制限されるステート変数aと不変項Bでのみ制限されるステート変数b)は別のオブジェクトでロックしてよい
    • 動的なロック順デッドロック
      • 同じクラスの2つのオブジェクトを1度に操作する場合などにやりがち。グローバルに固定順序でロックをとるように工夫する
        • ロックのネストに着目すると見つけやすい
        • 固定順序に関する工夫はメソッド内に押し込めて外部から隠してしまうとよい
      • メソッドをまたいだロックは発見しにくいが、すでにメソッド内でロックを取っている時、よそ者のメソッドを呼び出さないようにするとまだ発見しやすい
      • ロックを取らないで外部のメソッドを呼ぶことを オープンコール と呼ぶ
    • Lockインタフェースの tryLock を使って時間が切れたらロックを外すようにして確率的にロック解除という話も
  • Executorフレームワークの使い方
    • Producer-Consumer型の実装をやりたい時はExecutorFramework使うと便利
      • ExecutorFrameworkを使うと、タスクをどういう風にスレッド間へ分配するか、タスク自体がどうであるか、といったのを分離して考えやすい
    • タイムアウトしたFutureは、そのままだと元のスレッドへreturnするだけでFuture動いてるスレッドは止まっていないため、cancelしよう
    • ThreadPoolの用途など(第8章)
  • オブジェクトの可視性とメモリモデル
    • ロックをしないであるスレッドで処理していた内容が他のスレッドに見えないことがあるが、それはなぜか
      • たとえばスレッドAから int a = 0; int b = 0; int a = 2; b = 3; を実行した時、スレッドBで System.out.println("a = " + a + " b = " + b); したとき a = 0 b = 3 と出力されることがある理由
    • JVMの最適化のルールは、ロックが取られていない場合は、一つのスレッドの中でトータルの実行結果が意味的に変わらないのであれば実行順を入れ替えて良いようになっている
    • 変数を保存している領域は、スレッドごとがローカルにアクセスする領域と、他のスレッドと共有している領域がある
      • あるスレッドで宣言&初期化された変数が他のスレッドにもアクセスできるなる手順は スレッドごとのローカル領域に書き込み, 共有メモリに書き込み の順になる
      • 共有メモリに書き込み まで行われないと他のスレッドからは古い値が見える
    • ロックをすると、この実行順の最適化を制限することができ、ロックを後に獲得した他のスレッドから前のスレッドがしていた操作の内容が見えることが保証される
  • スレッドセーフなクラス設計と委譲
    • 1つの不変項にたずさわるステート変数がたくさんある場合はそれらを別のデータクラスとしてくくり出して管理すると扱いやすくなる
    • 並行性を求められるような部分は既存のJavaのライブラリクラスが使えないか検討する
      • 機能を拡張したい場合は継承より委譲を使うと良い
      • なぜなら、ステート変数にアクセスするためのロックオブジェクトがサブクラスに可視だとわからないから
      • (Javaのライブラリクラスはだいたいselfでロックしているのでクライアントサイドロックが可能だが、それは不安定な実装となる)
      • 大きな万能ライブラリやExecutorなどとまでいかずともLatch, CyclicBarrier, Semaphoreなど基本的なものを知っておくと特にテスト用モックさっくり書きたい時手軽(多分動作がわからないRxJavaなどのライブラリを調べる時にも)[感想]
  • テストについて
    • まずはシングルスレッドと同じ内容のテストをまずやる
      • 原因が並行性にないことをはっきりさせるため
    • ブロックする操作の試験
      • 逐次処理のテストにおける例外の試験に似たような立ち位置
      • 一定時間後にinterrupt()メソッドよんで、スレッドが終了するのをjoin()で待って、スレッドのステータス確認、みたいな構成 p.282
    • 資源管理のテストでは、不具合を誘発するためにスレッドの重なりを出すため、CyclicBarrierなどを使ってドライバに一工夫するとよい
    • コールバックなどテストに使えるAPIの利用も検討
      • RxJavaのテスト用クラスの利用などは多分この立ち位置からのアプローチでは[感想]
    • テスト自体が並行性を壊してしまう可能性がある、実行性能についての試験は静的/動的コンパイラの最適化などの要素もあるのでなるべくアプリケーションに近い環境で行う、など

*1:余暇で

*2:大規模分散データストアを扱っているような方にとっては本書の内容はおそらくホットであり、自分が本書を知ったのもデータマイグレーション三銃士という大規模データストアの移行に関する勉強会の関連資料を漁っている時