头图

Abstract synchronous queue AQS parsing

AQS - low-level support for locks

AbstractQueuedSynchronizer The abstract synchronization queue is referred to as AQS. It is the basic component for implementing the synchronizer. The bottom layer of the lock in the concurrent package is implemented by AQS. Let's take a look at the class diagram structure of AQS.

image-20220119113503475

The figure shows, the AQS is a FIFO of deque , the internal nodes by elemental recording head and tail of the tail and head of the queue, the queue element type Node .

Among them, the thread variable in Node is used to store the thread entering the AQS queue, and SHARED is used to mark that the thread is blocked and suspended when it acquires shared resources and is put into the AQS queue; EXCLUSIVE is used to mark the thread is suspended when it acquires exclusive resources. Put it into the AQS queue; waitStatus records the waiting state of the current thread, which can be CANCELLED (thread cancellation), SIGNAL (thread needs to be woken up), CONDITION (thread waiting in condition queue), PROPAGATE (when releasing shared resources) Notify other nodes); prev records the predecessor node of the current node, and next is the predecessor node.

state is maintained in AQS, and the value can be modified through the getState, setState, compareAndSetState functions.

  • For the implementation of ReentrantLock, state can represent the number of times the current thread acquires the lock ;
  • For the read-write lock ReentrantReadWriteLock, the high 16 bits of state represent the read state, that is, the number of times the lock was acquired, and the low 16 bits represent the number of threads that have acquired the write lock 161f114a1d1070;
  • For Semaphore, state represents the number of currently available signals ;
  • For CountDownlatch, state is used to represent the current value of the counter ;

AQS has an inner class ConditionObject, which is used to achieve thread synchronization in conjunction with locks. ConditionObject can directly access variables inside the AQS object, such as state state value and queue.

ConditionObject is condition variable , each condition variable corresponds to a condition queue ( after calling the await method of the condition variable, and firstWaiter represents the head element of the queue, and lastWaiter represents the tail of the queue element.

条件队列

Here we first talk about several states represented by waitStatus.

  • CANCELLED (value: 1): Indicates that the current node has been canceled. When timeout or interruption (in the case of responding to interruption), it will be triggered to change to this state, and the node after entering this state will not change.
  • SIGNAL (value: -1): Indicates that the successor node is waiting for the current node to wake up. When the successor node joins the queue, the state of the predecessor node will be updated to SIGNAL.
  • CONDITION (value: -2): indicates that the node is waiting on the Condition, when other threads call the signal() method of the Condition, the node in the CONDITION state will transfer from the condition queue to the synchronization queue , waiting to obtain synchronization Lock.
  • PROPAGATE (value: -3): In shared mode, the predecessor node will not only wake up its successor node, but also may wake up the successor node of the successor.
  • are: 161f114a1d123f 0 : The default state when a new node is enqueued.

For AQS, the key to thread synchronization is to operate the state value state. According to whether the state belongs to a thread, the way of operating the state is divided into exclusive and shared.

Obtain resources in exclusive mode through: void acquire(int arg) 、void acquireInterruptibly(int arg) .

Release resources in exclusive mode via: boolean release(int arg) .

Resource acquisition in shared mode: void acquireShared(int arg) , void acquireSharedInterruptibly(int arg) .

Release resources in shared mode through: boolean releaseShared(int arg) .

Acquiring resources in exclusive mode is bound to a specific thread, that is to say, if a thread acquires a resource, it will mark it as acquired by this thread, and other threads will find that the resource is not owned by itself by operating state to acquire the resource. Then block.

For example, in the implementation of the exclusive lock ReentrantLock: when a thread acquires the ReentrantLock lock, the AQS first uses the CAS operation to change the state value from 0 to 1, and then sets the current lock holder as the current thread. When the thread acquires the lock again it is found that when the lock holder, the state value will be from 1 to 2, which is provided reentrant number , but he was not found on the lock holder when another thread acquires the lock Will be put into the AQS blocking queue and suspended.

The shared method of acquiring resources is not related to specific threads. When multiple threads request resources, they acquire resources through CAS. After one thread acquires resources, other threads acquire resources if the current resources can meet the needs. , you only need to obtain it through CAS.

For example, for Semaphore semaphore, when a thread obtains a semaphore through the acquire method, it will first check whether the current number of semaphores meets the needs. If not, the current thread will be put into the blocking queue, and if it is satisfied, the semaphore will be obtained through spin CAS.

In the exclusive mode, the process of acquiring and releasing resources is as follows:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

When a thread calls the acquire method to acquire exclusive resources, it first uses the tryAcquire method to try to acquire resources, specifically to set the value of the state variable state, and returns directly if successful; if it fails, the current thread is encapsulated as a Node node of type Node.EXCLUSIVE Then insert into the tail of the AQS blocking queue, and call LockSupport.park(this) to suspend itself.

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

When a thread calls the release method, it will try to use the tryRelease operation to release resources, which is also to set the value of the state variable state, and then call the LockSupport.unpark(thread) method to activate a blocked thread in the AQS queue. The activated thread uses tryAcquire to try to see if the current variable state value can still meet its own needs. If it is satisfied, it will continue to execute downward, otherwise it will be put into the queue and suspended.

📢 Note: The AQS class does not provide tryAcquire and tryRelease methods, which need to be implemented by specific subclasses. Use the CAS algorithm to try to modify the state state value according to different scenarios, and what does the increase or decrease of the state state value mean?

For example, the exclusive lock ReentrantLock inherited from AQS implementation, when the status is 0, it means the lock is idle, and when it is 1, it means the lock has been occupied. When rewriting tryAcquire, you need to use the CAS algorithm internally to check whether the current state is 0. If it is 0, use CAS to set it to 1, and set the current lock holder to the current thread, and then return true, if CAS fails, return false.

In the shared mode, the process of acquiring and releasing resources is as follows:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

When a thread calls acquireShared to acquire a shared resource, it will first try to acquire the resource through tryAcquireShared. Specifically, it will set the value of the state variable state, and return directly if it succeeds. If it fails, the current thread will be encapsulated as a Node node of type Node.SHARED and inserted into AQS to block. tail of the queue and suspend itself using LockSupport.park(this).

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

When the thread calls releaseShared, it still tries to release resources by trying the tryReleaseShared method, and also sets the value of the state variable state, and then uses LockSupport.unpark(thread) to activate a thread blocked in the AQS blocking queue, and the activated thread uses the tryReleaseShared method to view Whether the current state still meets its own needs, the activation thread will continue to execute downward if it is satisfied, otherwise it will be put into the AQS queue and suspended.

📢 It should also be noted that the AQS class does not provide the available tryAcquireShared and tryReleaseShared methods, which need to be implemented by subclasses.

For example, when the read lock in ReentrantReadWriteLock, a read-write lock inherited from AQS, rewrites tryAcquireShared, it first checks whether the write lock is held by other threads. If so, it returns false directly. Otherwise, use CAS to increment the upper 16 bits of the state (in ReentrantReadWriteLock). , the upper 16 bits of state are the number of times the read lock is acquired).

