1. 概要

簡単に言うと、ロックは、標準の同期ブロックよりも柔軟で洗練されたスレッド同期メカニズムです。

ロックインターフェースは、Java1.5から使用されています。 It’s defined inside the java.util.concurrent.lock package, and it provides extensive operations for locking.

In this tutorial, we’ll explore different implementations of the Lock interface and their applications.

2. ロックと同期ブロックの違い

There are a few differences between the use of synchronized block and using Lock APIs:

  • A synchronizedblock is fully contained within a method. We can have Lock APIs lock() and unlock() operation in separate methods.
  • A synchronized block doesn’t support the fairness. Any thread can acquire the lock once released, and no preference can be specified. We can achieve fairness within the Lock APIs by specifying the fairness property. It makes sure that the longest waiting thread is given access to the lock.
  • 同期されたブロックにアクセスできない場合、スレッドはブロックされます。 Lock APIは、tryLock()メソッドを提供します。 The thread acquires lock only if it’s available and not held by any other thread. This reduces blocking time of thread waiting for the lock.
  • A thread that is in “waiting” state to acquire the access to synchronized block can’t be interrupted. The Lock API provides a method lockInterruptibly() that can be used to interrupt the thread when it’s waiting for the lock.

3. ロックAPI

Lockインターフェースのメソッドを見てみましょう。

  • void lock() – Acquire the lock if it’s available. If the lock isn’t available, a thread gets blocked until the lock is released.
  • void lockInterruptibly() – This is similar to the lock(), but it allows the blocked thread to be interrupted and resume the execution through a thrown java.lang.InterruptedException.
  • boolean tryLock() – This is a nonblocking version of lock() method. It attempts to acquire the lock immediately, return true if locking succeeds.
  • boolean tryLock(long timeout, TimeUnit timeUnit) – This is similar to tryLock(), except it waits up the given timeout before giving up trying to acquire the Lock.
  • void unlock() unlocks the Lock instance.

A locked instance should always be unlocked to avoid deadlock condition.

ロックを使用するための推奨コードブロックには、 try /catchおよびfinallyブロックが含まれている必要があります。

Lock lock = ...; 
lock.lock();
try {
    // access to the shared resource
} finally {
    lock.unlock();
}

In addition to the Lock interface, we have a ReadWriteLock interface that maintains a pair of locks, one for read-only operations and one for the write operation. 書き込みがない限り、読み取りロックは複数のスレッドによって同時に保持される場合があります。

ReadWriteLock は、読み取りまたは書き込みロックを取得するメソッドを宣言します。

  • Lock readLock() returns the lock that’s used for reading.
  • Lock writeLock() returns the lock that’s used for writing.

4. ロックの実装

4.1. ReentrantLock

ReentrantLock クラスは、Lockインターフェースを実装します。 It offers the same concurrency and memory semantics as the implicit monitor lock accessed using synchronized methods and statements, with extended capabilities.

Let’s see how we can use ReentrantLock for synchronization:

public class SharedObject {
    //...
    ReentrantLock lock = new ReentrantLock();
    int counter = 0;

    public void perform() {
        lock.lock();
        try {
            // Critical section here
            count++;
        } finally {
            lock.unlock();
        }
    }
    //...
}

We need to make sure that we are wrapping the lock() and the unlock() calls in the try-finally block to avoid the deadlock situations.

tryLock()がどのように機能するかを見てみましょう。

public void performTryLock(){
    //...
    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
    
    if(isLockAcquired) {
        try {
            //Critical section here
        } finally {
            lock.unlock();
        }
    }
    //...
}

In this case, the thread calling tryLock() will wait for one second and will give up waiting if the lock isn’t available.

4.2. ReentrantReadWriteLock

ReentrantReadWriteLock クラスは、ReadWriteLockインターフェイスを実装します。

Let’s see the rules for acquiring the ReadLock or WriteLock by a thread:

  • Read Lock – If no thread acquired the write lock or requested for it, multiple threads can acquire the read lock.
  • Write Lock – If no threads are reading or writing, only one thread can acquire the write lock.

Let’s look at how to make use of the ReadWriteLock:

public class SynchronizedHashMapWithReadWriteLock {

    Map<String,String> 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();
        }
    }
    //...
}

For both write methods, we need to surround the critical section with the write lock — only one thread can get access to it:

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で導入されました。 It also supports both read and write locks.

ただし、ロック取得メソッドは、ロックを解放したり、ロックがまだ有効かどうかを確認したりするために使用されるスタンプを返します。

public class StampedLockDemo {
    Map<String,String> 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つの機能は、楽観的ロックです。 Most of the time, read operations don’t need to wait for write operation completion, and as a result of this, the full-fledged read lock isn’t required.

代わりに、読み取りロックにアップグレードできます。

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. Working With Conditions

Condition クラスは、クリティカルセクションの実行中にスレッドが何らかの条件の発生を待機する機能を提供します。

これは、スレッドがクリティカルセクションへのアクセスを取得したが、その操作を実行するために必要な条件がない場合に発生する可能性があります。 For example, a reader thread can get access to the lock of a shared queue that still doesn’t have any data to consume.

Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication.

Conditions have similar mechanisms, but we can also specify multiple conditions:

public class ReentrantLockWithCondition {

    Stack<String> 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. 結論

In this article, we saw different implementations of the Lock interface and the newly introduced StampedLock class.

We also explored how we can make use of the Condition class to work with multiple conditions.

The complete code for this article is available over on GitHub.