ファイル » スレッドセーフなプログラムを書く(Java).md
スレッドセーフなプログラムを書く(Java)
まえがき
横浜で決済端末に搭載する SDK 開発をしていた際に、マルチスレッドを十分に制御できていないことに起因する複数のバグが見つかりました。
- 排他制御すべきところを、複数スレッドから読み書きしてしまうことで起きる状態不整合
- 不必要な排他制御によって起きるデッドロックによるフリーズ
これらの原因解析は難しいです。バグの発生がタイミング依存で、手順を再試行してもなかなか再現しなかったりします。横浜での業務を振り返ると、設計段階で正しい排他制御ができる(=スレッドセーフな)設計ができれば良かったなと思います。
synchronized 修飾子
Java 言語ではメソッドに synchronized 修飾子をつけることで、そのメソッドは複数スレッドからの同時アクセスはできなくなります。
ただし、どのようなメソッドでも synchronized を付ければいいわけではありません。
スレッドセーフなプログラムを書くにあたり、まずは、どのような処理をメソッド化して、それを synchronized にするかを考える必要があります。
スレッドセーフなプログラムのサンプル
まずは、同時アクセスから守りたいものを 1 つ決める必要があります。例として以下のようなものです。
- 特定のデータに対する読み書き
- 特定のデバイスやサーバに対するアクセス
特定のデータに対する読み書きを例にサンプルコードを書いてみます。
/**
* データ A のアクセスを排他制御する
*
* データ A について DB アクセス中(書き込み or 読み込み)は、他のスレッドからアクセスできない
* 現在処理中のアクセスが完了するまで、他のスレッドからのアクセスはブロックされる
*/
public class ADataStore {
/**
* 本クラスのインスタンスは常に 1 つ以下で無ければならない
* 複数のインスタンスを生成されると、それぞれのインスタンスから同時アクセス可能になるため
* この例では、Singleton パターンでインスタンスを常に 1 つ以下にする
*/
private static ADataStore mInstance = new ADataStore();
public static ADataStore getInstance() {
return mInstance;
}
public synchronized void setA(int a) {
// DB への書き込み処理
}
public synchronized int getA() {
// DB からの読み込み処理
}
}
上では、データ A に対する同時アクセスを制御することで、スレッドセーフを実現しました。
次に、このクラスを改造してデータ B も扱えるようにしたいと思います。
単に、上のメソッド setA(int)/getA() を量産するだけでもスレッドセーフは実現できますが、より本質的に排他制御を行なうならば、次のようなコードになります。
/**
* 複数データのアクセスを排他制御する
*
* データ A について DB アクセス中であっても
* データ B には DB アクセス可能
*/
public class MultiDataStore {
/**
* 本クラスのインスタンスは常に 1 つ以下で無ければならない
*/
private static MultiDataStore mInstance = new MultiDataStore();
/**
* データ A へのアクセスをロックするためのオブジェクト
*/
private final Object mALockObject = new Object();
/**
* データ B へのアクセスをロックするためのオブジェクト
*/
private final Object mBLockObject = new Object();
public static MultiDataStore getInstance() {
return mInstance;
}
public void setA(int a) {
synchronized (mALockObject) {
// DB への書き込み処理
}
}
public synchronized int getA() {
synchronized (mALockObject) {
// DB からの読み込み処理
}
}
public void setB(int b) {
synchronized (mBLockObject) {
// DB への書き込み処理
}
}
public synchronized int getB() {
synchronized (mBLockObject) {
// DB からの読み込み処理
}
}
}
このように書くと、もしデータ A の書き込み処理に時間がかかったとしても、データ B への読み書きが可能になります。先のサンプルのメソッドを量産するよりもパフォーマンスの向上が望めます。