Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Mastering Java Concurrency with the J.U.C. Framework: Locks, AQS, and Synchronization Helpers

Tech 1

The java.util.concurrent package (often referred to as J.U.C.) standardizes16 advanced thread management and synchronization18 patterns beyond basic synchronized blocks. It supplies11 explicit locks, thread coordination aids, and a flexible synchronizer framework that serves22 as the backbone for many of its components.

The Lock Interface and ReentrantLock

Lock (from java.util.concurrent.locks) overcomes13 limitations of built-in monitors: it supports timed acquisition, interruptible waits, and multiple condition queues. The most common implementation is ReentrantLock, which allows8 a thread already holding the lock to acquire it again without blocking4 itself.

Basic usage follows a try-finally pattern to guarantee release:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SequenceGenerator {
    private final Lock mutex = new ReentrantLock();
    private long sequence = 0;

    public long nextId() {
        mutex.lock();
        try {
            sequence++;
            return sequence;
        } finally {
            mutex.unlock();
        }
    }
}

Interruptible and Polling Lock Attempts

  • lockInterruptibly() allows7 a waiting thread to be interrupted while queuing for the lock. If the current thread is already executing inside the critical section, interruption does not release the lock.
  • tryLock() immediately returns false if the lock is unavailable, enabling non‑blocking alternative actions.
Lock door = new ReentrantLock();

// Interruptible wait
public void enter() throws InterruptedException {
    door.lockInterruptibly();
    try {
        // critical region
    } finally {
        door.unlock();
    }
}

// Non‑blocking attempt
public boolean attemptEnter() {
    if (door.tryLock()) {
        try {
            // critical region
            return true;
        } finally {
            door.unlock();
        }
    }
    return false;
}

Condition Objects

Each Lock can create multiple Condition instences via newCondition(). They replace wait/notify and provide finer‑grained signaling. Methods await(), signal(), and signalAll() behave like their Object counterparts but are bound to a specific lock.

class BoundedBuffer<T> {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final T[] buffer;
    private int count, putIdx, takeIdx;