⚠️ In addition to rewriting the methods described above, the lock implemented based on AQS also needs to rewrite the isHeldExclusively method to determine whether the lock is exclusively or shared by the current thread.

In addition, we found that acquireInterruptibly(int arg) , acquireSharedInterruptibly(int arg) have the Interruptibly keyword. So what's the difference between with and without this keyword?

In fact, the method without the Interruptibly keyword means that it does not respond to the interrupt, that is, when the thread calls the method without Interruptibly to obtain resources or the acquisition fails and is suspended, other threads interrupt the thread, then the thread will not be thrown because of being interrupted. Exception, continue to obtain resources or be suspended, that is, do not respond to the terminal, ignore interrupt .

With the Interruptibly keyword, an InterruptedException will be thrown and returned.

Let's take a look at how AQS maintains the queue, mainly looking at the enqueue operation.

When a thread fails to acquire the lock, the thread will be converted to a Node node, and then use the enq (final Node node) method to insert the node into the AQS blocking queue.

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;//(1)
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))//(2)
                tail = head;
        } else {
            node.prev = t;//(3)
            if (compareAndSetTail(t, node)) {//(4)
                t.next = node;
                return t;
            }
        }
    }
}

步骤图

As shown in the above code, in the first loop, when the AQS queue status is shown in the figure (default), the head and tail point to null; when the code (1) is executed, the node t points to the tail node, and the queue status is shown in the steps shown in the figure. As shown in (1), t is null at this time. When executing code (2), use the CAS algorithm to set a sentinel node as the head node. If the setting is successful, let the tail node also point to the sentinel node. At this time, the queue status is as shown in step (2) ) shown.

Next, we need to insert the node node, so code (1) is executed after the second loop, and the queue state is shown in step (3) in the following figure; then execute code (3) to set the predecessor node of node as the tail node, The queue status is shown in step (4) in the figure below; then the node node is set as the tail node through the CAS algorithm. After the CAS is successful, the queue status is shown in step (5) in the figure below; then the back-drive node of the original tail node is set to node node, the doubly linked list is completed. The queue status is shown in step (6) in the following figure.

步骤图

AQS - Support for Condition Variables

Synchronized and condition variables can achieve thread synchronization. The difference between them is that synchronized can only synchronize with a shared variable notify or wait method at the same time, while a lock of AQS can correspond to multiple condition variables.

Let's look at an example next.

public static void main(String[] args) {
    final ReentrantLock lock = new ReentrantLock();// (1)
    final Condition condition = lock.newCondition();// (2)
    lock.lock(); // (3)
    try {
        System.out.println("begin wait...");
        condition.await(); // (4)
        System.out.println("end wait...");
    } catch (Exception e) {
        lock.unlock(); // (5)
    }
    lock.lock(); // (6)
    try {
        System.out.println("begin signal...");
        condition.signal(); // (7)
        System.out.println("end signal...");
    } catch (Exception e) {
        lock.unlock(); // (8)
    }
}

