Javaロックの例チュートリアルへようこそ。通常、マルチスレッド環境で作業する場合、スレッドセーフ性のために synchronized を使用します。
Javaロック
たいていの場合、synchronizedキーワードが使われますが、いくつかの短所があり、Java ConcurrencyパッケージにLock APIが追加されました。Java 1.5 Concurrency APIでは、
java.util.concurrent.locks
パッケージが登場し、Lock
インターフェースといくつかの実装クラスが提供され、オブジェクトのロックメカニズムが改善されました。Java Lock APIの重要なインターフェースとクラスには、以下のものがあります:
-
Lock: これはLock APIの基本的なインターフェースです。synchronizedキーワードのすべての機能を提供し、異なる条件を作成してロックを行ったり、スレッドがロックを待機するためのタイムアウトを提供する方法も追加されています。重要なメソッドには、ロックを取得するためのlock()、ロックを解放するためのunlock()、一定期間ロックを待機するためのtryLock()、Conditionを作成するためのnewCondition()などがあります。
-
条件: 条件オブジェクトは、オブジェクトのwait-notifyモデルと似ていますが、異なる待機セットを作成する機能が追加されています。条件オブジェクトは常にロックオブジェクトによって作成されます。重要なメソッドには、wait()に類似したawait()や、notify()やnotifyAll()メソッドに類似したsignal()やsignalAll()があります。
-
ReadWriteLock: 読み取り専用の操作用と書き込み用の関連するロックのペアを含んでいます。読み取りロックは、ライタースレッドが存在しない限り、複数のリーダースレッドによって同時に保持されることができます。書き込みロックは排他的です。
-
ReentrantLock: これはLockインターフェースの最も広く使用される実装クラスです。このクラスは、synchronizedキーワードと同様の方法でLockインターフェースを実装しています。Lockインターフェースの実装に加えて、ReentrantLockにはロックを保持しているスレッド、ロックを取得しようと待機しているスレッドなどを取得するためのユーティリティメソッドがいくつか含まれています。synchronizedブロックは再入可能な性質を持っており、つまり、スレッドがモニターオブジェクトのロックを保持していて、他のsynchronizedブロックが同じモニターオブジェクトのロックを必要とする場合、スレッドはそのコードブロックに入ることができます。おそらく、このクラスの名前がReentrantLockである理由です。この機能を簡単な例で理解しましょう。
public class Test{ public synchronized foo(){ //何かをする bar(); } public synchronized bar(){ //さらに何かをする } }
スレッドがfoo()に入ると、Testオブジェクトにロックがかかっているので、bar()メソッドを実行しようとすると、既にTestオブジェクトに対してロックを保持しているため、スレッドはbar()メソッドを実行することが許可されます。つまり、synchronized(this)と同じです。
Java Lockの例 – JavaのReentrantLock
では、synchronizedキーワードをJava Lock APIで置き換える簡単な例を見てみましょう。スレッドセーフな操作が必要なリソースクラスと、スレッドセーフでないメソッドがいくつかある場合を考えてみましょう。
package com.journaldev.threads.lock;
public class Resource {
public void doSomething(){
//何らかの操作、DBの読み書きなど
}
public void doLogging(){
//ログ出力、スレッドセーフは不要
}
}
今、Resource メソッドを使用する Runnable クラスがあるとしましょう。
package com.journaldev.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();
}
}
Resource オブジェクトのロックを取得するために、synchronized ブロックを使用していることに注意してください。クラス内にダミーオブジェクトを作成し、それをロック用途に使用することもできました。では、Java の Lock API を使用して上記のプログラムを synchronized キーワードを使用せずに書き直す方法を見てみましょう。Java では ReentrantLock を使用します。
package com.journaldev.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{
//ロックを解除する
lock.unlock();
}
resource.doLogging();
}
}
上記のコードからわかるように、tryLock() メソッドを使用してスレッドが特定の時間だけ待機し、オブジェクトのロックを取得できない場合はログを出力して終了します。また、try-finally ブロックを使用して、doSomething() メソッドの呼び出しで例外がスローされた場合でもロックが解放されることに注意してください。
Java Lock vs synchronized
上記の詳細とプログラムに基づいて、Java Lock と synchronized の間の以下の違いを簡単にまとめることができます。
- Java Lock API は、ロックに対するより多くの可視性とオプションを提供しています。これに対して、スレッドがロックを無期限に待機する可能性がある synchronized では、tryLock() を使用してスレッドが特定の時間だけ待機することができます。
- 同期コードは非常にクリーンでメンテナンスしやすいですが、Lock を使用すると、lock() と unlock() メソッドの呼び出しづけの間に例外がスローされても Lock が解放されるようにするために、try-finally ブロックが必要になります。
- 同期ブロックまたはメソッドは、1つのメソッドのみをカバーできますが、Lock APIを使用すると、1つのメソッドでロックを取得し、別のメソッドで解放することができます。
- synchronizedキーワードは公平性を提供しませんが、ReentrantLockオブジェクトを作成する際に公平性をtrueに設定することで、最も待機時間の長いスレッドが最初にロックを取得できるようにすることができます。
- Lockに対して異なる条件を作成し、異なるスレッドが異なる条件でawait()することができます。
これがJava Lockの例、JavaのReentrantLockおよびsynchronizedキーワードとの比較分析です。
Source:
https://www.digitalocean.com/community/tutorials/java-lock-example-reentrantlock