Beyond synchronized, the java.util.concurrent.locks package provides flexible locking and coordination utilities.

ReentrantLock

A mutual exclusion lock with more features than synchronized:

  private final ReentrantLock lock = new ReentrantLock();

public void updateCounter() {
    lock.lock();
    try {
        counter++;
    } finally {
        lock.unlock(); // always in finally
    }
}
  

Advantages over synchronized

  • tryLock() — attempt lock without blocking
  • lockInterruptibly() — respond to thread interruption
  • Fairnessnew ReentrantLock(true) for FIFO ordering
  • Multiple Condition objects per lock
  if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // critical section
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("Could not acquire lock");
}
  

ReadWriteLock

Allows multiple concurrent readers or one exclusive writer:

  private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, String> cache = new HashMap<>();

public String get(String key) {
    readLock.lock();
    try {
        return cache.get(key);
    } finally {
        readLock.unlock();
    }
}

public void put(String key, String value) {
    writeLock.lock();
    try {
        cache.put(key, value);
    } finally {
        writeLock.unlock();
    }
}
  

Ideal for read-heavy workloads like caches.

Semaphore

Controls access to a limited number of resources:

  Semaphore semaphore = new Semaphore(3); // max 3 concurrent accesses

public void accessResource() throws InterruptedException {
    semaphore.acquire();
    try {
        // use limited resource (e.g., database connection)
    } finally {
        semaphore.release();
    }
}
  

CountDownLatch

One or more threads wait until a set of operations completes:

  CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    executor.submit(() -> {
        try {
            doWork();
        } finally {
            latch.countDown();
        }
    });
}

latch.await(); // blocks until count reaches zero
System.out.println("All tasks finished");
  

CyclicBarrier

A set of threads wait for each other at a barrier point, then all proceed:

  CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All arrived"));

for (int i = 0; i < 3; i++) {
    executor.submit(() -> {
        doPhase1();
        barrier.await(); // wait for all threads
        doPhase2();
    });
}
  

Unlike CountDownLatch, a CyclicBarrier is reusable.

StampedLock (Java 8+)

Optimistic read lock for read-heavy scenarios:

  StampedLock sl = new StampedLock();
double x, y;

public double distanceFromOrigin() {
    long stamp = sl.tryOptimisticRead();
    double curX = x, curY = y;
    if (!sl.validate(stamp)) {
        stamp = sl.readLock();
        try {
            curX = x;
            curY = y;
        } finally {
            sl.unlockRead(stamp);
        }
    }
    return Math.sqrt(curX * curX + curY * curY);
}
  

When to Use What

Tool Use Case
synchronized Simple mutual exclusion
ReentrantLock Need tryLock, fairness, or conditions
ReadWriteLock Many readers, few writers
Semaphore Limit concurrent access to N resources
CountDownLatch Wait for N events to complete
CyclicBarrier Threads synchronize at a checkpoint

Best Practices

  • Always unlock in a finally block
  • Prefer higher-level utilities (ConcurrentHashMap) over manual locking when possible
  • Avoid holding locks during I/O operations
  • Use ReadWriteLock only when reads significantly outnumber writes — it has overhead