Javaの並行処理を理解する(入門編)


例えば以下の点について、ちゃんと並行処理を考慮した実装ができていますか?

  • シングルトン
  • インクリメント(++)やデクリメント(--
  • longdoubleへの代入
  • <%! %>タグで宣言された変数

もし、何の考慮もせずにこれらのコードを実装していた場合、 本番システムでとんでもないバグを引き起こす可能性があります。

本稿では以下の流れに沿って並行処理を解説したいと思います。

  • 並行処理とは
  • 並行処理をなぜ考慮しなければならないのか
  • スレッドセーフな変数とそうでない変数
  • 並行処理はテストが難しい

「並行処理」とは

まずはじめにJavaがメモリ上でどのように動作しているかを理解しましょう。

「プロセス」と「スレッド」

  • プロセス : 各Javaアプリケーションに対してメモリ上に割り当てられた「メモリ空間」と呼ばれる領域で動作する処理単位
  • スレッド : プロセス上で一行ずつコードを実行する一連のプログラムの処理
  • マルチプロセス : 複数のプロセスが同時に動作すること
  • マルチスレッド : 複数のスレッドが同時に実行されること

メモリ空間にはプログラムを実行するための様々なデータが格納されています。プロセス間ではメモリ空間は共有されませんが、スレッド間ではメモリ空間が共有されます。つまり、 異なるスレッド間で同じデータへアクセスすることができます。

スレッド処理の種類

  • 逐次(Sequential) : 一つのプロセスで複数のスレッドを順番に処理すること
  • 並列(Parallel) : 複数のプロセスで各々のスレッドを同時に処理すること
  • 並行(Concurrent) : 一つのプロセスで複数のスレッドを切り替えながら動作することで、疑似的に複数のスレッドを同時に処理すること

これらの中で、並列処理と並行処理は複数のスレッドを同時に処理する「マルチスレッド」に該当します。一方、逐次処理はスレッドを一つずつ処理することから「シングルスレッド」と呼ばれます。

気をつけなければならないのは 並行処理 です。複数のスレッドは互いにメモリ空間を共有するため、スレッドが同時に実行された場合、片方のスレッドで読み書きしているデータをもう一方のスレッドが読み書きしてしまう恐れがあります。

IPA ISEC セキュア・プログラミング講座:C/C++言語編 第4章 不測の事態対策:レースコンディションの一般的対策

並行処理をなぜ考慮しなければならないのか

並行処理を考慮しなければならない理由を例題を通じて見ていきましょう。

例えば、以下のような図書館の貸し出し予約システムがあるとします。このとき、書籍「Java並行処理プログラミング」の現時点における予約人数は0人とします。

  1. 予約人数を取得する
  2. 取得した予約人数に1を加える
  3. 予約人数を処理2. の値で更新する

アリスとボブが同時に「Java並行処理プログラミング」を予約した場合、どうなるでしょうか。タイミングによっては、以下の順番で処理が実行されてしまうことがあります。

  • A1. アリスは予約人数を取得する(=0人)
  • A2. アリスは取得した予約人数に1を加える(=1人)
  • B1. ボブは予約人数を取得する(=0人)
  • B2. ボブは取得した予約人数に1を加える(=1人)
  • A3. アリスは予約人数を処理2. の値で更新する(=1人)
  • B3. ボブは予約人数を処理2. の値で更新する(=1人)

「誰よりも早く予約した」とお互いに信じているアリスとボブは、その翌日に図書館を訪れて喧嘩になってしまいました。

上記の例のように、同時にアクセスされることを想定していなかったデータに対して、実際には複数スレッドから並行でアクセスが行われてしまった場合に生じるバグのことを 「レースコンディション」 と呼びます。 またプログラムにおいてシングルスレッドで動作させる必要のある範囲を 「クリティカルセクション」 といいます。

一方で、複数のスレッドから同時に呼び出されても正常に動作するプログラムのことを 「スレッドセーフ」 であると表現します。

スレッドセーフな変数とそうでない変数

Javaには、スレッドセーフな変数とそうでない変数があります。これらの変数がメモリ上でどのように動作するかを解説したいと思います。

変数の種類

  • ローカル変数 : メソッド内に記述し、各メソッド、コンストラクタの状態を定義する変数。
  • インスタンス変数 : メソッドの外に記述し、他のメソッド、コンストラクタからも参照できる変数。同一クラス内の任意のメソッドから参照可能。
  • クラス変数 : インスタンス変数にstatic修飾子が付与された変数。同一クラスの複数のインスタンス間で値が共有される。
public class ClassSample{

    private String foo; //インスタンス変数
    private static String bar; //クラス変数

    public static void methodSample(){

        int num = 1; // ローカル変数

    }
}

これらの変数の中で スレッドセーフなのはローカル変数のみ です。

スレッドセーフな変数

ローカル変数はスレッド毎に固有である 「スタック領域」 と呼ばれるメモリ空間上の領域にデータが保持されるので、一つのスレッドからしかアクセスすることができません。そのため、他のスレッドが情報を書き換えたり、情報を取り違えて参照したりすることはありません。

スレッドセーフでない変数

インスタンス変数やクラス変数は複数のスレッドで共有される 「ヒープ領域」 と呼ばれるメモリ空間上の領域にデータが保持されるので、他のスレッドが情報を書き換えたり、情報を取り違えて参照したりすることがあります。

[実装編]スレッドセーフにすることを忘れてはいけない | 日経 xTECH(クロステック)

また変数の宣言やメソッド宣言時に使用する JSPの<%! %>タグにも注意が必要です。 <%! %>タグはJSPがコンパイルされると Servletの インスタンス変数 として展開されるため、スレッドセーフではありません。

並行処理はテストが難しい

レースコンディションはタイミングによって発生するバグであるため、 テストで検出することが非常に困難です。

Java並行処理の第一人者であるBrian Goetz氏(Javaアーキテクト, Oracle, 2018年3月現在)は並行処理に関する記事、『Javaの理論と実践: バグを確実につぶす』で以下のように言及しています。

当然のことですが、コードを書いている時がコードを高品質なものとする最善の時期と言えます。この時であれば何がどのように動作するのかを最も良く理解しているからです。

レースコンディションを埋め込まないためには、並行処理を理解したプログラマーによってプログラミングとレビューが行われるべき でしょう。

同記事では、並行処理のバグを検出する上で部分的ではあるが有効であるツールとして「FindBugs」を推奨しています。「FindBugs」の後継である「Spotbugs」にはレースコンディションを検出する項目がいくつか用意されています。(『SpotBugs マニュアル, 検知可能なバグの詳細』)

またFacebookは Java並行性バグの静的解析ツール「RacerD」 を2017年10月にオープンソースとして公開しました。以下の記事で紹介しているので是非ご覧ください。

Facebook製Java並行性バグ解析ツール「RacerD」を試してみた - Qiita

まとめ

  • 並行処理 」とは一つのプロセスで複数のスレッドを同時に処理することである。
  • レースコンディション 」とは、同時にアクセスされることを想定していなかったデータに対して、複数スレッドから並行でアクセスされた場合に生じるバグのことである。
  • 複数のスレッドから同時に呼び出されても正常に動作するプログラムのことを 「 スレッドセーフ 」 であると表現する。
  • ローカル変数は スレッドセーフである。
  • インスタンス変数、クラス変数、<%! %>タグは スレッドセーフではない。
  • レースコンディションはテストで検出することが難しい。
  • 並行処理を理解したプログラマーによってプログラミングとレビューが行われるべきである。
  • 部分的ではあるが有効である並行性バグの静的解析ツールとして「SpotBugs」、「RacerD」がある。

参考書籍

参考ウェブサイト