You learn the "waste" AQS source code

Version: JDK 11

For this article, I have already scrapped, too much. My ability is limited, so I can't promise to speak clearly.

The premise is that the judge knows something about this, especially some concepts, such as CAS, queue, thread life cycle, etc.

Also note: this is pure AQS source code parsing, without combining actual subclasses and usage scenarios, so it is difficult to understand. Later, I will analyze the subclasses of AQS to strengthen my understanding.

1. What is AQS?

AQS is AbstractQueuedSynchronizeran abbreviation , translated as Abstract Queue Synchronizer .

As the name suggests, where there is abstraction, there is implementation, and what abstraction is for is very important.

  • Abstraction: defines the general logic of resource acquisition, and requires subclasses to maintain the synchronization state in different scenarios .
  • Queue: If the requested resource is occupied, the requesting thread needs a mechanism for blocking, waiting and waking up. And this mechanism, AQS is implemented with the CLH queue, and puts the thread that cannot get the lock temporarily into the queue.
  • Synchronizer: If you don't look at the queue, it is actually an "abstract synchronizer". That is, the basic framework of synchronization tools and locks is defined. Based on AQS, custom synchronization tools can be built simply and efficiently: such as ReentrantLock, Semaphore, etc.

2. The principle of AQS

A synchronizer generally provides two operations, acquire and release. acquire blocks the calling thread until or unless the synchronization state allows it; release changes the synchronization state so that one or more threads blocked by acquire continue to execute. Corresponding to the pseudocode in [1]:

acquire:
while (synchronization state does not allow acquire) {
    
    
	enqueue current thread if not already queued;
	possibly block current thread;
}
dequeue current thread if it was queued;

release:
update synchronization state;
if (state may permit a blocked thread to acquire)
	unblock one or more queued threads;

A series of classes implemented by AQS are actually based on two operations to unify abstract concepts, such as Lock.lock and unlock, Semaphore.acquire and release, etc.

Simply put, if you need to implement the above process, you will go back to the capabilities that AQS needs to provide at least and the collaboration between them:

  • Atomic management of synchronization state
  • Thread blocking and unblocking
  • Queuing of blocking threads

Based on these capabilities, AQS will extend:

  • can be interrupted;
  • timeout control;
  • Condition, used to support await/signal operations in the form of monitors.

The synchronizer will have two implementations (or two management methods) for the control of the synchronization state: exclusive mode and shared mode .

3. Core structure

3.1 Synchronization status

Through the management of the synchronization state, it is possible to identify shared resources that can be obtained.

Because multiple threads may go back and try to operate the synchronization state, there are two key points: volatile and CAS.

It can be noticed that the operation method is protected, because the specific meaning of state is defined and operated by subclasses, such as representing the number of resources and the status of locks.

  • For the subclass Sync in ReentrantLock, state indicates the number of times the thread is locked (0 means unlocked, otherwise it means how many times it is locked by the same thread-reentrant)
  • For ReentrantReadWriteLock, the high 16 bits of the state represent the number of times the thread locks the read lock, and the low 16 bits represent the number of times the thread locks the write lock (read locks and write locks are also reentrant, and there will be mutual Repulsion)
  • For Semaphore, state represents its available semaphore. The short and loose statement can be: represents the number of resources that can be obtained.
  • For CountDownLatch, the state indicates how many "locks" the latch still has (a lock in the real sense, not represented by the Lock or synchronized keywords in the code), and it can be realized that the state becomes 0 after multiple threads execute the unlock, marking the The latch is opened (the corresponding blocked thread can only be released for execution at this time).
/**
 * The synchronization state.
 */
private volatile int state;

// 返回同步状态的当前值。 
protected final int getState() {
    
    
    return state;
}
// 设置同步状态的值。
protected final void setState(int newState) {
    
    
    state = newState;
}
// 如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。
protected final boolean compareAndSetState(int expect, int update) {
    
    
    return STATE.compareAndSet(this, expect, update);
}
3.2 Thread blocking and wakeup

Thread blocking and wakeup are mainly operated by the LockSupport class. This class is a layer of encapsulation of Unsafe# park & ​​unpark, and on this basis, the timeout method is allowed.

park blocks the thread until or unless unparked.

  • Before calling park, call unpark, then park is useless;
  • Calls to unpark are not counted, so calling the unpark method multiple times before a par call will only undo a park operation.
