Multithreading tutorial AQS principle

Posted by avillanu on Sat, 05 Mar 2022 08:58:41 +0100

Multithreading tutorial (34) AQS principle

1. General

Its full name is AbstractQueuedSynchronizer, which is the framework of blocking locks and related synchronizer tools

characteristic:

  • The state attribute is used to represent the state of resources (exclusive mode and shared mode). Subclasses need to define how to maintain this state and control how to obtain and release locks

    • getState - get state
    • setState - set state
    • compareAndSetState - cas mechanism sets state state
    • Exclusive mode allows only one thread to access resources, while shared mode allows multiple threads to access resources
  • It provides a FIFO based waiting queue, which is similar to the EntryList condition variable of Monitor to realize the waiting and wake-up mechanism, and supports multiple condition variables, similar to the WaitSet of Monitor

Subclasses mainly implement such methods (UnsupportedOperationException is thrown by default)

  • tryAcquire

  • tryRelease

  • tryAcquireShared

  • tryReleaseShared

  • isHeldExclusively

Get lock pose

// If lock acquisition fails
if (!tryAcquire(arg)) {
 // To join the queue, you can choose to block the current thread park unpark
}

Release lock posture

// If the lock is released successfully
if (tryRelease(arg)) {
 // Let the blocked thread resume operation
}

Personal understanding: in short, AQS is an abstract class of locks? We can meet our needs by using AQS to create locks with specific attributes.

2. Realize non reentrant lock

2.1 custom synchronizer
final class MySync extends AbstractQueuedSynchronizer {
    @Override
    protected boolean tryAcquire(int acquires) {
        if (acquires == 1){
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
        }
        return false;
    }
    @Override
    protected boolean tryRelease(int acquires) {
        if(acquires == 1) {
            if(getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        return false;
    }
    protected Condition newCondition() {
        return new ConditionObject();
    }
    @Override
    protected boolean isHeldExclusively() {
        return getState() == 1;
    }
}
2.2 Custom Lock

With a custom synchronizer, it is easy to reuse AQS and realize a fully functional custom lock

class MyLock implements Lock {
    static MySync sync = new MySync();
    @Override
    // The attempt is unsuccessful and enters the waiting queue
    public void lock() {
        sync.acquire(1);
    }
    @Override
    // The attempt is unsuccessful. It enters the waiting queue and can be interrupted
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    // Try once, return unsuccessfully, and do not enter the queue
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    @Override
    // The attempt is unsuccessful and enters the waiting queue with a time limit
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    @Override
    // Release lock
    public void unlock() {
        sync.release(1);
    }
    @Override
    // Generate condition variable
    public Condition newCondition() {
        return sync.newCondition();
    }
}

Test it

MyLock lock = new MyLock();
new Thread(() -> {
    lock.lock();
    try {
        log.debug("locking...");
        sleep(1);
    } finally {
        log.debug("unlocking...");
        lock.unlock();
    }
},"t1").start();
new Thread(() -> {
    lock.lock();
    try {
        log.debug("locking...");
    } finally {
        log.debug("unlocking...");
        lock.unlock();
    }
},"t2").start();

output

22:29:28.727 c.TestAqs [t1] - locking... 
22:29:29.732 c.TestAqs [t1] - unlocking... 
22:29:29.732 c.TestAqs [t2] - locking... 
22:29:29.732 c.TestAqs [t2] - unlocking...

Non reentrant test

If you change to the following code, you will find that you will also be blocked (you will only print locking once)

lock.lock();
log.debug("locking...");
lock.lock();
log.debug("locking...");

The implementation of AQS is to create a class that inherits AQS inside the lock, then customize some functions you need, and then let the external object that inherits the lock call the class that inherits AQS.

1) state design

state uses volatile with cas to ensure its atomicity during modification

State uses 32bit int to maintain the synchronization state, because the test results using long on many platforms are not ideal

2) Blocking recovery design

The early APIs that control thread pause and resume are suspend and resume, but they are not available because if you call resume first

Then suspend will not perceive it

The solution is to use park & unpark to pause and resume threads. The specific principle has been mentioned before. Unpark first and then park again

problem

Park & unpark is for the thread, not for the synchronizer, so the control granularity is more fine

park threads can also be interrupted through interrupt

3) Queue design

FIFO first in first out queue is used, and priority queue is not supported

CLH queue is used for reference in the design, which is a one-way lockless queue

Topics: JavaEE Back-end