Javaでキーによるロックを取得する
1. 概要
この記事では、特定のキーをロックして、他のキーのアクションを妨げることなく、そのキーの同時アクションを防ぐ方法を説明します。
一般に、2つのメソッドを実装し、それらを操作する方法を理解する必要があります。
- void lock(String key)
- ボイドアンロック(文字列キー)
チュートリアルを簡単にするために、キーはStringsであると常に想定します。 equalsおよびhashCodeメソッドがHashMapキーとして使用されるため、これらが正しく定義されているという唯一の条件の下で、必要なタイプのオブジェクトに置き換えることができます。 。
2. 単純な相互に排他的なロック
まず、対応するキーがすでに使用されている場合、要求されたアクションをブロックしたいとします。 ここでは、想像していた lock メソッドではなく、 boolean tryLock(String key)メソッドを定義します。
具体的には、使用中のキーをいつでも入力できるセットのキーを維持することを目指しています。 したがって、キーに対して新しいアクションが要求されたときに、そのキーがすでに別のスレッドによって使用されていることがわかった場合は、それを拒否する必要があります。
ここで直面する問題は、Setのスレッドセーフな実装がないことです。 したがって、ConcurrentHashMapに裏打ちされたSetを使用します。 ConcurrentHashMap を使用すると、マルチスレッド環境でのデータの一貫性が保証されます。
これを実際に見てみましょう:
public class SimpleExclusiveLockByKey {
private static Set<String> usedKeys= ConcurrentHashMap.newKeySet();
public boolean tryLock(String key) {
return usedKeys.add(key);
}
public void unlock(String key) {
usedKeys.remove(key);
}
}
このクラスの使用方法は次のとおりです。
String key = "key";
SimpleExclusiveLockByKey lockByKey = new SimpleExclusiveLockByKey();
try {
lockByKey.tryLock(key);
// insert the code that needs to be executed only if the key lock is available
} finally { // CRUCIAL
lockByKey.unlock(key);
}
finallyブロックの存在を主張しましょう:その中でunlockメソッドを呼び出すことが重要です。このように、コードが
3. キーによるロックの取得と解放
ここで、問題をさらに掘り下げて、同じキーに対する同時アクションを単純に拒否するのではなく、キーに対する現在のアクションが終了するまで新しい着信アクションを待機させたいとしましょう。
アプリケーションフローは次のようになります。
- 最初のスレッドはキーのロックを要求します:それはキーのロックを取得します
- 2番目のスレッドは同じキーのロックを要求します:スレッド2は待機するように指示されます
- 最初のスレッドがキーのロックを解除します
- 2番目のスレッドはキーのロックを取得し、そのアクションを実行できます
3.1. スレッドカウンターでロックを定義する
この場合、Lockを使用するのが自然に聞こえます。 簡単に言うと、 Lock は、スレッドの同期に使用されるオブジェクトであり、取得できるようになるまでスレッドをブロックできます。 Lock はインターフェースであり、その基本実装であるReentrantLockを使用します。
Lockを内部クラスでラップすることから始めましょう。 このクラスは、キーのロックを現在待機しているスレッドの数を追跡できます。 スレッドカウンターをインクリメントするメソッドとデクリメントするメソッドの2つのメソッドを公開します。
private static class LockWrapper {
private final Lock lock = new ReentrantLock();
private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);
private LockWrapper addThreadInQueue() {
numberOfThreadsInQueue.incrementAndGet();
return this;
}
private int removeThreadFromQueue() {
return numberOfThreadsInQueue.decrementAndGet();
}
}
3.2. ロックにキューイングスレッドを処理させます
さらに、ConcurrentHashMapを引き続き使用します。 ただし、以前のように Map のキーを単純に抽出する代わりに、LockWrapperオブジェクトを値として使用します。
private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();
スレッドがキーのロックを取得したい場合、LockWrapperがこのキーにすでに存在するかどうかを確認する必要があります。
- そうでない場合は、カウンターを1に設定して、指定されたキーに対して新しいLockWrapperをインスタンス化します。
- その場合、既存の LockWrapper を返し、関連するカウンターをインクリメントします
これがどのように行われるか見てみましょう:
public void lock(String key) {
LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
lockWrapper.lock.lock();
}
HashMapのcomputeメソッドを使用しているため、コードは非常に簡潔です。 このメソッドの機能について詳しく説明しましょう。
- compute メソッドは、最初の引数としてkeyを使用してlocksに適用されます。初期値はのkeyに対応します。ロックが取得されます
- computeの2番目の引数として指定されたBiFunctionがキーと初期値に適用されます:結果は新しい値を与えます
- 新しい値は、ロックのキーキーの初期値を置き換えます
3.3. マップエントリのロックを解除し、オプションで削除する
さらに、スレッドがロックを解放すると、LockWrapperに関連付けられているスレッドの数が減ります。 カウントがゼロになったら、ConcurrentHashMapからキーを削除します。
public void unlock(String key) {
LockWrapper lockWrapper = locks.get(key);
lockWrapper.lock.unlock();
if (lockWrapper.removeThreadFromQueue() == 0) {
// NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
locks.remove(key, lockWrapper);
}
}
3.4. 概要
一言で言えば、私たちのクラス全体が最終的にどのように見えるかを見てみましょう:
public class LockByKey {
private static class LockWrapper {
private final Lock lock = new ReentrantLock();
private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);
private LockWrapper addThreadInQueue() {
numberOfThreadsInQueue.incrementAndGet();
return this;
}
private int removeThreadFromQueue() {
return numberOfThreadsInQueue.decrementAndGet();
}
}
private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();
public void lock(String key) {
LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
lockWrapper.lock.lock();
}
public void unlock(String key) {
LockWrapper lockWrapper = locks.get(key);
lockWrapper.lock.unlock();
if (lockWrapper.removeThreadFromQueue() == 0) {
// NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
locks.remove(key, lockWrapper);
}
}
}
使用法は以前と非常に似ています。
String key = "key";
LockByKey lockByKey = new LockByKey();
try {
lockByKey.lock(key);
// insert your code here
} finally { // CRUCIAL
lockByKey.unlock(key);
}
4. 同時に複数のアクションを許可する
最後になりましたが、別のケースを考えてみましょう。一度に1つのスレッドだけが特定のキーに対してアクションを実行できるようにするのではなく、同じキーに対して同時に動作できるスレッドの数を整数に制限します n。 簡単にするために、 n =2に設定します。
ユースケースを詳しく説明しましょう。
- 最初のスレッドはキーのロックを取得しようとしています:そうすることが許可されます
- 2番目のスレッドが同じロックを取得したい:それも許可されます
- 3番目のスレッドが同じキーのロックを要求します。最初の2つのスレッドの1つがロックを解除するまでキューに入れる必要があります
セマフォはこのために作られています。 セマフォは、リソースに同時にアクセスするスレッドの数を制限するために使用されるオブジェクトです。
グローバル機能とコードは、ロックを使用した場合と非常によく似ています。
public class SimultaneousEntriesLockByKey {
private static final int ALLOWED_THREADS = 2;
private static ConcurrentHashMap<String, Semaphore> semaphores = new ConcurrentHashMap<String, Semaphore>();
public void lock(String key) {
Semaphore semaphore = semaphores.compute(key, (k, v) -> v == null ? new Semaphore(ALLOWED_THREADS) : v);
semaphore.acquireUninterruptibly();
}
public void unlock(String key) {
Semaphore semaphore = semaphores.get(key);
semaphore.release();
if (semaphore.availablePermits() == ALLOWED_THREADS) {
semaphores.remove(key, semaphore);
}
}
}
使用法は同じです:
String key = "key";
SimultaneousEntriesLockByKey lockByKey = new SimultaneousEntriesLockByKey();
try {
lockByKey.lock(key);
// insert your code here
} finally { // CRUCIAL
lockByKey.unlock(key);
}
5. 結論
この記事では、キーにロックを設定して、同時アクションを完全に妨げるか、同時アクションの数を1つ(ロックを使用)またはそれ以上(セマフォを使用)に制限する方法を説明しました。
いつものように、コードはGitHubでから入手できます。