Backend Development 15 min read

Implementing a Custom Java AQS Lock from Scratch

This tutorial walks through building a simple Java lock using AbstractQueuedSynchronizer (AQS), starting with a basic MyLock class, adding thread parking with Unsafe, creating a FIFO thread container, and refining lock and unlock methods to achieve correct synchronization and avoid lock starvation.

Hujiang Technology
Hujiang Technology
Hujiang Technology
Implementing a Custom Java AQS Lock from Scratch

AbstractQueuedSynchronizer (AQS) is the core of Java's concurrency utilities. The article guides readers to implement a minimal lock by first defining a MyLock class with empty lock() and unlock() methods, then writing a test class that spawns multiple threads to exercise the lock.

public class MyLock {
    public void lock() {}
    public void unlock() {}
}

Running the test shows interleaved thread output, indicating the lock does not work. To fix this, the tutorial introduces Unsafe methods such as compareAndSwapInt and park / unpark for atomic state changes and thread parking.

public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int val);
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);

A static helper class UNSAFE obtains the singleton Unsafe instance via reflection and caches field offsets for later use.

public class UNSAFE {
    public static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (Exception e) { e.printStackTrace(); }
    }
}

Because a simple ConcurrentHashMap does not preserve FIFO order, a custom linked‑list container MyThreadContainer is created. It holds a volatile head and tail of Node objects, each storing a thread reference and links to previous/next nodes.

public class MyThreadContainer {
    volatile Node head;
    volatile Node tail;
    private static long headOffset = 0L;
    private static long tailOffset = 0L;
    static {
        try {
            headOffset = UNSAFE.unsafe.objectFieldOffset(MyThreadContainer.class.getDeclaredField("head"));
            tailOffset = UNSAFE.unsafe.objectFieldOffset(MyThreadContainer.class.getDeclaredField("tail"));
        } catch (NoSuchFieldException e) { e.printStackTrace(); }
    }
    private boolean compareAndSetHead(Node node) {
        return UNSAFE.unsafe.compareAndSwapObject(this, headOffset, null, node);
    }
    private boolean compareAndSetTail(Node expect, Node node) {
        return UNSAFE.unsafe.compareAndSwapObject(this, tailOffset, expect, node);
    }
    class Node {
        volatile Thread thread;
        volatile Node prev;
        volatile Node next;
        Node(Thread t) { this.thread = t; }
    }
    public Node addWaiter() {
        Node node = new Node(Thread.currentThread());
        while (true) {
            Node pred = tail;
            if (pred != null) {
                if (compareAndSetTail(pred, node)) {
                    node.prev = pred;
                    pred.next = node;
                    return node;
                }
            } else {
                if (compareAndSetHead(node)) {
                    tail = node;
                    return node;
                }
            }
        }
    }
}

The MyLock.lock() method now attempts an atomic state transition; if it fails, it enqueues the current thread via threadContainer.addWaiter() and parks the thread.

public void lock() {
    while (true) {
        if (UNSAFE.unsafe.compareAndSwapInt(this, stateOffset, 0, 1)) {
            System.out.println("Thread " + Thread.currentThread().getName() + ", acquired lock");
            break;
        } else {
            threadContainer.addWaiter();
            UNSAFE.unsafe.park(false, 0L);
        }
    }
}

The unlock() method resets the state and unparks the first waiting thread, also fixing the linked list pointers to maintain FIFO order.

public void unlock() {
    UNSAFE.unsafe.compareAndSwapInt(this, stateOffset, 1, 0);
    if (head != null) {
        Node oldHead = head;
        Node newHead = oldHead.next;
        head = newHead;
        oldHead.next = null;
        UNSAFE.unsafe.unpark(oldHead.thread);
        if (newHead != null) newHead.prev = null;
    }
}

Running the updated test shows that only one thread holds the lock at a time, and waiting threads acquire the lock in the order they were parked, confirming a correct FIFO lock implementation.

JavaConcurrencyLockAQSUnsafe
Hujiang Technology
Written by

Hujiang Technology

We focus on the real-world challenges developers face, delivering authentic, practical content and a direct platform for technical networking among developers.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.