Multithreaded Learning (V)

Posted by wdseelig on Thu, 02 Dec 2021 01:12:55 +0100

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;
    }

Topics: Java Multithreading lock