並行性
Effective Java 第 3 版の個人的メモ
- 項目 78 共有された可変データへのアクセスを同期する
- 項目 79 過剰な同期は避ける
- 項目 80 スレッドよりもエグゼキュータ、タスク、ストリームを選ぶ
- 項目 81 wait と notify よりも並行処理ユーティリティを選ぶ
- 項目 82 スレッド安全性を文書化する
- 項目 83 遅延初期化を注意して使う
- 項目 84 スレッドスケジューラに依存しない
1. 項目 78 共有された可変データへのアクセスを同期する
1.1. 結論
- synchronizedは同期化を行い、アトミック性と可視性を保証する
- 複数スレッドが可変データを共有する場合には、そのデータを読み書きするスレッドは同期を行わなければならない
- 同期を行わなければ、あるスレッドで行われた変更が他のスレッドから見えることが保証されないから
- volatileはアトミック性は保証しないが、可視性を保証する
- スレッド間通信だけが必要で、相互排他が必要なければ、volatileを使用できるが、正しく使うのは難しい
- 可変データの共有はなるべく回避すべき
1.2. アトミック性
- 1つのスレッドだけがある時点で1つのメソッドやブロックを実行する性質
- 下のようにsynchronizedを使うと、メソッドや処理をアトミックにできる
1
2
3
|
public synchronized void method() {
//アトミックな処理
}
|
1.3. 可視性
- あるスレッドから変数に書き込んだ値が、別スレッドから観測できる性質
- 下記のクラスのバックグラウンドスレッドは停止するか?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class StopThread1 {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThead = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
});
backgroundThead.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
|
- バックグラウンドスレッドは無限ループする可能性がある
- バックグラウンドスレッドからメインスレッドのbackgroundTheadに書き込んだ値が見えるかが保証されないから
- 下記のクラスのバックグラウンドスレッドは停止するか?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class StopThread2 {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThead = new Thread(() -> {
int i = 0;
while (!stopRequested()) {
i++;
}
});
backgroundThead.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
|
- バックグラウンドスレッドは停止することが保証される
- 読み込み操作と書き込み操作の両方が同期されていて、バックグラウンドスレッドからメインスレッドのbackgroundTheadに書き込んだ値が見えることが保証されるから
1.4. volatile
- volatileはアトミック性は保証しないが、可視性を保証する
- 先ほどのStopThread2クラスはvolatileを使うことで、下記のようにより簡単なコードにできる
- synchronizedを相互排他のためではなく、スレッド間通信のためだけにしようしているから
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class StopThread3 {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThead = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
});
backgroundThead.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
|
- volatileは「アトミックのように見えて実はアトミックではない」という操作に対して使わないようにする注意が必要
- volatileはアトミック性を保証しないから
- 例えば、下記のgenerateSerialNumber()を複数のスレッドから呼び出したら、違う値が帰ってくるか?
1
2
3
4
5
|
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
|
- 違う値が帰ってくることは保証されない
- インクリメントは「アトミックに見えるが実はアトミックでない操作」だから。インクリメントは、値の読み出しと古い値に1を加えた値を書き戻す2つの操作を行う。スレッドが古い値を読み出して新たな値を書き戻す間に、別のスレッドがフィールドを読み出すと、最初のスレッドと同じ値が見えて、同じ値を返してしまう
- nextSerialNumber + 1 はまずどこに書き込まれているか?
- スタック上の一時領域
- メモリは参照と書き込みだけしかできない。計算できない。
1.5. 可変データの共有の回避
- 今までの議論のように、可変データへの共有は難しいので、回避できるのであれば回避すべき
- 回避には2つの方法がある
- 不変データを共有する
- 可変データを共有しない(可変データは単一スレッドで使う)
- 上記の方針を採用した場合は文書化することが重要
- AtomicBooleanのようなライブラリを使うと良い。
2. 項目 79 過剰な同期は避ける
2.1. 結論
- 同期された領域内では、できる限り少ない処理をするべき
- デッドロックやデータ破壊、パフォーマンス低下の原因となるから
2.2. 過剰な同期によるエラーやデッドロック