3.3 Thread saving in exclusive mode

The thread associated with Node identifies the thread that is waiting when the resource is occupied, so there must be a thread that already occupies the resource. And the parent class of AQS AbstractOwnableSynchronizerplays this role, the source code is very simple, just to maintain this exclusive thread:

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    
    

    protected AbstractOwnableSynchronizer() {
    
     }

    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
    
    
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
    
    
        return exclusiveOwnerThread;
    }
}
3.4 CLH queue variants

CLH locks are Craig, Landin, and Hagersten (CLH) locks, because their underlying layer is implemented based on queues (that is, strict first-in-first-out), and are generally called CLH queue locks. CLH lock is also a scalable and high-performance lock based on linked list, which is generally used for spin locks.

The first change - the link of the node

The node of CLH lock maintains the position of the predecessor node through pred (the original version does not have it, but the spin lock application scenario has it), and judges the current node by judging the state of the predecessor node through spin.

Entering the queue is only based on the tail to judge the competition and operation; otherwise, it is only based on the head.

 		+------+  prev +-----+       +-----+
 head   |      | <---- |     | <---- |     |  tail
 		+------+       +-----+       +-----+

The advantage of this design is that, unlike ordinary queues that are not associated between nodes, AQS can use CLH locks to handle "timeout" and "cancellation": when the predecessor node times out or cancels, it can be forwarded through pred" not cancelled” on the node link. (The original purpose is to go to the previous node to use its state field.)

And this is not enough, when the CLH lock changes the state of the previous node, the next node must be aware of it based on spin. But in AQS, it is also necessary to maintain the back-drive node next to explicitly wake up the next node (thread).

If next is empty (probably canceled), look forward from tail to see if there is a real next node.

Second Variation - Node Properties

The original node attributes are used to judge the spin, and the most important thread blocking and waking up in AQS is the communication between nodes. Therefore, it is required that the state of the node will be more abundant, which is used for communication between nodes in the exclusive and shared mode, and possibly in the case of condition.

4 CLH queue variant structure

4.1 Queue structure

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-V111JxLl-1616247740859)(/Users/jry/Downloads/code-snapshot (3)].png)

Node defines the structure of the CLH queue, which is quite different from the original CLH queue:

  • waitStatus : Identify the current (the biggest difference from the original CLH) node status, and list all status constants for setting.

    The flow between states will be analyzed in detail later; especially the negative value state involves communication between threads.

  • prev, next: predecessor and successor nodes;

  • thread: A node is actually a "waiting" thread.

  • SHARED, EXCLUSIVE: Node instance, used to represent the possession mode (shared or exclusive). May be set on nextWaiter.

  • nextWaiter: Indicates the next waiting node on the condition; or SHARED identifies sharing. Naturally, there is no waiting.

    If there is no value, then it is easiest, identifying exclusive mode.

  • CLH originally had a virtual head node, but in AQS, the head node is delayed until the first competition; (similarly, the tail node is also, but the head and tail are the same at the beginning.)

There are still important variable operation handles and corresponding CAS operations, just know that there is this thing:

// 其实底层用的还是 Unsafe
private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
    
    
    try {
    
    
        MethodHandles.Lookup l = MethodHandles.lookup();
        NEXT = l.findVarHandle(Node.class, "next", Node.class);
        PREV = l.findVarHandle(Node.class, "prev", Node.class);
        THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
        WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
    } catch (ReflectiveOperationException e) {
    
    
        throw new ExceptionInInitializerError(e);
    }
}

In the end, there are actually two queues in the overall data structure of AQS: the synchronous waiting queue (the name mentioned in the code is SyncQueue ), and the conditional queue (nextWaiter: ConditionQueue ) implemented based on this structure . Simply put, it looks like this:

img(Image source from: https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/112645551)

4.2 SyncQueue

SyncQueue is a blocking queue waiting for state resources. It is the field that AQS is responsible for processing. It abstracts and standardizes a set of queues that use blocking threads as nodes and associate thread blocking, awakening, and propagation with enqueue and dequeue operations.

// 延迟初始化;或者通过 setHead() 设置
// 状态一定不会是 CANCELLED
// 头结点是虚拟结点, 所以一定不会关联线程
private transient volatile Node head;

// 延迟初始化;只能通过 enq() 添加新的结点
private transient volatile Node tail;

