Reentrant Lock

3 minute read

ReentrantLock

Works same as synchronized keyword applied to an object

  • but requires explicit locking and unlocking
    • prone to errors if forget to unlock, or if there is an exception after locking and before unlocking

SOLUTION : always lock in the try block and unlock in the finally block

  • this pattern also helps when you want to unlock after return statement
Reentrancy

The same thread can acquire the read or write lock multiple times without causing a deadlock.

  • For example, if a thread already holds the write lock, it can acquire it again without blocking.
Fairness

By default, it uses a non-fair policy where thread scheduling is not guaranteed to follow any specific order.

  • However, a fair version can be used where threads are granted locks in the order they requested them, which can help prevent thread starvation.
Lock lock = new ReentrantLock();
public int task() {
    lock.lock();
    try {
      // Critical section
      return doTask();//returns an integer
    } finally {//Guaranteed to execute
        lock.unlock();//with return statements, this is the only way to unlock 
    }
}

With this extra complexity we have more control over lock & get more Lock operations

Queries

  • int getQueueLength() : Returns an estimate of the number of threads waiting to acquire the lock.
  • Thread getOwner() : Returns the thread currently holding the lock, or null if no thread holds the lock.
  • boolean isHeldByCurrentThread() : Returns true if the current thread holds the lock.
  • boolean isLocked() : Returns true if the lock is currently held by any thread.

By default, both synchronized keyword and ReentrantLock() does not provide a fairness guarantee.

  • But, ReentrantLock(true) can be used to enforce fairness
  • may affect the throughput as maintaining fairness comes with a cost.

LockInterruptibly

  • Useful with Watchdog for deadlock detection and recovery
  • Waking up threads to do cleanup
try{
    //lock.lock();
    lock.lockInterruptibly();
        ...
} catch(InterruptedException){
    if(Thread.currentThread().isInterrupted()){
        doCleanUp&Exit();
    }
}

lock() and tryLock()

  • never blocks
  • Attempts to acquire the lock immediately.
  • If the lock is available, it is acquired and the method returns true.
  • If the lock is not available, it returns false immediately **without blocking ** or waiting.

Use Cases

  • tryLock(): When immediate feedback is needed without waiting. eg Video/Image processing, Trading systems, UI Applications
  • tryLock(long time, TimeUnit unit): When waiting for a limited time is acceptable and you want to handle lock acquisition with a timeout.

Regular lock

lock.lock();//sleeps when the lock is not free
try{
   // Critical section
} finally() {
    lock.unlock();
}
Summary
ReentrantLock lock = new ReentrantLock();
public void update(int key, int value) {
  lock.lock();
  try {
    writeToDatabase(key, value); //slow
  } finally {
    lock.unlock();
  }
}

public int read(int key) {
  lock.lock();
  try {
    return readFromDatabase(key); //slow
  } finally {
    lock.unlock();
  }
}

for ReentrantLock lock = new ReentrantLock();

  • Only one thread can execute writeToDatabase(key, value) as The lock protects the critical section from concurrent access
  • Only one thread can execute readFromDatabase(key) since it’s guarded by a lock

ReentrantReadWriteLock

Synchronized and ReentrantLock do not allow multiple readers to access a shared resource concurrently

But when read operations are predominant or when read operations aren’t fast due to

  • read from many variables
  • read from complex data structure

Under these circumstances, mutual exclusion negatively impacts performance.

ReentrantReadWriteLock solves this problem.

ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();

Read Lock

Multiple threads can hold the read lock simultaneously as long as no thread holds the write lock.

  • This is useful for scenarios where multiple threads need to read data concurrently without modifying it.
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
public int read(int key) {
  readLock.lock();
  try {
    return readFromDatabase(key); //slow
  } finally {
    readLock.unlock();
  }
}

Write Lock:

Only one thread can hold the write lock at a time, and no other threads (either read or write) can acquire the lock.

  • This ensures exclusive access to the resource for modifications.
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock writeLock = lock.writeLock();
public void update(int key, int value) {
  writeLock.lock();
  try {
    writeToDatabase(key, value); //slow
  } finally {
    writeLock.unlock();
  }
}

Summary

ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();

public void update(int key, int value) {
  writeLock.lock();
  try {
     writeToDatabase(key, value); //slow
  } finally {
     writeLock.unlock();
  }
 }

public int read(int key) {
  readLock.lock();
  try {
     return readFromDatabase(key); //slow
  } finally {
     readLock.unlock();
  }
}

for ReadWriteLock lock = new ReentrantReadWriteLock();

  • writeToDatabase(key, value); method is guarded by a write lock, and only one thread can acquire a write lock at a time
  • readFromDatabase(key); is guarded by a read lock. Many threads can acquire that lock as long as no other thread is holding the write lock