This code first creates another exclusive lock ReentrantLock object, also based on AQS.

The second step uses the newCondition() method of the created Lock object to create a ConditionObject variable, which is a condition variable corresponding to the Lock lock.

📢 A Lock object can create multiple condition variables.

The third step acquires an exclusive lock, and then the fourth step calls the await() method of the condition variable to block and suspend the current thread. When other threads call the signal() method of the condition variable, the blocked thread will return from await. It should be noted that it is the same as calling the wait() method of Object. Throws an IllegalMonitorStateException exception. The fifth step releases the acquired lock.

In the above code, the function of lock.newCondition() is to new a ConditionObject object declared inside AQS. ConditionObject is the inner class of AQS, which can access the variables (such as state variable state) and methods inside AQS. A condition queue (singly linked list queue) is maintained inside each condition variable to store the thread blocked when calling the await() method of the condition variable. Note that this conditional queue and AQS queue are not the same thing .

Let's take a look at the await() method source code:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();

            // 创建新的node节点,并插入到条件队列末尾(1)
            Node node = addConditionWaiter();
            // 释放锁并返回状态位(2)
            int savedState = fullyRelease(node);
            int interruptMode = 0;
                      // 调用park方法阻塞挂起当前线程(3)
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //...
        }

In this method, when the thread calls the await() method of the condition variable, it will construct a node node of type Node.CONDITION internally, and then insert the node into the end of the condition queue, and then the current thread will release the acquired lock. That is, the state value is manipulated, and it is blocked and suspended.

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

When another thread calls the signal method of the condition variable (the lock() method of the lock must be called first to obtain the lock), a thread node in the condition queue will be removed from the condition queue and placed in the blocking queue of AQS internally. , then activate the thread.

📢 It should be noted that AQS only provides the implementation of ConditionObject, and does not provide the newCondition function, which needs to be implemented by subclasses.

Let's take a look at how to put into the condition queue after the await() method is blocked.

private Node addConditionWaiter() {
    //获取尾部节点
    Node t = lastWaiter;
    // 如果lastWaiter不为空,则检查该队列是否有被Cancel的节点
    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;
}

📢 Note: When multiple threads call the lock.lock() method, only one thread acquires the lock, and other threads will be converted to Node nodes and inserted into the corresponding AQS blocking queue, and spin CAS to try to acquire the lock.

If the thread that acquires the lock calls the await method of the corresponding condition variable, the thread will release the acquired lock and be converted into a Node node and inserted into the condition queue corresponding to the condition variable.

When another thread calls the signal or signalAll method of the condition variable, it will move one or all Node nodes in the condition queue to the blocking queue of AQS, and wait for an opportunity to acquire the lock.

Summary: A lock corresponds to an AQS blocking queue, corresponding to multiple condition variables, each of which has its own condition queue.

Implement custom exclusive locks

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2022/1/20 实现自定义独占锁
 * @Description
 */
public class NonReentrantLock implements Lock, Serializable {

    //自定义实现AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        //是否持有锁
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        //如果state == 0 尝试获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            assert arg == 1;
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //尝试释放锁 设置state == 0
        @Override
        protected boolean tryRelease(int arg) {
            assert arg == 1;
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        //提供条件变量接口
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

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

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

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

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

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

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

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

Implement producer consumer based on custom lock

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2022/1/20
 * @Description 生产者消费者模型
 */
public class LockTest {

    final static NonReentrantLock lock = new NonReentrantLock();
    final static Condition consumerCondition = lock.newCondition();
    final static Condition producerCondition = lock.newCondition();
    final static Queue<String> QUEUE = new LinkedBlockingQueue<>();
    final static int QUEUE_SIZE = 10;

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        // 启消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("消费者消费了" + lockTest.get());
                }
            }, "consumer_" + i).start();
        }
        // 启动生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(200));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lockTest.put("物品-" + new Random().nextInt(1000));
                }
            }, "产品-" + i).start();
        }

    }

    private void put(String name) {
        //获取独占锁
        lock.lock();
        try {
            //如果队列满了,则等待
            while (QUEUE.size() == QUEUE_SIZE) {
                producerCondition.await();
            }
            QUEUE.add(name);
            System.out.println(Thread.currentThread().getName() + "生产了" + name);
            //唤醒消费线程
            consumerCondition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private String get() {
        String ret = "";
        //获取独占锁
        lock.lock();
        try {
            //如果队列空了,则等待
            while (QUEUE.size() == 0) {
                consumerCondition.await();
            }
            //消费一个元素
            ret = QUEUE.poll();
            //唤醒生产线程
            producerCondition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return ret;
    }

}

神秘杰克
765 声望382 粉丝

Be a good developer.