// 用于结点出队, 所以这个节点就没用了, 用来设为 head
// 仅通过acquire方法调用, 成功就是获取到资源了, 不用排队了
// 所以对应到上文中, 头结点的状态不会是 CANCELLED
private void setHead(Node node) {
    
    
    head = node;
    // 自然就不关联线程了
    node.thread = null;
    node.prev = null;
}

// VarHandle mechanics
private static final VarHandle STATE;
private static final VarHandle HEAD;
private static final VarHandle TAIL;

static {
    
    
    try {
    
    
        MethodHandles.Lookup l = MethodHandles.lookup();
        STATE = l.findVarHandle(AbstractQueuedLongSynchronizer.class, "state", long.class);
        HEAD = l.findVarHandle(AbstractQueuedLongSynchronizer.class, "head", Node.class);
        TAIL = l.findVarHandle(AbstractQueuedLongSynchronizer.class, "tail", Node.class);
    } catch (ReflectiveOperationException e) {
    
    
        throw new ExceptionInInitializerError(e);
    }

    // Reduce the risk of rare disastrous classloading in first call to
    // LockSupport.park: https://bugs.openjdk.java.net/browse/JDK-8074773
    Class<?> ensureLoaded = LockSupport.class;
}

private final void initializeSyncQueue() {
    
    
    Node h;
    // 设置虚拟头结点, 头尾相同
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}
// CAS 设置 tail
private final boolean compareAndSetTail(Node expect, Node update) {
    
    
    return TAIL.compareAndSet(this, expect, update);
}

When a node is enqueued, it means that it has been occupied or exhausted. Threads that continue to try to acquire are queued, and when joining the queue, if the queue has not been initialized, the queue will be initialized first (mainly because the head is a virtual node).

// 结点入队; 首次则初始化
// 主要用于 ConditionQueue 中
private Node enq(Node node) {
    
    
    // 循环 + CAS
    for (;;) {
    
    
        Node oldTail = tail;
        if (oldTail != null) {
    
    
            node.setPrevRelaxed(oldTail);
            // 尾结点的设置可能存在竞争, 所以需要 CAS
            if (compareAndSetTail(oldTail, node)) {
    
    
                oldTail.next = node;
                return oldTail;
            }
        } else {
    
    
            // 相当于头尾皆空, 第一次入队
            initializeSyncQueue();
        }
    }
}
// 主要用于 SyncQueue 的入队
private Node addWaiter(Node mode) {
    
    
    // 不同于 enq, node 代表是预设的 EXCLUSIVE 或 SHARED.
    // 标识"独占" or "共享"
    Node node = new Node(mode);

    for (;;) {
    
    
        Node oldTail = tail;
        if (oldTail != null) {
    
    
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
    
    
                oldTail.next = node;
                return node;
            }
        } else {
    
    
            initializeSyncQueue();
        }
    }
}

// 用于结点出队, 所以这个节点就没用了, 用来设为 head
// 仅通过acquire方法调用, 成功就是获取到资源了, 不用排队了
// 所以对应到上文中, 头结点的状态不会是 CANCELLED
private void setHead(Node node) {
    
    
    head = node;
    // 自然就不关联线程了
    node.thread = null;
    node.prev = null;
}

Enqueue:

unnamed

