The locks framework of Java multi-threaded JUC: AQS overview

1. Introduction to AQS

The AbstractQueuedSynchronizer abstract class (hereinafter referred to as AQS) is java.util.concurrentthe core of the entire package. In JDK1.5, Doug Lea introduced the JUC package, and most of the synchronizers in the package were built based on AQS. The AQS framework provides a set of general mechanisms to manage synchronization state, block/wake up threads, and manage waiting queues.

The well-known synchronizers such as ReentrantLock, CountDownLatch, CyclicBarrier, etc. actually implement the APIs exposed by the AQS framework through internal classes to achieve various synchronizer functions. The main difference between these synchronizers is actually the definition of the synchronization state.

The AQS framework separates a series of concerns when constructing a synchronizer. All its operations revolve around the resource-synchronization state (synchronization state), and solve the following problems for users:

  1. Can resources be accessed at the same time? Or can only be accessed by one thread at the same time? (Shared/Exclusive Function)
  2. How to concurrently manage threads that access resources? (Waiting queue)
  3. If the thread can't wait for resources, how to exit from the waiting queue? (Timeout/interrupt)

This is actually a typical template method design pattern: the parent class (AQS framework) defines the skeleton and internal operation details, and the specific rules are implemented by the subclass.
The AQS framework leaves one remaining question to the user:
what is a resource? How to define whether the resource can be accessed?

Let's take a look at the definition of this problem for several common synchronizers:

Synchronizer Definition of resources
ReentrantLock The resource represents an exclusive lock. State 0 means the lock is available; 1 means occupied; N means the number of reentrants
CountDownLatch The resource represents a countdown counter. State of 0 means that the counter is reset to zero, and all threads can access resources; N means that the counter is not reset to zero, and all threads need to be blocked.
Semaphore Resources represent semaphores or tokens. State ≤ 0 means that no token is available and all threads need to be blocked; greater than 0 means that the token is available. Each time the thread acquires a token, the State is reduced by 1, and the thread does not release a token, and the State is increased by 1.
ReentrantReadWriteLock Resources represent shared read locks and exclusive write locks. The state is logically divided into two 16-bit unsigned shorts, which record the number of threads used by the read lock and the number of times the write lock is reentered.

In summary, the AQS framework provides the following functions:

1.1 Provide a set of template framework

Due to the existence of concurrency, there are many situations that need to be considered. Therefore, whether these two goals can be achieved in a relatively simple way is very important, because for users (users of the AQS framework), many times do not care Intricate details inside. AQS actually uses the template method pattern to achieve this. Most of the methods in AQS are final or private, which means that Doug Lea does not want users to use these methods directly, but only overwrites some of the methods specified by the template. .
AQS allows users to solve the above-mentioned " how to define whether the resource can be accessed " problem by exposing the following APIs :

Hook method description
tryAcquire Exclusive access (number of resources)
tryRelease Exclusive release (number of resources)
tryAcquireShared Shared acquisition (number of resources)
tryReleaseShared Shared acquisition (number of resources)
isHeldExclusively Exclusive status

1.2 Support interruption, timeout

Remember those methods of lock interruption, time-limited waiting, and lock attempt in the Lock interface? The realization of these methods is actually provided by AQS built-in.
All synchronizers using the AQS framework support the following operations:

  • Blocking and non-blocking (such as tryLock) synchronization;
  • Optional timeout setting, so that the caller can give up waiting;
  • Interruptible blocking operation.

1.3 Support exclusive mode and shared mode

1.4 Support Condition wait

The Condition interface can be seen as a substitute for the wait(), notify(), and notifyAll() methods of the Obechct class, and is used in conjunction with Lock.
The AQS framework ConditionObjectimplements the Condition interface through an internal class to provide the function of conditional waiting for subclasses.

Two, AQS method description

As mentioned in the first part of this chapter, AQS uses the template method pattern, most of which are final or private. We call this type of method * Skeleton Method* , which means that these methods are defined by the AQS framework itself. Skeleton, sub-categories cannot be overwritten.
The following will briefly describe some of the more important methods by category, and the specific implementation details and principles will be explained in detail in the subsequent parts of this series.

2.1 CAS operation

CAS, that is, CompareAndSet, the implementation of CAS operations in Java is entrusted to a class named UnSafe. Regarding the Unsafeclass, this class will be introduced in detail in the future. At present, as long as you know, atomic operations on fields can be achieved through this class.

Method name Modifier description
compareAndSetState protected final CAS modify synchronization state value
compareAndSetHead private final CAS modifies the head pointer of the waiting queue
compareAndSetTail private final CAS modifies the tail pointer of the waiting queue
compareAndSetWaitStatus private static final CAS modifies the waiting state of the node
compareAndSetNext private static final CAS modifies the next pointer of the node

2.2 The core operation of the waiting queue

Method name Modifier description
enq private Enqueue operation
addWaiter private Enqueue operation
setHead private Set head node
unparkSuccessor private Wake up successor node
doReleaseShared private Release shared node
setHeadAndPropagate private Set the head node and propagate the wake-up

2.3 Obtaining resources

