78.共有されるmutableなデータには、同期したアクセスをすべし
- 同期化の一つの役割は、排他制御であり、変数を矛盾した状態で読み取らせないようにすることにある。また、他スレッドが行った変更を見ることができるようにするのも同期化の役割である(後半いまいちわからん)。
- 排他制御だけでなく、スレッド間の信頼ある通信のためにも同期化は必要である。
- Thread#stop()は使ってはいけない。使うとデータが壊れてしまう。
- 以下のコードは一見、1秒で止まるように見えるが、実際には止まらない。バックグラウンドで動作するスレッドがいつstopRequestedを見に行くか、同期が保証されていないので、JVMは
while (!stopRequested)
i++;
を
if (!stopRequested)
while (true)
i++;
のように変換する。この最適化はhoistingと呼ばれる。(「liveness failure:プログラムがそれ以上先に進めなくなるエラー」の例)
package tryAny.effectiveJava;
import java.util.concurrent.TimeUnit;
public class StopThread {
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;
}
}
- 以下のように、stopRequested の読み込み、書き込みともに同期化してやれば期待通りに1秒で止まる。
package tryAny.effectiveJava;
import java.util.concurrent.TimeUnit;
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();
}
}
- 以下のように、stopRequestedにvolatile修飾子をつけてやれば、一番新しく書き込まれた値を読むことを保証できるので、期待通りに1秒で止まる。
package tryAny.effectiveJava;
import java.util.concurrent.TimeUnit;
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;
}
}
- ++のインクリメント処理はアトミックなものではないので、以下のコードが1ずつ上昇していくユニークな値を返す保証はない。解消する場合には、AtomicLongを使うべき。(「safety failure:プログラムが誤った結果を返すエラー」の例)
// safety failure
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}
- 本章で扱われている問題を避けるには、mutableなデータの処理は1つのスレッドに限定させておくようにする。