Locks in Java

Naveen AnamaneniUpdated · 6 min read

The Lock interface in java.util.concurrent.locks is an alternative to synchronized that gives more control. You take and release the lock explicitly, you can try to get a lock without waiting forever, you can make a lock fair, and you can be interrupted while waiting. The package has three main lock types: ReentrantLock, ReentrantReadWriteLock (many readers or one writer) and StampedLock (with fast optimistic reads). In this lesson, you will learn all three and when each is the right choice.

Methods of Lock

MethodDescription
lock()It is used to take the lock, waiting as long as needed.
unlock()It releases the lock; must be called, usually in finally.
tryLock()It takes the lock only if it's free right now; returns true or false.
tryLock(time, unit)It waits at most the given time.
lockInterruptibly()It waits, but can be interrupted.
newCondition()It creates a Condition for waiting and signaling (see "Condition Interface in Java").

The Standard Pattern

Pattern
lock.lock();
try {
    // use the shared data
} finally {
    lock.unlock();
}

Unlike synchronized, nothing releases a Lock automatically: if an exception skips unlock(), every other thread waits forever. The finally block guarantees the release.

Example: ReentrantLock with tryLock

Let's see an example where two people try to use one printer; the second gives up instead of waiting.

Printer.java▶ Try it
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
 
public class Printer {
    private final ReentrantLock lock = new ReentrantLock();
 
    void print(String who) throws InterruptedException {
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                System.out.println(who + " is printing");
                Thread.sleep(300);
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(who + " gave up: printer busy");
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Printer printer = new Printer();
        Thread a = new Thread(() -> run(printer, "Navika"));
        Thread b = new Thread(() -> run(printer, "Srihitha"));
        a.start();
        Thread.sleep(50);
        b.start();
        a.join();
        b.join();
        System.out.println("Fair lock? " + printer.lock.isFair()
            + ", locked now? " + printer.lock.isLocked());
    }
 
    static void run(Printer p, String who) {
        try {
            p.print(who);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Output
Navika is printing
Srihitha gave up: printer busy
Fair lock? false, locked now? false

Explanation: Navika takes the lock and prints for 300 milliseconds. Srihitha tries 50 milliseconds later with tryLock(100 ms); the lock is still busy after 100 milliseconds, so the call returns false and Srihitha gives up instead of waiting. synchronized can't do this. new ReentrantLock(true) would create a fair lock that serves waiting threads in arrival order.

ReadWriteLock: Many Readers or One Writer

When data is read far more often than written, a ReentrantReadWriteLock lets many readers hold the read lock at once, while a writer gets the write lock alone:

PriceList.java▶ Try it
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class PriceList {
    private final Map<String, Integer> prices = new HashMap<>();
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
 
    Integer get(String item) {
        rw.readLock().lock();
        try {
            return prices.get(item);
        } finally {
            rw.readLock().unlock();
        }
    }
 
    void put(String item, int price) {
        rw.writeLock().lock();
        try {
            prices.put(item, price);
        } finally {
            rw.writeLock().unlock();
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        PriceList list = new PriceList();
        list.put("notebook", 60);
 
        long start = System.nanoTime();
        Thread[] readers = new Thread[4];
        for (int i = 0; i < readers.length; i++) {
            readers[i] = new Thread(() -> {
                list.rw.readLock().lock();
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    list.rw.readLock().unlock();
                }
            });
            readers[i].start();
        }
        for (Thread r : readers) r.join();
        long ms = (System.nanoTime() - start) / 1_000_000;
 
        System.out.println("notebook costs " + list.get("notebook"));
        System.out.println("4 readers shared the lock (under 400 ms)? "
            + (ms < 400));
    }
}
Output
notebook costs 60
4 readers shared the lock (under 400 ms)? true

Explanation: Four readers each held the read lock for 200 milliseconds, and the whole thing took a little over 200, because readers don't block each other. A writer would wait until all readers left, and readers would wait while a writer held the lock.

StampedLock

StampedLock (Java 8) adds optimistic reading: a reader takes a cheap "stamp" without locking, reads the data, then calls validate(stamp) to check that no write happened in between. If one did, it falls back to a real read lock. It's very fast for read-mostly data, but it isn't reentrant and is harder to use correctly.

Comparing the Locks

synchronizedReentrantLockReentrantReadWriteLockStampedLock
ReleaseAutomaticManual in finallyManualManual (by stamp)
Try with timeoutNoYesYesYes
Fairness optionNoYesYesNo
Many readers at onceNoNoYesYes (+ optimistic)
ReentrantYesYesYesNo
Conditionswait/notifynewCondition()Write lock onlyNo

Common Mistakes

Calling unlock() outside finally. An exception leaves the lock held forever.

Calling lock() inside the try. If lock() fails, finally calls unlock() on a lock you don't hold; call it just before try.

Trying to upgrade a read lock to a write lock in ReentrantReadWriteLock. It deadlocks; release the read lock first.

Interview Corner

What is the difference between synchronized and ReentrantLock?

Both provide reentrant mutual exclusion with the same memory-visibility guarantees. synchronized is built into the language and releases the lock automatically when the block exits. ReentrantLock requires an explicit unlock(), usually in finally, but adds tryLock (with or without a timeout), lockInterruptibly, an optional fairness policy, multiple Condition objects, and methods to query the lock's state. Use synchronized by default and ReentrantLock when those extra features are needed.

Practice

  1. Easy: Rewrite a synchronized counter with a ReentrantLock, using the lock-try-finally pattern.
  2. Harder: Write a transfer method between two accounts that uses tryLock with a timeout on both accounts' locks, retrying after a short random pause if it can't get both, so it never deadlocks.

Quick check: Locks

4 questions
1Where should unlock() be called?
2What does tryLock() return if the lock is busy?
3Which lock lets many readers hold it at once?
4Is StampedLock reentrant?

Key Takeaways

Was this tutorial helpful?

Lesson 418 of 482