Java Concurrency In Practice
Chapter2 Thread Safety
2.1 What is thread safety?
- thread-safeなコードとは、つまり、shared, mutable state へのアクセスを適切にマネージすること
- オブジェクトが thread-safe であることが必要とされるかどうかは、それが複数のスレッドからアクセスされるかどうかに依る
- a class is thread-safe when it continues to behave correctly when accessed from multiple threads
- stateless objects are thread-safe
2.2 Atomicity
Race Condition
- occurs when the correctness of a computation depneds on the relative timing or interleaning of multiple threads by the runtime
- in other words, when getting the right answer relies on lucky timing
Race Conditions in lazy initialization
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject isntance = null;
public ExpensiveObject getInstance() {
if (instance == null) {
instance = new ExpensiveObject();
}
return instance;
}
}
スレッドAがgetInstace
を呼び出した直後に、スレッドBがgetInstance
を呼び出すとどうなるか想像する。
- スレッドA:
instance
がnullであることを確認 - スレッドA:
ExpensiveObject
のインスタンスの生成を開始 - スレッドB:
instance
がnullであることを確認 - スレッドB:
ExpensiveObject
のインスタンスの生成を開始 - スレッドA:
ExpensiveObject
のインスタンスの生成が完了し、instance
変数へ代入 - スレッドA:
instance
をreturn - スレッドB:
ExpensiveObject
のインスタンスの生成が完了し、instance
変数へ代入 - スレッドB:
instance
をreturn
5でinstance
へ代入したあとに、再度7でinstance
へ異なるインスタンス変数を代入している。
これはLazyInitRace
の想定している使われ方と異なる。
スレッドAが返したinstance
変数を呼び元では、使いまわされるインスタンスとして扱う。
しかし、スレッドBが新たなインスタンスをinstance
変数へ代入してしまっている。
2.3 Locking
The definition of thread safety requires that invariants be preserbved regardless of timing or interleaving of operations in multiple threads.
複数の変数が不変条件に含まれている場合、それぞれの変数は独立して更新時に一連の処理(read-modify-write operations)を排他的にできればよいわけではない。
→same atomic operationとして複数の変数を更新しなければいけない。
Intrinsic locks
@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors);
else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
}
synchronizedブロックを利用することで、一連の処理をスレッドセーフにすることができる。
ただし、他スレッドの処理をロックする範囲が非常に大きく、パフォーマンス問題の懸念あり。
2.3.2 Reentrancy
よくわからん。。。
2.4 Guarding state with locks
2.5 Liveness and performance
先ほどのSynchronizedFactorizer
の並列処理を図で表したもの。
複数のリクエストを並列で同時処理するという本来の目的を達成できず、ユーザへのレスポンスタイムが長くなる。
→パフォーマンスと効果のバランスを考慮して、適切な長さの synchronized ブロックを利用することが大切
Future / CompletableFuture (java.util.concurrent)
Future: Java 5 で導入された
Completable Future: Java 8 で導入されて、Future の欠点を克服した拡張版らしい(どのような欠点?)
Executor / ExecutorService (java.util.concurrent)
ForkJoinPool (java.util.concurrent)
Reactive Streams (java.util.concurrent.Flow)
Project Loom (仮想スレッド) [Java 21以降]
Javaの標準ライブラリが提供している各非同期モジュールの簡単な違い
- 簡易な非同期処理:CompletableFuture
- タスクの並列実行:ExecutorService
- 大規模データの分割処理:ForkJoinPool
- リアクティブプログラミング:Flow (Reactive Streams)
- 非同期I/O:NIO
- 高並行処理:Virtual Thread (Project Loom)
非同期、並行、並列の違い
Chapter3 Sharing Objects
3.1 Visibility
複数スレッドが動作している状況では、一度変数に書き込んだ値に関して、のちに再度同じ変数から取得した場合に値が異なっている場面が考えられる。
以下はその例である。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
// 実行時に ready 変数が false にハードコードされて無限ループする可能性あり
while (!ready) {
Thread.yield();
}
// number に値が代入される前に処理が走り、0 が出力される可能性あり
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
Stale data
@ThreadSafe
public class SYnchronizedInteger {
@GuardedBy("this") private int value;
// setter のみならず getter も synchronized とするべき
// そうでないと、陳腐化した値を参照する可能性がある
public synchronized int get() { return value; }
public synchronized void set(int value) { this.value = value; }
}
上記の図で、M のロック間で更新した変数を参照する場合、必ず M のロック間で参照するようにする
そうでないと、stale data を参照してしまうリスクがある。
volatile variables
volatile variables を使うことができるのは、次の条件を満たす場合のみ。
- 変数に書き込む値が現在の値に依存しない or 書き込みは1つのスレッドからのみと保証できる
- 他の変数も含めて、Atomic に処理する必要がない
3.2 Publication and escape
いまいち何を言っているか理解できていない。。。
LISTING3.7 の例で、なぜthis
への参照が escape すると言っているのか
Safe construction practices
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e)
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
LISTING 3.8 Using a factory method to prevent the this
reference from escaping during construction.
3.3 Thread confinement(閉じ込め)
各スレッドに変数スコープを閉じ込めて利用することで、Race Conditionを防ぐ
- Stack Confinementの利用(スタック領域を利用)
- ThreadLocalの利用
3.4 Immutability
@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i, BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length)
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equal(i)) return null;
else return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
LISTING 3.12 Immutable holder for caching a number and its factors.
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromReq(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}
LISTING 3.13 Caching the last result using a volatile reference to an immutable holder object.
OneValueCache
は Immutable であり、LISTING 3.13 のcache
変数はvolatile修飾子で定義されているため、1度に1つのスレッドからしかアクセスされないことが保証されている。
そのため、LISTING 3.13 はスレッドセーフな実装となる。
3.5 Safe publication
public class Holder {
private int n;
public Holder(int n) { this.n = n; }
public void assertSanity() {
if (n != n) throw new AssertionError("This statement is false.");
}
}
LISTING 3.15. Class at risk of failure if not properly published.
Holderをインスタンス化しているスレッド以外のスレッドが、インスタンスの参照を保持し、assertSanity
メソッドを呼び出した場合にAssertionError
をスローする可能性がある。
十分な同期方式を採用できていない場合、陳腐化したデータを読み取るリスクがあり、これによりエラーが発生した場合に非常に調査が難しくなる。
public static Holder holder = new Holder(42);
変数を Publish するうえで、Static initializer を使う方法はもっとも簡単で安全な方法である。
JVMによるクラス初期化のタイミングで実行されて初期化されるため。
オブジェクトを安全にスレッド間で共有する
オブジェクトへの参照を取得するときには、そのオブジェクトに対してどのような操作が許容されているのか、利用前にロックを取得する必要はあるか、読み取り専用か否か、ドキュメントから確認しておく必要がある。
Thread Local (Chapter3.3 に関連あり)
// ThreadLocal の仕組み
// ThreadLocal は各スレッドが自分専用のローカルコピーを持つ
// 他のスレッドが同じ ThreadLocal オブジェクトにアクセスしても、異なる値が保存・参照されるため、スレッド間でのデータ共有が防がれる
// データ共有が防がれれば、変数またはクラスをスレッドセーフになるように設計する必要が無くなる(実装難易度が大幅に下がる)
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
public int getValue() {
return threadLocalValue.get();
}
public void setValue(int value) {
threadLocalValue.set(value);
}
public static void main(String[] args) {
final ThreadLocalExample example = new ThreadLocalExample();
// スレッドA
final Thread threadA = new Thread(() -> {
example.setValue(10);
System.out.println("Thread A Value: " + example.getValue());
});
// スレッドB
final Thread threadB = new Thread(() -> {
example.setValue(20);
System.out.println("Thread B Value: " + example.getValue());
});
// ThradLocal を使っていることにより、スレッド間で値が共有されないため、データ競合(Race Condition)が発生しない
threadA.start();
threadB.start();
}
}
ヒープ領域・スタック領域
Chapter4 Compsing Objects
この章では、より簡単にスレッドセーフなクラスを定義する方法、スレッドセーフに違反しないようにメンテしていく方法を解説する。
4.1 Designing a thread-safe class
スレッドセーフなクラス:不変条件(invariants)が、同時に複数スレッドからアクセスされた場合にも保持されることを保証すること
State-dependent operations
例として、空のキューから要素を削除することはできない。
つまりキューから要素を削除するには、事前条件が伴う。
a single-threaded program: 事前条件が満たされていない場合、失敗する以外に選択肢はない
a concurrent program: 事前条件が後にtrue
になる可能性があるので待機することが選択肢にある
4.2 Instance confinement
// スレッドセーフだが、他メソッドから Person セットへアクセスする度に、
// synchronized ブロックを定義する必要がある(でないとスレッドセーフにならない)
@ThreadSafe
public class PersonSet {
@GuardedBy("this")
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
LISTING 4.2 Using confinement to ensure thread safety
Java monitor pattern
public class PrivateLock {
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod() {
synchronized(myLock) {
// Access or modify the state of widget
}
}
}
LISTING 4.3. Guarding state with a private lock
ロックオブジェクトを private にすることで、利用者側はそのロックオブジェクトによるロックを取得することができない(public なオブジェクトは意図的にもそうでない場合にも利用者側がロック取得可)
4.3 Delegating thread safety
4.4 Adding functionality to existing thread-safe classes
@NotThreadSafe
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent) list.add(x);
return absent;
}
}
LISTING 4.14. Non-thread-safe attempt to implement put-if-absent
上記の例では、list
変数が public 修飾子で定義されており、個別に操作が可能。
list
のロックとthis
のロックは異なるので、ロックが分散して操作競合が発生しうる。
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
public ImprovedList(List<T> list) { this.list = list; }
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains) list.add(x);
return !contains;
}
public synchronized void clear() { list.clear(); }
// ... similarly delegate other List methods
}
LISTING4.16. Implementing put-if-absent using composition.
4.5 Documenting synchronization policies
Chapter5 Building Blocks
5.1 Synchronized collections
Problems with synchronized collections
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
LISTING 5.1. Compound actions on Vector that may produce confusing results.
public static Object getLast(Vector list) {
synchronized(list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list) {
synchronized(list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
LISTING 5.2. Compound actions on Vector
using client-side locking.
for (int i = 0; i < vector.size(); i++) doSomething(vector.get(i));
LISTING 5.3. Iteration that may throw ArrayIndexOutOfBoundsException
.
synchronized(vector) {
for (int i = 0; i < vector.size(); i++) doSomething(vector.get(i));
}
LISTING 5.4. Iteration with client-side locking.
非同期に複数スレッドが多く当該処理を実行する場合、大幅な処理時間増加を引き起こす可能性あり。
Iterators and ConcurrentModificationException
List<Widget> widgetList = Collections.synchronizedList(new ArrayList<Widget>());
...
// May throw ConcurrentModificationException
for (Widget w : widgetList) doSomething(w);
LISTING 5.5. Iterating a List with an Iterator.
解決策として、widgetList
をロック対象とすることが考えられるが、以下のようなデメリットがある。
- リストのサイズが大きい もしくは
doSomething
が負荷の高い処理 の場合に長時間ロックがかかる - ロックをかけた状態で
doSomething
を呼び出すため、デッドロックのリスクが高まる - スループットやCPU利用率に対しての懸念が増す
代替案としては、
- リストのDeepCopyをイテレーション時に利用する(正しくパフォコストは意識すべし)