java.util.concurrent.Locksのガイド
1. 概要
簡単に言えば、ロックは、標準のsynchronizedブロックよりも柔軟で洗練されたスレッド同期メカニズムです。
Lockインターフェースは、Java1.5以降に存在しています。 これはjava.util.concurrent.lockパッケージ内で定義され、ロックのための広範な操作を提供します。
この記事では、Lockインターフェースとそのアプリケーションのさまざまな実装について説明します。
2. ロックと同期ブロックの違い
同期されたblockの使用とLockのAPIの使用にはいくつかの違いがあります。
-
A synchronized block is fully contained within a method –Lock APIのlock()およびunlock()操作を別々のメソッドで行うことができます
-
synchronized blockは公平性をサポートしていません。解放されると、どのスレッドもロックを取得できます。優先順位を指定することはできません。 We can achieve fairness within the Lock APIs by specifying the fairness property。 最長の待機スレッドにロックへのアクセス権が付与されるようにします
-
同期されたblockにアクセスできない場合、スレッドはブロックされます。 The Lock API provides tryLock() method. The thread acquires lock only if it’s available and not held by any other thread.これにより、ロックを待機しているスレッドのブロッキング時間が短縮されます
-
synchronized blockへのアクセスを取得するために「待機中」の状態にあるスレッドは、中断できません。 The Lock API provides a method lockInterruptibly() which can be used to interrupt the thread when it’s waiting for the lock
3. Lock API
Lockインターフェースのメソッドを見てみましょう。
-
*void lock()* –は、使用可能な場合はロックを取得します。ロックが利用できない場合、ロックが解除されるまでスレッドはブロックされます
-
void lockInterruptibly() –これはlock(),に似ていますが、ブロックされたスレッドを中断し、スローされたjava.lang.InterruptedExceptionを介して実行を再開できます。
-
boolean tryLock() –これはlock()メソッドの非ブロッキングバージョンです。すぐにロックを取得しようとし、ロックが成功した場合はtrueを返します
-
*boolean tryLock(long timeout, TimeUnit timeUnit)* –これはtryLock(),と似ていますが、Lockの取得をあきらめる前に指定されたタイムアウトを待機する点が異なります。
-
void unlock() –Lockインスタンスのロックを解除します
ロックされたインスタンスは、デッドロック状態を回避するために常にロック解除する必要があります。 ロックを使用するための推奨コードブロックには、try/catchおよびfinallyブロックが含まれている必要があります。
Lock lock = ...;
lock.lock();
try {
// access to the shared resource
} finally {
lock.unlock();
}
Lockインターフェース,に加えて、1つは読み取り専用操作用、もう1つは書き込み操作用のロックのペアを維持するReadWriteLockインターフェースがあります。 読み取りロックは、書き込みがない限り、複数のスレッドによって同時に保持されます。
ReadWriteLockは、読み取りまたは書き込みロックを取得するメソッドを宣言します。
-
*Lock readLock()* –は、読み取りに使用されるロックを返します
-
Lock writeLock() –書き込みに使用されるロックを返します
4. ロックの実装
4.1. ReentrantLock
ReentrantLockクラスは、Lockインターフェースを実装します。 synchronizedメソッドおよびステートメントを使用してアクセスされる暗黙的なモニターロックと同じ同時実行性およびメモリセマンティクスを提供し、拡張機能を備えています。
ReenrtantLock for同期を使用する方法を見てみましょう。
public class SharedObject {
//...
ReentrantLock lock = new ReentrantLock();
int counter = 0;
public void perform() {
lock.lock();
try {
// Critical section here
count++;
} finally {
lock.unlock();
}
}
//...
}
デッドロック状態を回避するために、lock()およびunlock()呼び出しをtry-finallyブロックでラップしていることを確認する必要があります。
tryLock()がどのように機能するかを見てみましょう。
public void performTryLock(){
//...
boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
if(isLockAcquired) {
try {
//Critical section here
} finally {
lock.unlock();
}
}
//...
}
この場合、tryLock(),を呼び出すスレッドは1秒間待機し、ロックが利用できない場合は待機をあきらめます。
4.2. ReentrantReadWriteLock
ReentrantReadWriteLockクラスは、ReadWriteLockインターフェースを実装します。
スレッドによってReadLockまたはWriteLockを取得するためのルールを見てみましょう。
-
Read Lock –書き込みロックを取得した、または要求したスレッドがない場合、複数のスレッドが読み取りロックを取得できます
-
Write Lock –読み取りまたは書き込みを行っているスレッドがない場合、書き込みロックを取得できるのは1つのスレッドのみです。
ReadWriteLockを使用する方法を見てみましょう。
public class SynchronizedHashMapWithReadWriteLock {
Map syncHashMap = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
// ...
Lock writeLock = lock.writeLock();
public void put(String key, String value) {
try {
writeLock.lock();
syncHashMap.put(key, value);
} finally {
writeLock.unlock();
}
}
...
public String remove(String key){
try {
writeLock.lock();
return syncHashMap.remove(key);
} finally {
writeLock.unlock();
}
}
//...
}
どちらの書き込み方法でも、クリティカルセクションを書き込みロックで囲む必要があります。1つのスレッドのみがアクセスできます。
Lock readLock = lock.readLock();
//...
public String get(String key){
try {
readLock.lock();
return syncHashMap.get(key);
} finally {
readLock.unlock();
}
}
public boolean containsKey(String key) {
try {
readLock.lock();
return syncHashMap.containsKey(key);
} finally {
readLock.unlock();
}
}
どちらの読み取り方法でも、クリティカルセクションを読み取りロックで囲む必要があります。 書き込み操作が進行中でない場合、複数のスレッドがこのセクションにアクセスできます。
4.3. StampedLock
StampedLockはJava8で導入されました。 また、読み取りロックと書き込みロックの両方をサポートしています。 ただし、ロック取得メソッドは、ロックを解除するため、またはロックがまだ有効かどうかを確認するために使用されるスタンプを返します。
public class StampedLockDemo {
Map map = new HashMap<>();
private StampedLock lock = new StampedLock();
public void put(String key, String value){
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public String get(String key) throws InterruptedException {
long stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
}
StampedLockによって提供されるもう1つの機能は、楽観的ロックです。 ほとんどの場合、読み取り操作は書き込み操作の完了を待つ必要がなく、その結果、本格的な読み取りロックは必要ありません。
代わりに、読み取りロックにアップグレードできます。
public String readWithOptimisticLock(String key) {
long stamp = lock.tryOptimisticRead();
String value = map.get(key);
if(!lock.validate(stamp)) {
stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlock(stamp);
}
}
return value;
}
5. Conditionsの操作
Conditionクラスは、クリティカルセクションの実行中にスレッドが何らかの条件の発生を待機する機能を提供します。
これは、スレッドがクリティカルセクションへのアクセスを取得したが、その操作を実行するために必要な条件がない場合に発生する可能性があります。 たとえば、リーダースレッドは、消費するデータがまだない共有キューのロックにアクセスできます。
従来、Javaはスレッド相互通信のためにwait(), notify() and notifyAll()メソッドを提供していました。 Conditionsにも同様のメカニズムがありますが、さらに、複数の条件を指定できます。
public class ReentrantLockWithCondition {
Stack stack = new Stack<>();
int CAPACITY = 5;
ReentrantLock lock = new ReentrantLock();
Condition stackEmptyCondition = lock.newCondition();
Condition stackFullCondition = lock.newCondition();
public void pushToStack(String item){
try {
lock.lock();
while(stack.size() == CAPACITY) {
stackFullCondition.await();
}
stack.push(item);
stackEmptyCondition.signalAll();
} finally {
lock.unlock();
}
}
public String popFromStack() {
try {
lock.lock();
while(stack.size() == 0) {
stackEmptyCondition.await();
}
return stack.pop();
} finally {
stackFullCondition.signalAll();
lock.unlock();
}
}
}
6. 結論
この記事では、Lockインターフェースと新しく導入されたStampedLockクラスのさまざまな実装を見てきました。 また、Conditionクラスを使用して複数の条件を処理する方法についても説明しました。
このチュートリアルの完全なコードは、over on GitHubで入手できます。