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