After learning the AQS source code, realize a lock yourself

Posted by kylebragger on Fri, 28 Jan 2022 00:14:32 +0100

I studied a few days ago AQS source code In order to deepen the impression, today we will implement a lock based on AQS

1. Implement non reentrant lock based on AQS

Previously, we learned the source code of AQS and learned that a custom AQS needs to rewrite a series of functions and define the meaning of the atomic variable state.

In the following, we implement a lock ourselves. Defining state as 0 means that the lock is not held by a thread, and state as 1 means that the lock has been held by a thread. Because it is a non reentrant lock, it is not necessary to record the number of times the thread holding the lock obtains the lock. In addition, our customized lock support condition variable is because we want to implement the producer consumer model

class NonReentrantLock implements Lock, Serializable {
    //Implement AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        //Is the lock occupied
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        //If state is 0, an attempt is made to acquire the lock
        @Override
        protected boolean tryAcquire(int acquires) {
            assert acquires == 1;
            if (compareAndSetState(0, 1)) {
                //If CAS succeeds, the current thread will be set to obtain the lock
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //Try to release the lock and change the state to 1
        @Override
        protected boolean tryRelease(int releases) {
            assert releases == 1;
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        //Provide condition variable interface
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    //Create a Sync to do specific work
    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
}

In the above code, nonrentrantlock defines an internal class Sync to implement specific lock operations, while Sync inherits AQS. Since we implement exclusive mode locks, Sync overrides three methods tryAcquire, tryRelease and isHeldExclusively. In addition, Sync provides newCondition, which is used to support condition variables

2. Implement producer consumer model with user-defined lock

public class AQSDemo {
    final static NonReentrantLock lock = new NonReentrantLock();
    final static Condition notFull = lock.newCondition();
    final static Condition notEmpty = lock.newCondition();

    final static Queue<String> queue = new LinkedBlockingQueue<>();
    final static int queueSize = 10;

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            lock.lock();
            try {
                //Wait if the queue is full
                while (queue.size() == queueSize) {
                    notEmpty.await();
                }
                //Add element to queue
                queue.add("ele");
                //Wake up consumer thread
                notFull.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        Thread consumer = new Thread(() -> {
            lock.lock();
            try {
                //Wait if the queue is full
                while (queue.size() == 0) {
                    notFull.await();
                }
                //Consumption queue
                queue.poll();
                //Wake up production thread
                notEmpty.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        producer.start();
        consumer.start();
    }
}

The above code first creates an object Lock of NonReentrantLock, and then calls lock.. Newcondition creates two condition variables for synchronization between producer and consumer threads.

In the main function, the producer thread is created first, and lock is called inside the thread Lock() obtains the exclusive lock, and then determines whether the current queue is full. If it is full, use notempty Await() blocks suspending the current thread. It should be noted that while instead of if is used to avoid false wake-up. If the queue is dissatisfied, add elements to the queue directly, then call notFull.. Signalall() wakes up all consuming threads blocked by consuming elements, and finally releases the acquired lock.

Topics: Java Back-end JavaSE