1. AQS
The full name is AbstractQueuedSynchronizer, which is the framework for blocking locks and related synchronizer tools. It is characterized by the state attribute representing the state of the resource (exclusive and shared), and subclasses that define how to maintain this state and control how locks are acquired and released.
1. getState-Get state;
2. setState-Set state;
3. compareAndSetState-cas mechanism sets state;
4. Exclusive mode means that only one thread can access resources, while shared mode allows multiple threads to access resources.
5. A FIFO-based wait queue is provided, similar to the Monitor's EntryList;
6. Conditional variables to achieve wait, wake up mechanism, support multiple conditional variables, similar to WaitSet of Monitor;
Subclasses mainly implement these methods(Default throw UnsupportedOperationException) tryAcquire tryRelease tryAcquireShared tryReleaseShared isHeldExclusively
Implement non-reentrant locks based on AQS:
package com.concurrent.test; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; public class TestAqs { public static void main(String[] args) { MyLock lock = new MyLock(); new Thread(() -> { lock.lock(); try{ System.out.println("locking..."); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("unlocking..."); lock.unlock(); } }, "t1").start(); new Thread(() -> { lock.lock(); try{ System.out.println("locking..."); }finally { System.out.println("unlocking..."); lock.unlock(); } }, "t2").start(); } } // Custom locks (non-reentrant locks) class MyLock implements Lock { // Exclusive Lock Synchronizer Class class Mysync extends AbstractQueuedSynchronizer{ @Override protected boolean tryAcquire(int arg) { if(compareAndSetState(0, 1)){ // Lock added and owner set to current thread setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } @Override protected boolean tryRelease(int arg) { // Notice the order of the two, and the variable after volatile adds a write barrier so that the values after it see the latest values before it setExclusiveOwnerThread(null); setState(0); return true; } @Override // Whether to hold exclusive locks protected boolean isHeldExclusively() { return getState() == 1; } public Condition newCondition(){ return new ConditionObject(); } } private Mysync sync = new Mysync(); @Override // Lock (unsuccessfully enters the waiting queue) public void lock() { sync.acquire(1); } @Override // Lock to interrupt public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override // Attempt to lock (once) public boolean tryLock() { return sync.tryAcquire(1); } @Override // Attempt to lock with timeout public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); } @Override // Unlock public void unlock() { sync.release(1); } @Override public Condition newCondition() { return sync.newCondition(); } }
2. ReentrantLock principle
Locking and unlocking process
Default unfair implementation
public ReentrantLock() { sync = new NonfairSync(); }
NonfairSync inherits from AQS
Unfair Lock Lock Lock Unlock
final void lock() { if (compareAndSetState(0, 1)) // Direct competitive lock success (one lock success) setExclusiveOwnerThread(Thread.currentThread()); else // Failed to lock, take acquire method acquire(1); } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { // Get the precursor node of the current node final Node p = node.predecessor(); // If the precursor node is the head node and is in the second place, try again to obtain the lock if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } -------------------------------------------------------- // Release lock // ReentrantLock.unlock public void unlock() { sync.release(1); } // aqs public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } // ReentrantLock protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
When there is no competition
When the first competition appears
Thread-1 executed
1. cas failed in attempting to change state from 0 to 1;
2. Enter the tryAcquire logic, when the state is already 1, the result still fails;
3. Next enter the addWaiter logic to construct the Node queue:
- The yellow triangle in the diagram represents the waitStatus state of the Node, where 0 is the default normal state;
- Node is lazy to create;
- The first Node, called Dummy or Sentinel, occupies a space and does not have a thread associated with it;
The current thread enters acquireQueued logic
1. acquireQueued will continuously attempt to acquire a lock in a dead loop, failing to enter the park blocking;
2. If you are next to the head, try tryAcquire again to acquire the lock, and of course the state is still 1, failing;
3. Enter shouldParkAfterFailedAcquire logic and change the precursor node, waitStatus of head, to -1, which returns false this time.
4. ShouParkAfterFailedAcquire returns to acquireQueued after execution and tryAcquire attempts to acquire the lock again, of course, the state is still 1 and fails.
5. When entering shouldParkAfterFailedAcquire again, it returns true because waitStatus of its precursor node is already -1.
6. Enter parkAndCheckInterrupt, Thread-1 park (in gray)
Again, multiple threads failed to compete through the process described above, and this happened
Thread-0 releases the lock and enters the tryRelease process if successful - Set exclusiveOwnerThread to null;
- state = 0;
The current queue is not null and head waitStatus=-1 enters the unparkSuccessor process:
1. Find the nearest Node in the queue to the head (not cancelled), unpark resumes its operation, Thread-1 in this case;
2. Return to the acquireQueued process of Thread-1;
If the lock is successful (no competition), it will be set - exclusiveOwnerThread is Thread-1, state = 1;
- head points to the Node where Thread-1 was just located, and the Node empties the Thread;
- The original head can be garbage collected because it is disconnected from the chain list;
If there are other threads competing (unfair representation), such as Thread-4
If it happens to be preempted by Thread-4 - Thread-4 is set to exclusiveOwnerThread, state = 1;
- Thread-1 enters the acquireQueued process again, fails to acquire the lock, and re-enters the park blocking;
Reentrant principle
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // If a lock has been acquired, the thread is still the current thread, indicating that a lock reentry has occurred else if (current == getExclusiveOwnerThread()) { // state++ int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // Lock reentry is supported and only state is released when it is reduced to 0 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
Interruptible principle
Non-interruptable mode:
In this mode, even if it is interrupted, it will remain in the AQS queue until the lock is acquired before it can continue to run (yes! Only the interrupt flag is set to true).
private final boolean parkAndCheckInterrupt() { // If the interrupt flag is already true, the park will fail LockSupport.park(this); // interrupted knows the interrupt flag (here it is guaranteed that a thread can park here multiple times) return Thread.interrupted(); } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; // Or a lock needs to be acquired before the interrupt state can be returned (therefore, interrupts are invalid until the lock is not acquired and enter Park in parkAndCheckInterrupt multiple times) return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // Return interrupt status to true if interrupt is awakened interrupted = true; } } finally { if (failed) cancelAcquire(node); } } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // Regenerate an interrupt selfInterrupt(); }
Interruptable mode:
public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // If no lock is obtained, enter (1) if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } // (1) Interruptable lock acquisition process private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // This is where interrupt s will enter during park // At this point, an exception is thrown and not re-entered for(;;) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
Therefore, no interruption resets only the interruption mark, and if no lock money is obtained, it enters the dead-loop park again, so the interruption is invalid. Interruptable locks can be interrupted by throwing an exception instead of entering the dead-loop.
Fair Lock Implementation Principle
Unfair Lock Implementation
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // If a lock has not been acquired if (c == 0) { // Attempting to obtain with cas is unfair: don't check AQS queues if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // If a lock has been acquired, the thread is still the current thread, indicating that a lock reentry has occurred else if (current == getExclusiveOwnerThread()) { // state++ int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // Get failed, return to caller return false; }
Fair Lock Implementation
// The main difference between unfair locks is the implementation of the tryAcquire method protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // Check AQS queue for precursor nodes before competing if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } // Is there a node in the queue public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; // H!= T means there is Node in the queue return h != t && // (s = h.next) == null means there is no second in the queue yet ((s = h.next) == null || // Or the second-oldest thread in the queue is not this thread s.thread != Thread.currentThread()); }
Conditional variable implementation principles
Each conditional variable actually corresponds to a waiting queue whose implementation class is ConditionObject
await process
Start Thread-0 holding the lock, call await, enter ConditionObject's addConditionWaiter process, create a new Node status of -2 (Node.CONDITION), associate Thread-0, join the waiting queue tail
Next enter the AQS fullyRelease process to release the lock on the synchronizer
The next node in the unpark AQS queue, Competitive Lock, Thread-1 will compete successfully if there are no other competing threads
park blocks Thread-0
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // Enter the ConditionObject's addConditionWaiter process, create a new Node status of -2 (Node.CONDITION), associate Thread-0, join the waiting queue tail Node node = addConditionWaiter(); // Release lock int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { // The current thread enters the park waiting to be waked up LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); } private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; } final int fullyRelease(Node node) { boolean failed = true; try { int savedState = getState(); // Re-entrant lock once release if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; } }
signal process
Assume Thread-1 wants to wake up Thread-0
Enter the doSignal process of the ConditionObject to get the first Node in the waiting queue, the Node where Thread-0 is located
Execute the transferForSignal process, add the Node to the end of the AQS queue, change the waitStatus of Thread-0 to 0, and the waitStatus of Thread-3 to -1
Thread-1 releases the lock and enters the unlock process
public final void signal() { // Does the current thread hold locks if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // Take the first node in conditionObject Node first = firstWaiter; if (first != null) // Execute doSignal method doSignal(first); } private void doSignal(Node first) { do { // If there is only one node in the queue, leave the tail node empty if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // If the node transfer succeeds, it exits, the transfer fails and the queue is not empty, Node moves back and continues trying } while (!transferForSignal(first) && (first = firstWaiter) != null); } final boolean transferForSignal(Node node) { // Change the state of the current node from -2 to 0 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; // Add the current node to the end of the aqs queue and return to the precursor node of the current node Node p = enq(node); int ws = p.waitStatus; // Return success if the precursor node waitStatus > 0 or the precursor node waitStatus is modified to -1 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // Otherwise, enter the unpark process LockSupport.unpark(node.thread); return true; }