ReentrantLockとは
Javaでは、複数スレッドが共有リソースに安全にアクセスできるようにするために、いくつかの同期メカニズムが提供されています。その中でも、ReentrantLockは柔軟で強力なロック機構として知られています。本記事では、ReentrantLockの基本的な使い方、synchronizedとの比較、条件変数の利用方法、およびフェアロックとノンフェアロックの違いについて詳しく説明します。
1. 基本的な使い方
ReentrantLockはリエントラント(再入可能)なロックであり、一度取得したロックを同じスレッド内で再取得できる仕組みを持っています。以下に基本的な使用例を示します。
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = counter::increment;
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("カウント結果: " + counter.getCount());
}
}
この例では、lock.lock()でロックを取得し、処理が終了したらunlock()でロックを解放しています。例外が発生しても確実にロックを解放するために、try-finallyを使用しています。
2. synchronizedとの比較
ReentrantLockとsynchronizedはどちらもスレッド同期に利用されますが、以下のような違いがあります。
- 機能の豊富さ:
ReentrantLockは、synchronizedよりも豊富な機能を提供します。たとえば、ロックの取得にタイムアウトを設定したり、割り込みを可能にする機能があります。 - パフォーマンス:高競合状況において、
ReentrantLockは一般的にsynchronizedよりもパフォーマンスが優れています。 - 柔軟性:ロックの取得や解放をより柔軟に制御できます。
タイムアウトと割り込み可能なロックの例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class AdvancedLock {
private final ReentrantLock lock = new ReentrantLock();
public void tryLockWithTimeout() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println("ロック取得成功");
} finally {
lock.unlock();
}
} else {
System.out.println("ロック取得失敗");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void lockInterruptiblyExample() throws InterruptedException {
lock.lockInterruptibly();
try {
System.out.println("割り込み可能なロック取得");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
AdvancedLock example = new AdvancedLock();
Thread t1 = new Thread(example::tryLockWithTimeout);
Thread t2 = new Thread(() -> {
try {
example.lockInterruptiblyExample();
} catch (InterruptedException e) {
System.out.println("ロック割り込み");
}
});
t1.start();
t2.start();
}
}
3. Conditionによる複雑な同期処理
ReentrantLockには、Conditionインターフェースを介して、より柔軟なスレッド間通信を提供する機能があります。これは、Object.wait()やnotify()と似ていますが、より複雑な同期制御が可能です。
プロデューサー・コンシューマーの例
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == CAPACITY) {
notFull.await();
}
queue.add(value);
System.out.println("生成: " + value);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.poll();
System.out.println("消費: " + value);
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Runnable producer = () -> {
for (int i = 0; i < 10; i++) {
try {
example.produce(i);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
Runnable consumer = () -> {
for (int i = 0; i < 10; i++) {
try {
example.consume();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
Thread p = new Thread(producer);
Thread c = new Thread(consumer);
p.start();
c.start();
}
}
この例では、notFullとnotEmptyという2つのConditionを使用して、キューが満杯または空のときに適切に待機と通知を行っています。
4. フェアロックとノンフェアロック
ReentrantLockは、コンストラクタにtrueを渡すことでフェアロック(公平性のあるロック)を実装できます。フェアロックでは、ロック取得の順序がリクエスト順に保証されます。
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final ReentrantLock lock = new ReentrantLock(true);
public void accessResource() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " がロックを取得");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
FairLockExample example = new FairLockExample();
Runnable task = example::accessResource;
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
}