    public BoundedBuffer(int capacity) {
        buffer = (T[]) new Object[capacity];
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length)
                notFull.await();
            buffer[putIdx] = item;
            if (++putIdx == buffer.length) putIdx = 0;
            count++;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            T item = buffer[takeIdx];
            if (++takeIdx == buffer.length) takeIdx = 0;
            count--;
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock vs synchronized

Aspect synchronized ReentrantLock
Implementation JVM‑internal monitor Pure Java (JDK)
Performance Comparable in modern JVMs Comparable
Interruptible wait No Yes (lockInterruptibly)
Fairness Non‑fair only Configurable (fair/non‑fair)
Condition queues One implicit per object Multiple explicit Condition objects
Lock release Automatic (scope exit/exception) Manual (must call unlock in finally)

In general, prefer synchronized for simplicity unless you need advanced features. ReentrantLock’s explicit unlocking forces the developer to handle11 cleanup correctly, while the JVM guarantees monitor release.

ReadWriteLock

The ReadWriteLock interface separates read and write locks. It allows6 multiple concurrent readers but only one writer. This pattern boosts throughput when reads dominate. ReentrantReadWriteLock is the standard implementation.

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ConfigCache {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private Object cachedConfig;

    public Object readConfig() {
        rwLock.readLock().lock();
        try {
            return cachedConfig;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void updateConfig(Object newConfig) {
        rwLock.writeLock().lock();
        try {
            cachedConfig = newConfig;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

errRead and write7 operations are mutually exclusive. Only read‑read7 pairs are non‑blocking. Write requests have priority over1 readers in the default non‑fair mode to avoid writer starvation.

LockSupport

LockSupport provides primitive thread park/unpark capabilities. It is the low‑level building block for higher‑level synchronizers. Each thread carries a binary permit (initial 0).

  • park() consumes the permit if available; otherwise the thread blocks until a permit is granted or an interrupt occurs.
  • unpark(Thread) supplies a permit (if none already present). Consecutive unpark calls do not accumulate more than one.

Unlike wait/notify, LockSupport does not require the caller to be inside a monitor and is immune to missed signals (the order of park/unpark does not matter).

import java.util.concurrent.locks.LockSupport;

public class SimpleFuture<T> {
    private volatile T result;
    private final Thread waiter;

    public SimpleFuture(Thread waiter) {
        this.waiter = waiter;
    }

    public T get() {
        while (null == result)
            LockSupport.park();
        return result;
    }

    public void complete(T value) {
        this.result = value;
        LockSupport.unpark(waiter);
    }
}

AbstractQueuedSynchronizer (AQS)

AQS is the skeleton upon which many J.U.C. synchronizers are built (ReentrantLock, Semaphore, CountDownLatch, ReentrantReadWriteLock). It maintains:

  • An int state (re‑interpreted by each subclass) that represents ownership or permits.
  • A FIFO CLH variant queue to park waiting threads. Each queue node includes a thread reference and a wait status.

The framework uses compareAndSetState (CAS) to atomically update the state. When a thread fails to acquire the synchronizer, it is enqueued and then parked with LockSupport.park(). Up on release, the head node’s successor is unparked, and the cycle repeats.

###1 ReentrantLock Acquisition Flow (AQS perspective)

  1. lock() attempts a quick CAS of state from 0 to 1 (in non‑fair mode).
  2. On failure it calls acquire(1), which delegates to tryAcquire – overridden by fair/non‑fair implementations. tryAcquire checks state and, if the current thread is the owner, increments the hold count (reentrancy).
  3. If acquisition fails, the thread is wrapped into a Node and added to the tail of the queue via CAS (with a dummy head node if the queue was empty).
  4. The node then enters a guarded spin loop (acquireQueued). Only the head’s successor may attempt tryAcquire again. Otherwise the node checks whether it should park; after verifying the predecessor’s signal status, it calls LockSupport.park(this) and waits.
  5. When an unpark occurs, the thread resumes, clears the interrupt flag if needed, and retries.

A similar symmetrical path exists for release: state is decremented, and LockSupport.unpark wakes the next waiter.

Synchronizer Tools

CountDownLatch

A latch initialized with a count that must reach zero before blocked threads can proceed.alen One‑shot use; the count cannot be reset.

import java.util.concurrent.CountDownLatch;

public class RocketLaunch {
    public static void main(String[] args) throws InterruptedException {
        int systems = 5;
        CountDownLatch latch = new CountDownLatch(systems);

        for (int i = 0; i < systems; i++) {
            new Thread(() -> {
                System.out.println("System check complete");
                latch.countDown();
            }).start();
        }

        latch.await();
        System.out.println("All systems ready – lift‑off!");
    }
}

CyclicBarrier

A reusable barrier at which a fixed number of threads wait for each other. When the last thread arrives, all are released and an optional barrier action runs.

import java.util.concurrent.CyclicBarrier;

public class TeamAssembly {
    public static void main(String[] args) {
        int teamSize = 4;
        CyclicBarrier barrier = new CyclicBarrier(teamSize, 
            () -> System.out.println("Team assembled – begin session") );

        for (int i = 0; i < teamSize; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " arrived");
                    barrier.await();
                    // collaborate after barrier
                } catch (Exception e) {
                    Thread.currentThread().interrupt();
                }
            }, "Member-" + i).start();
        }
    }
}

Internally, CyclicBarrier1 relies on ReentrantLock and a condition to manage the count. After each cycle the barrier is reset, allowing reuse.

Semaphore

A semaphore maintains a set of virtual permits. acquire() blocks until a permit is available; release() returns a permit, potentially unblocking a waiting thread. It limits concurrent access to a resource.

import java.util.concurrent.Semaphore;

public class ConnectionPool {
    private static final int MAX_CONNECTIONS = 3;
    private final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);

    public void executeQuery() throws InterruptedException {
        semaphore.acquire();
        try {
            System.out.println("Query running – available permits: " 
                               + semaphore.availablePermits());
        } finally {
            semaphore.release();
        }
    }

    public static void main(String[] args) {
        ConnectionPool pool = new ConnectionPool();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    pool.executeQuery();
                } catch (InterruptedException ignored) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }
}

The1 underlying AQS state1 represents3 remaining permits.2 If acquire() reduces the state below zero, the calling thread is2 parked in the queue and unparked when a permit is returned.

These6 synchronizers, together with the1 lock framework, form the core of the Java concurrency toolkit, enabling scalable and correct multi‑threaded applications without starting from low‑level primitives.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.