Dequeue:

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-XotB6UpU-1616247740869)(https://gitee.com/ryan_july/clouding_img/raw/master/img/21312312413123 .gif)]

4.3 ConditionQueue

ConditionQueue is a relatively special and complex queue in AQS. Compared with SyncQueue, which only handles resource scrambling and thread communication in AQS, ConditionQueue is a data structure designed to match Object#wait, notify, and notifyAll, so that developers can manually operate threads.

ConditionQueue is based on SyncQueue, or is based on the Node structure. And its operation entrance, I believe that students who have been in contact with JUC should know it, that is Conditionthe interface .

The relevant methods in AQS are all there ConditionObject, and this class just implements Conditonthe interface .

In the waiting method of Condition, "false wakeup" is repeatedly mentioned. It is recommended to use the waiting method in the loop, and re-judgment whether the condition is satisfied after waking up.

public interface Condition {
    
    
    // 当前线程进入等待状态直到被唤醒或者中断
    void await() throws InterruptedException;
    // 当前线程进入等待状态,不响应中断,阻塞直到被唤醒
    void awaitUninterruptibly();
    // 当前线程进入等待状态直到被唤醒或者中断,阻塞带时间限制
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    // 当前线程进入等待状态直到被唤醒或者中断,阻塞带时间限制
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    // 当前线程进入等待状态直到被唤醒或者中断,阻塞带时间限制
    boolean awaitUntil(Date deadline) throws InterruptedException;
    // 唤醒单个阻塞线程
    void signal();
    // 唤醒所有阻塞线程
    void signalAll();
}    

ConditionIt can be said that there is only the only implementation class at present, that is AQS ConditionObject, which Condtionis . Let's take a look at ConditionObjectthe source code. Like SyncQueue, we only focus on the method of entering and exiting the queue for the time being, so as to facilitate understanding of the queue structure.

public class ConditionObject implements Condition, java.io.Serializable {
    
    
    
    private transient Node firstWaiter;
    private transient Node lastWaiter;
    public ConditionObject() {
    
    }

    // 每个 await 方法第一步就是调用该方法加入队列
    private Node addConditionWaiter() {
    
    
        if (!isHeldExclusively())
            // 只有独占模式下,才有 Condition 的作用
            throw new IllegalMonitorStateException();
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        // 在 ConditionQueue 中,只要不是 CONDITION 状态, 都看做取消等待了.需要清除出去
        if (t != null && t.waitStatus != Node.CONDITION) {
    
    
            // 遍历清除"取消"结点
            unlinkCancelledWaiters();
            t = lastWaiter;
        }

        Node node = new Node(Node.CONDITION);

        if (t == null)
            firstWaiter = node;
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
    }
    
    // 一般都是在等待期间进行取消
    // 1.插入新结点发现 lastWaiter 是取消的
    // 2.线程被唤醒时, 如果后面还有等待的结点,就做一次处理
    private void unlinkCancelledWaiters() {
    
    
        Node t = firstWaiter;
        Node trail = null;
        while (t != null) {
    
    
            Node next = t.nextWaiter;
            if (t.waitStatus != Node.CONDITION) {
    
    
                t.nextWaiter = null;
                if (trail == null)
                    firstWaiter = next;
                else
                    trail.nextWaiter = next;
                if (next == null)
                    lastWaiter = trail;
            }
            else
                trail = t;
            t = next;
        }
    }
}  

Now that the core structure is finished, all AQS operations are based on such a system. In fact, from the overall point of view, perhaps we have figured out the context:

  • 1. Obtain resources and operate state;
  • 2. Resources are occupied
    • 2.1 In the exclusive mode, the thread needs to wait, and the thread is packaged as a node and queued;
      • When entering the team for the first time, the head virtual node must be constructed first;
      • Blocking nodes enqueue;
    • 2.2 In shared mode, it may be possible to continue to operate the state;
  • 3. How to compete for resources?
  • 4. I can’t grab anymore, how to quit?
  • 5. The resource is released, how to wake up the queued follow-up nodes to grab it?
  • 6. Will those who can't rush and run away affect other people queuing?

Let’s take a look at the method of obtaining resources and manipulating state first

5. Template API for operating resources

Since AQS is only an abstract framework, which defines the standard process and operation of the synchronizer, the remaining point for subclasses to implement extensions is to give the actual meaning related to the state and the corresponding operations (acquire and release, based on AQS's implementation of state CAS methods) (including exclusive and shared).

isHeldExclusively()//该线程是否正在独占资源。只有用到 condition (AQS.ConditionObject)才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

These methods are protected, and the methods are all throw new UnsupportedOperationException(). Subclasses must use state's CAS method to implement these methods to give state meaning in actual application scenarios.

ps: Other methods are either private or final.

Of course, it is not necessary to implement all of them, and it is still based on subclass scenarios. For example, if it is only used in the exclusive mode, it is not necessary to implement the method of the shared mode, for example ReentrantLock; similarly, it is not necessary to implement the exclusive method of the shared mode, for example Semaphore.

But it is not limited to all implementations. For example ReentrantReadWriteLock, write locks are exclusive, and read locks are shared.

Taking ReentrantLock as an example, the state is initialized to 0, indicating the unlocked state. When A thread lock(), it will call tryAcquire() to monopolize the lock and state+1. After that, other threads will fail when tryAcquire(), and other threads will have the opportunity to acquire the lock until the A thread unlock() reaches state=0 (that is, the lock is released). The lock is reentrant, and thread A can acquire this lock repeatedly (state will accumulate). But pay attention to how many times you need to release it, so as to ensure that the state can return to zero.

Taking CountDownLatch as an example, the task is divided into N sub-threads to execute, and the state is also initialized to N (note that N must be consistent with the number of threads). The main thread calls await() (tryAcquireShared()) to block. After each child thread is executed, countDown() (releaseShared(1)) once, and the state will be decremented by 1 in CAS. After all sub-threads are executed (that is, state=0), the main thread will be unpark(), and then the main thread will return from the await() function to continue the remaining actions.

6. Acquire and release resources

The public methods of AQS itself are limited, and only methods related to acquire and release are involved in resource operations, which distinguish between timeouts and interruptions.

In theory, if the subclass has no special circumstances, as long as the template method is correctly defined, the user can directly use the public method of AQS to actually use the concurrency tool. For example, ReentrantLocklock, tryLock and release are all public methods that directly call AQS.

public void lock() {
    
     sync.acquire(1);}
public void lockInterruptibly() throws InterruptedException {
    
    
    sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    
    
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
    
      sync.release(1);}

Whether it is acquire or release, there are two modes, which are exclusive and shared, which are often mentioned above.

We have already talked about the two bottom-level operations above: resource operations (subclass implementation) and thread enqueue.

Next, we need to sort out the whole process by distinguishing the process, and go through the source code of AQS, which involves thread wake-up, thread communication, resource competition, state transfer, etc.

6.1 Exclusive acquisition
6.1.1 Process overview

Let me tell the conclusion first, that is, look at the picture! Note that I am not searching in depth, but in breadth. First analyze the methods at one level, because they are all similar and basically the same; then proceed step by step.

AQS

6.1.2 Public entry method
// 1.独占获取,忽略中断.
// arg 的含义取决于 state 的含义
public final void acquire(int arg) {
    
    
    // a.首先尝试获取资源
    if (!tryAcquire(arg) &&
        // b.失败, 标志为独占结点, 进入等待队列
        // c.继续循环尝试获取资源(头结点) || 阻塞,等待前驱结点唤醒
        // c-d.异常的情况下, 可能需要取消排队, 唤醒后继结点, 恢复中断
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
    
    
        // e.如果原来是中断的, 则恢复中断
        selfInterrupt();
    }
    
}
// 2.可被中断的独占获取
public final void acquireInterruptibly(int arg) throws InterruptedException {
    
    
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        // 因为响应中断,所以不用恢复中断
        // 和 acquire 中的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 是一样的效果,只是阻塞时响应中断
        doAcquireInterruptibly(arg);
    	// 异常的情况下, 可能需要取消排队, 唤醒后继结点
}

// 3.可被中断的超时独占获取
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
    throws InterruptedException {
    
    
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        // 和 acquire 中的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 是一样的效果
        // 不同的是 1.阻塞时响应中断; 2.超时阻塞、循环获取资源时,首先判断时间允许。否则需要取消排队, 唤醒后继结点
        doAcquireNanos(arg, nanosTimeout);
    // 异常的情况下, 可能需要取消排队, 唤醒后继结点
}

The outermost layer is very simple. One is to judge and handle interrupts, and the other is to try to obtain resources first.

But once the acquisition fails, subsequent queue operations are required (such as packaging as nodes, blocking, waking up, canceling, processing predecessor nodes, etc.).

acquireQueued + addWaiter, doAcquireInterruptibly(arg), doAcquireNanosand do exactly this kind of thing, but the handling of timeout and blocking is different.

6.1.3 Infinite loop acquisition, queuing, blocking

Let me take a look at the first type acquireQueued(the rest are similar), which is the frame area in the above picture:

final boolean acquireQueued(final Node node, int arg) {
    
    
    // 用来标记原始中断状态
    boolean interrupted = false;
    try {
    
    
        for (;;) {
    
    
            final Node p = node.predecessor();
            // 每次循环判断前驱结点是否 head,
            // 是,则说明前面无等待的线程, 尝试获取资源
            if (p == head && tryAcquire(arg)) {
    
    
                // 获取成功, 出队
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // 判断是否阻塞
            if (shouldParkAfterFailedAcquire(p, node))
                // 阻塞, 并拿到原始的中断状态
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
    
    
        // 取消排队等待
        cancelAcquire(node);
        if (interrupted)
            // 恢复中断
            selfInterrupt();
        throw t;
    }
}

// 所有类型的获取方法,在循环中都依据该方法,进行阻塞判断
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 而当前线程要阻塞的前提, 就是要找个小伙伴到时候叫醒他. 而这个小伙伴,通常就是前驱结点
        // 怎么让小伙伴到时候记得提醒自己, 就是将它的状态设为 SIGNAL, 表示到时候叫醒我(如何叫醒见 unparkSuccessor)
        return true;
    if (ws > 0) {
    
    
        // 表示前驱结点是取消状态, 往前把取消的都清理掉
        do {
    
    
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    
    
        // 前驱结点可能正在获取, 所以这里先设置状态, 但不阻塞, 再尝试一次
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

// 阻塞当前线程,获取并且重置线程的中断标记位
private final boolean parkAndCheckInterrupt() {
    
    
    LockSupport.park(this);
    return Thread.interrupted();
}

The other two types have added interrupt processing or timeout processing:

private void doAcquireInterruptibly(int arg) throws InterruptedException {
    
    
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
    
    
        for (;;) {
    
    
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
    
    
                setHead(node);
                p.next = null; // help GC
                return;
            }
            // 这里是要响应中断的.
            if (shouldParkAfterFailedAcquire(p, node) &&
                    // 被唤醒之后马上检查中断状态, 并尝试恢复
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
    
    
        cancelAcquire(node);
        throw t;
    }
}
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    
    
    // 先检查超时
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
    
    
        for (;;) {
    
    
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
    
    
                setHead(node);
                p.next = null; // help GC
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            // 每次循环(唤醒)都检查超时
            if (nanosTimeout <= 0L) {
    
    
                cancelAcquire(node);
                return false;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    // 如果超时剩余时间小于 1000 ns, 就继续循环.避免线程状态切换的损耗
                nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
    
    
        cancelAcquire(node);
        throw t;
    }
}
6.1.4 Cancel queue

When an exception occurs, or an interruption occurs, or a timeout occurs, the previously addWaiteradded removed (maybe not removed immediately, and the "cancellation status" is used in the middle to avoid affecting other nodes), which means canceling the wait.

Then it is necessary to do some processing according to the situation of the current node.

image-20210319155215045

private void cancelAcquire(Node node) {
    
    
    // Ignore if node doesn't exist
    if (node == null)
        return;
    // 1.置空节点持有的线程,因为此时节点线程已经发生中断
    node.thread = null;

    // 2.跳过已取消的前驱结点, 找个第一个有效的前驱
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    // 3.将状态设为取消
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
    
    
        // 4.1如果是 tail, 重新设置 tail, 并且将前驱的 next 置空
        pred.compareAndSetNext(predNext, null);
    } else {
    
    
        int ws;
        // 4.2 既不是 tail, 也不是 head.next
        // 则将前继节点的waitStatus置为SIGNAL(因为后面肯定还有等待的)
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
                // 注意 原来的状态不能是取消, 或者等待取消(thread == null)
            pred.thread != null) {
    
    
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                // 并使node的前继节点指向node的后继节点
                pred.compareAndSetNext(predNext, next);
        } else {
    
    
            // 4.3 如果node是head的后继节点,则直接唤醒node的后继节点
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}
6.2 Exclusive release

Relatively speaking, the exclusive release is just a set of templates, and the logic is clear

  • Release resources (definitely involves the judgment of holding threads, but it is done by subclasses)
  • reset head state
  • Wake up the thread of head's effective successor node
// 独占释放
public final boolean release(int arg) {
    
    
    // 1.首先释放资源
    if (tryRelease(arg)) {
    
    
        Node h = head;
        // 2.头结点不为空, 并且是阻塞的情况(需要唤醒后继的状态)
        if (h != null && h.waitStatus != 0)
            // 3.唤醒后继结点(跳过中间可能存在取消的结点)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    
    
    int ws = node.waitStatus;
    if (ws < 0)
        // 重置为 0. 后面结点唤醒后会成为新的 head
        node.compareAndSetWaitStatus(ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    
    
        s = null;
        // 如果是空的,或者取消,则从后往前找到第一个等待唤醒的:
        // 1. addWaiter(Node node) 是先设置前驱节点 后设置后继节点 虽然这两步分别是原子的 但在两步之间还是可能存在后继节点未链接完成的情况
        // 2. 在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node
        // 如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        // 被唤醒, 结合前文, 就是退出 parkAndCheckInterrupt, 重新循环
        LockSupport.unpark(s.thread);
}
6.3 Shared Acquisition
6.3.1 Overview

In the shared mode, the acquisition and release are the same as the resource operation and dequeue entry in the exclusive mode, the difference is (the process node in the red font in the figure below )

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-28VCQeZm-1616247740874)(/Volumes/personal/note/AQS-copy of page 1.png)]

6.3.2 Public entry method
// 忽略中断的共享获取
// 最后恢复中断
public final void acquireShared(int arg) {
    
    
    if (tryAcquireShared(arg) < 0)
        // 获取失败, 结点入队, 循环尝试或阻塞
        doAcquireShared(arg);
}
// 可被中断的共享获取
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    
    
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        // 同 acquireShared, 循环或阻塞过程检查中断
        doAcquireSharedInterruptibly(arg);
}
// 可被中断的超时共享获取
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    
    
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquireShared(arg) >= 0 ||
        // 同 acquireShared, 循环或阻塞过程检查中断以及是否超时
        doAcquireSharedNanos(arg, nanosTimeout);
}
6.3.3 Loop acquisition, queuing, blocking

From the above figure, we can clearly see that there are only three processes in red font:

  • The node is enqueued, and the mode is shared.
  • After the acquisition is successful, the remaining available resources are returned; the current thread needs to wake up the successor nodes in turn instead of exiting directly.
  • When released, the successor nodes are woken up in batches at the same time.

So, just take doAcquireSharedas an example:

private void doAcquireShared(int arg) {
    
    
    // 共享模式
    final Node node = addWaiter(Node.SHARED);
    boolean interrupted = false;
    try {
    
    
        for (;;) {
    
    
            final Node p = node.predecessor();
            if (p == head) {
    
    
                int r = tryAcquireShared(arg);
                =================================	
           |     if (r >= 0) {
    
    						|
           |        // 设置头结点, 并传播都后继结点	    |
           |        setHeadAndPropagate(node, r);	|
           |        p.next = null; // help GC		|
           |        return;							|
               =================================
                }
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
    
    
        cancelAcquire(node);
        throw t;
    } finally {
    
    
        if (interrupted)
            selfInterrupt();
    }
}
private void setHeadAndPropagate(Node node, int propagate) {
    
    
    Node h = head; // Record old head for check below
    setHead(node);
    // h.waitStatus 是同时处理 SIGNAL 和  PROPAGATE, 结合 doReleaseShared 设置 PROPAGATE.
    if (propagate > 0
        // 应该是 0 的(当前被唤醒, 或直接拿到)
        // 如果不是 0, 则应该是 PROPAGATE. 说明 setHead 完成之前, 其他线程释放资源,然后将老的 head 改为 PROPAGATE.见 doReleaseShared
        || h == null || h.waitStatus < 0 ||
        // 新的 head 可能有三种情况
        // 1. 0 :没有后继结点或刚入队,没来得及改头, 见 shouldParkAfterFailedAcquire
        // 2. -1 : 已入队, 并将 head 改为 SIGNAL , 见 shouldParkAfterFailedAcquire
        // 2. -3 : 其他线程释放资源, 将 head 还为 0 时, 改为 PROPAGATE. 见 doReleaseShared
        (h = head) == null || h.waitStatus < 0) {
    
    
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

private void doReleaseShared() {
    
    
    for (;;) {
    
    
        Node h = head;
        if (h != null && h != tail) {
    
    
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
    
    
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 如果是需要信号的结点, 则直接尝试唤醒
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                // 否则设置为传播
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            // 可能其他线程拿到资源出队了, 所以继续循环
            break;
    }
}

I have always wondered what the state of PROPAGATE is useful for. Combining doReleaseShared and setHeadAndPropagate, it mainly solves the problem of subsequent wake-up in concurrent situations.

If doReleaseShared does not set PROPAGATE, setHeadAndPropagate only judges 0 and -1.

So what was the result? Refer to an article: https://juejin.cn/post/6910104545133920269#heading-17

image-20210319184923989

6.4 Shared release
public final boolean releaseShared(int arg) {
    
    
    if (tryReleaseShared(arg)) {
    
    
        // 见上文
        doReleaseShared();
        return true;
    }
    return false;
}

So far, I have finally finished the process of acquiring, queuing, blocking, waking up, and releasing in AQS, which is very difficult.

7. Queue check method

7.1 Open method public final
boolean hasQueuedThreads();//是否有等待线程
Collection<Thread> getExclusiveQueuedThreads();//获取独占等待线程列表
boolean hasContended() {
    
     return head != null;}// 是否有线程争抢过资源
Thread getFirstQueuedThread();//第一个等待线程
boolean isQueued(Thread thread);//指定线程是否在排队
boolean hasQueuedPredecessors()//当前线程前是否还有其他排队的线程,一般用作公平锁的实现中
int getQueueLength()// 排队长度,以 thread 不为空 为准
Collection<Thread> getQueuedThreads()//排队线程集合
Collection<Thread> getExclusiveQueuedThreads()//独占排队线程集合
Collection<Thread> getSharedQueuedThreads()//共享排队线程集合
7.2 Non-public methods
//获取第一个等待线程,区别于公有方法直接使用 head == tail 判断失败,则使用该方法。
// 先从前往后,找不到则从后往前。避免并发影响
private Thread fullGetFirstQueuedThread();
final boolean apparentlyFirstQueuedIsExclusive()//是否存在第一个排队的线程是以独占模式。读写锁非公平实现中获取读锁时先判断是否有在等待写锁

So far, the method of AQS Sync Queue process is also finished. The remaining Node operation methods are designed for Condition Queue, and there will be a separate article and subclass implementation later.

8. Framework method summary

Here is a picture from the technical team of Meituan. It is clear at a glance. I don’t think there is any need to reinvent the wheel.

Image source: https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

img

9. Exclusive mode implementation example

public class MyLock {
    
    

    class Sync extends AbstractQueuedSynchronizer {
    
    

        protected boolean tryAcquire(int arg) {
    
    
            return compareAndSetState(0, 1);
        }

        protected boolean tryRelease(int arg) {
    
    
            setState(0);
            return true;
        }

        protected boolean isHeldExclusively() {
    
    
            // 主要是 condition用到, 随便意思一下
            return true;
        }
    }

    Sync sync = new Sync();

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

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

Run an example with this tool

public class AqsDemo {
    
    
    static int lockCount = 0;
    static int unlockCount = 0;
    static MyLock myLock = new MyLock();

    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable runnable = new Runnable() {
    
    
            @Override
            public void run () {
    
    
                try {
    
    
                    for (int i = 0; i < 10000; i++) {
    
    
                        unlockCount++;
                    }
                    // 睡一会,确保会有线程切换
                    Thread.sleep(1000);
                    myLock.lock();
                    System.out.println(Thread.currentThread().getName() + ":我开始跑了");
                    for (int i = 0; i < 10000; i++) {
    
    
                        lockCount++;
                    }
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + ":我跑完了");
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    myLock.unlock();
                }

            }
        };
        Thread thread1 = new Thread(runnable, "线程1");
        Thread thread2 = new Thread(runnable, "线程2");
        thread1.start();thread2.start();thread1.join();thread2.join();
        System.out.println("加锁累加: " + lockCount);
        System.out.println("未加锁累加: " + unlockCount);
    }
}

result:

线程1:我开始跑了   
线程1:我跑完了    // 即使中间睡了一秒钟,也不会切换,线程是持有锁的
线程2:我开始跑了
线程2:我跑完了
加锁累加: 20000
未加锁累加: 14667

Summarize

There are a lot of source codes, which are rather boring, and may be easy to understand and incompetent. It mainly comes from my own context and habits of looking at the source code, and I hope it can be used as a reference for everyone.

image-20210320210451358

This is the context before I wrote it myself, and it doesn't feel very good. Because the length is very long, I couldn't put down the description and use of subclass implementation tools

shoulders of giants

  • The java.util.concurrent Synchronizer Framework Chinese translation
  • http://www.throwable.club/2019/04/07/java-juc-aqs-source-code/
  • https://snailclimb.gitee.io/javaguide/#/docs/java/multi-thread/AQS%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8AAQS%E5%90%8C%E6%AD%A5%E7%BB%84%E4%BB%B6%E6%80%BB%E7%BB%93?id=_23-aqs-%e5%ba%95%e5%b1%82%e4%bd%bf%e7%94%a8%e4%ba%86%e6%a8%a1%e6%9d%bf%e6%96%b9%e6%b3%95%e6%a8%a1%e5%bc%8f
  • https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

Guess you like

Origin blog.csdn.net/jiangxiayouyu/article/details/115035547