Javaのロックの例 – ReentrantLock
Java Lockの例のチュートリアルへようこそ。通常、マルチスレッド環境で作業する際には、スレッドの安全性のためにsynchronizedを使用します。
Javaのロック
ほとんどの場合、同期キーワードが最適な方法ですが、いくつかの欠点があり、Java ConcurrencyパッケージにLock APIを追加することにつながりました。Java 1.5 Concurrency APIでは、java.util.concurrent.locksパッケージにLockインターフェースといくつかの実装クラスが導入され、オブジェクトのロックメカニズムの改善が行われました。Java Lock APIのいくつかの重要なインターフェースとクラスは以下の通りです。
-
- ロック:これはロックAPIの基本インターフェースです。同期キーワードのすべての機能を提供し、異なるロックの条件を作成するための追加的な方法や、スレッドがロックを待つためのタイムアウトを提供します。重要なメソッドには、ロックを取得するためのlock()、ロックを解放するためのunlock()、一定期間ロックを待つためのtryLock()、条件を作成するためのnewCondition()などがあります。
条件:条件オブジェクトは、オブジェクトのwait-notifyモデルに似ており、さまざまな待機セットを作成するための追加機能があります。条件オブジェクトは常にロックオブジェクトによって作成されます。重要なメソッドには、wait()に類似したawait()、notify()およびnotifyAll()メソッドに類似したsignal()、signalAll()などがあります。
読み書きロック:読み取り専用操作用のロックと書き込み用のロックのペアが含まれています。読み取りロックは、ライタースレッドが存在しない限り、複数のリーダースレッドによって同時に保持されることがあります。書き込みロックは排他的です。
再入可能ロック:これはLockインターフェースの最も一般的に使用される実装クラスです。このクラスは、同期キーワードと同様の方法でLockインターフェースを実装しています。Lockインターフェースの実装に加えて、ReentrantLockにはロックを保持しているスレッド、ロックを獲得するために待機しているスレッドなどを取得するためのユーティリティメソッドも含まれています。同期ブロックは再入可能であり、つまりスレッドがモニターオブジェクト上でロックを保持している場合、別の同期ブロックが同じモニターオブジェクト上でロックを必要とする場合、スレッドはそのコードブロックに入ることができます。これがクラス名がReentrantLockである理由だと思います。この機能を簡単な例で理解しましょう。
public class Test {
public synchronized foo() {
//何かをする
bar();
}
public synchronized bar() {
//さらに何かをする
}
}
スレッドがfoo()に入ると、Testオブジェクトにロックがかかっているため、bar()メソッドを実行しようとすると、スレッドはTestオブジェクトに対して既にロックを保持しているため、bar()メソッドを実行することが許されます。これはsynchronized(this)と同じです。
Javaのロックの例 – JavaのReentrantLock
さて、synchronizedキーワードをJava Lock APIで置き換える簡単な例を見てみましょう。スレッドセーフである必要がある操作を持つResourceクラスがあるとしましょう。また、スレッドセーフでない必要があるメソッドもいくつかあります。
package com.scdev.threads.lock;
public class Resource {
public void doSomething(){
//do some operation, DB read, write etc
}
public void doLogging(){
//logging, no need for thread safety
}
}
さて、Resourceメソッドを使用するためのRunnableクラスがあるとしましょう。
package com.scdev.threads.lock;
public class SynchronizedLockExample implements Runnable{
private Resource resource;
public SynchronizedLockExample(Resource r){
this.resource = r;
}
@Override
public void run() {
synchronized (resource) {
resource.doSomething();
}
resource.doLogging();
}
}
リソースオブジェクトのロックを取得するために、同期化されたブロックを使用していることに注目してください。クラス内にダミーオブジェクトを作成し、それをロックの目的に使用することもできました。では、JavaのLock APIを使用して、上記のプログラムをsynchronizedキーワードを使用せずに書き換える方法を見てみましょう。Javaでは、ReentrantLockを使用します。
package com.scdev.threads.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrencyLockExample implements Runnable{
private Resource resource;
private Lock lock;
public ConcurrencyLockExample(Resource r){
this.resource = r;
this.lock = new ReentrantLock();
}
@Override
public void run() {
try {
if(lock.tryLock(10, TimeUnit.SECONDS)){
resource.doSomething();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//release lock
lock.unlock();
}
resource.doLogging();
}
}
もちろんですが、私はtryLock()メソッドを使用して、スレッドが一定の時間だけ待機することを確認し、オブジェクトのロックを取得できない場合は、ログに記録して終了します。また、重要な点として、doSomething()メソッドの呼び出しが例外をスローした場合でも、ロックが解放されるようにtry-finallyブロックを使用しています。
JavaのLockとsynchronizedの比較
上記の詳細とプログラムに基づいて、Javaのロックと同期の間には以下のような違いが明確に存在すると結論付けることが容易です。
-
- Java Lock API は、ロックの可視性とオプションが提供されており、synchronized とは異なり、スレッドがロックを永久に待機してしまう可能性がある場合、tryLock() を使用してスレッドが特定の時間のみ待機することができます。
-
- 同期化コードは非常にクリーンでメンテナンスしやすいですが、Lock を使用すると、lock() と unlock() のメソッド呼び出しの間に例外が発生した場合でも Lock が解放されるように try-finally ブロックを強制されます。
-
- 同期化ブロックやメソッドはただ1つのメソッドのみをカバーするのに対し、Lock API では1つのメソッドでロックを取得し、別のメソッドで解放することができます。
-
- synchronized キーワードは公平性を提供しませんが、ReentrantLock オブジェクトを作成する際に公平性を true に設定することで、最も待機時間の長いスレッドが最初にロックを取得することができます。
- Lock には異なる条件を作成することができ、異なるスレッドが異なる条件で await() することができます。
これでJavaのロックの例、JavaのReentrantLockとsynchronizedキーワードとの比較分析は終わりです。