Method name Modifier description
cancelAcquire private Cancel access to resources
shouldParkAfterFailedAcquire private static Determine whether to block the current calling thread
acquireQueued final Try to get resources, get failed, try to block the thread
doAcquireInterruptibly private Exclusive access to resources (response to interruption)
doAcquireNanos private Obtain resources exclusively (wait for a limited time)
doAcquireShared private Shared access to resources
doAcquireSharedInterruptibly private Shared access to resources (response to interruption)
doAcquireSharedNanos private Shared resources (waiting for a limited time)
Method name Modifier description
acquire public final Exclusive access to resources
acquireInterruptibly public final Exclusive access to resources (response to interruption)
acquireInterruptibly public final Obtain resources exclusively (wait for a limited time)
acquireShared public final Shared access to resources
acquireSharedInterruptibly public final Shared access to resources (response to interruption)
tryAcquireSharedNanos public final Shared resources (waiting for a limited time)

2.4 Release operations of resources

Method name Modifier description
release public final Release exclusive resources
releaseShared public final Release shared resources

3. Brief description of AQS principle

As we mentioned in the first section, the AQS framework separates a series of concerns when constructing a synchronizer. All its operations revolve around the resource-the synchronization state. Therefore, around the resource, there are three derived Basic question:

  1. Management of synchronization state
  2. Blocking/waking up threads
  3. Thread waiting queue management

3.1 Synchronization status

Definition of
synchronization state The synchronization state is actually a resource. AQS uses a single int (32 bits) to save the synchronization state, and exposes getState, setState, and compareAndSetState operations to read and update this state.

/**
 * 同步状态.
 */
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}
/**
 * 以原子的方式更新同步状态.
 * 利用Unsafe类实现
 */
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

3.2 Thread blocking/waking up

Before JDK 1.5, apart from the built-in monitor mechanism, there was no other way to safely and conveniently block and wake up the current thread.
After JDK1.5, the java.util.concurrent.locks package provides the LockSupport class as a tool for thread blocking and wake-up.

3.3 等待队列

等待队列,是AQS框架的核心,整个框架的关键其实就是如何在并发状态下管理被阻塞的线程。
等待队列是严格的FIFO队列,是Craig,Landin和Hagersten锁(CLH锁)的一种变种,采用双向链表实现,因此也叫CLH队列。

1. 结点定义
CLH队列中的结点是对线程的包装,结点一共有两种类型:独占(EXCLUSIVE)和共享(SHARED)。
每种类型的结点都有一些状态,其中独占结点使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享结点使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。

结点状态 描述
CANCELLED 1 取消。表示后驱结点被中断或超时,需要移出队列
SIGNAL -1 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
CONDITION -2 Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了
PROPAGATE -3 传播。适用于共享模式(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
INITIAL 0 默认。新结点会处于这种状态

AQS使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,之所以这种做是因为在双向链表的结构下,这样更容易实现取消和超时功能。

next指针:用于维护队列顺序,当临界区的资源被释放时,头结点通过next指针找到队首结点。
prev指针:用于在结点(线程)被取消时,让当前结点的前驱直接指向当前结点的后驱完成出队动作。

static final class Node {
    
    // 共享模式结点
    static final Node SHARED = new Node();
    
    // 独占模式结点
    static final Node EXCLUSIVE = null;

    static final int CANCELLED =  1;

    static final int SIGNAL    = -1;

    static final int CONDITION = -2;

    static final int PROPAGATE = -3;

    /**
    * INITAL:      0 - 默认,新结点会处于这种状态。
    * CANCELLED:   1 - 取消,表示后续结点被中断或超时,需要移出队列;
    * SIGNAL:      -1- 发信号,表示后续结点被阻塞了;(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
    * CONDITION:   -2- Condition专用,表示当前结点在Condition队列中,因为等待某个条件而被阻塞了;
    * PROPAGATE:   -3- 传播,适用于共享模式。(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
    * 
    * waitStatus表示的是后续结点状态,这是因为AQS中使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,这样更容易实现取消和超时功能。
    */
    volatile int waitStatus;

    // 前驱指针
    volatile Node prev;

    // 后驱指针
    volatile Node next;

    // 结点所包装的线程
    volatile Thread thread;

    // Condition队列使用,存储condition队列中的后继节点
    Node nextWaiter;

    Node() {
    }

    Node(Thread thread, Node mode) { 
        this.nextWaiter = mode;
        this.thread = thread;
    }
}

2. 队列定义
对于CLH队列,当线程请求资源时,如果请求不到,会将线程包装成结点,将其挂载在队列尾部。
CLH队列的示意图如下:

①初始状态,队列head和tail都指向空

②首个线程入队,先创建一个空的头结点,然后以自旋的方式不断尝试插入一个包含当前线程的新结点

image-20210306112441026

/**
 * 以自旋的方式不断尝试插入结点至队列尾部
 *
 * @return 当前结点的前驱结点
 */
private Node enq(final Node node) {
    for (; ; ) {
        Node t = tail;
        if (t == null) { // 如果队列为空,则创建一个空的head结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

四、总结

本章简要介绍了AQS的思想和原理,读者可以参考Doug Lea的论文,进一步了解AQS。
本文参考:https://segmentfault.com/a/1190000015562787

关注公众号,输入“java-summary”即可获得源码。

完成,收工!

传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。

Guess you like

Origin blog.csdn.net/wuxiaolongah/article/details/114435974