こんにちは、まさです。
Webアプリケーションなんかで複数のユーザから同時アクセスされるようなケースで、初回のリクエスト時だけ初期化処理をかけたいとかいう際の排他制御方法について書きます。
言い換えると、マルチスレッド環境で初回スレッドのみスレッドセーフな処理を実装したいというケースです。
スレッドセーフとは
スレッドセーフとは、マルチスレッドで動作するアプリケーションにおいて、複数スレッドが同時並行的に実行しても安全に処理ができることを言います。
例えば、複数スレッドからアクセスされる共有データがあるとき、一度に一つのスレッドのみがアクセスできるようにしておかないと、意図しない値の変更がなされてしまい問題です。
これは安全ではないため、スレッドアンセーフとなります。
共有のデータにアクセスする場合は、排他制御をかけるなどしてデータ不整合を防ぎ安全にする必要があります。
初回スレッドのみ排他制御する実装
まず、単純にフラグチェックだけのだめな例です。
※外からdoSomething()が実行されると想定してください。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
ThreadTest {
private static boolean initialized; // 初期化フラグ
public static void doSomething() {
if(!initialized) {
// 初回スレッドのみ通したい初期化処理
~省略~
initialized = true;
}
}
}
|
この書き方は初期化フラグを用意してチェックしています。
初期化完了後にフラグを更新し、後続のスレッドはそれをチェックしているので良さそうに見えるかもしませんが、スレッドアンセーフです。
以下の点で問題です。
- if文に入ってからinitializedフラグを更新するまでの間に初期化処理が入るため、その間に他スレッドがif分の中に入り込む可能性があります。
基本的に、排他制御したいチェック処理と実行処理は一連の動作としてまとめてロックする必要があります。
つぎに、
ある程度Javaの排他制御方法について知っている方は、
synchronizedを使えばいいと思えばいいかもしれません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
SampleClass {
private static boolean initialized; // 初期化フラグ
synchronized public static void doSomething() {
if(!initialized) {
// 初回スレッドのみ通したい初期化処理
~省略~
initialized = true;
}
}
}
|
これは、メソッドにsynchronizedをかけた例です。
チェック処理と排他制御したい実行処理のブロックがsynchronizedされているため、1スレッドのみが安全に初期化処理できます。
スレッドセーフとしてはOKです。
ただ、こちらだと以下の問題があります。
・毎回他スレッドは排他制御で待たされるため遅い、マルチスレッドで捌けない。
要は性能問題です。
そもそも複数スレッドが同時アクセスされるから安全にしたいのに、それが捌けなくなってはもともこもないですね。
ということで上記の合わせ技として
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
SampleClass {
private static boolean initialized; // 初期化フラグ
synchronized private static void initExclusive() {
// 1スレッドずつしか入らない
// 初回同時アクセス時には、後続で他スレッドが入る可能性があるためチェックいれます。
if(!initialized) {
// 初回スレッドのみ通したい初期化処理
~省略~
initialized = true;
}
}
public static void doSomething() {
if(!initialized) {
initExclusive();
}
}
}
|
これで性能も考慮したスレッドセーフの実装ができるかと思います。
– initExclusive()はsynchronizedをかけているため1スレッドずつしか入りません。
– 同時実行でdoSomething()が呼ばれた場合、最初のinitializedチェックでは複数スレッドが通ってしまうかもしれませんが、initExclusive()で待たされるため2回目のチェックで安全に弾かれます。
※補足
・メソッド化してsynchronized修飾子を付与しましたが、synchronizedブロック {}でも同じです。
・ロック処理にはReentrantReadWriteLockを使用する方法もあります。
(読み込みロックと書き込みロックが分けて使えるので便利です。)
以上です。