Following on from the previous article, this article focused on adding a timed tryLock method. After this introduction, ReentrantLock is implemented in all but condition, and is basically similar to the actual ReentrantLock code. As mentioned in the previous article, the actual ReentrantLock relies on AQS, but this article will not introduce AQS directly, just an incomplete analysis of AQS.

By the way, “Write Yourself ReentrantLock and ReentrantReadWriteLock” is expected to be divided into 3 or 4 parts due to its length.

Before getting into the introduction of timed tryLock methods, consider the following question: Can FairLock1 and UnfairLock1 reuse nodes?

Why do you ask? C++ code is concerned with the life cycle of an object. When implementing locks in C++, you must be aware of when Node memory can be reclaimed. If you know CLHLock, you can reuse nodes, up to N (threads) +1 (sentry) nodes, and in theory no recycling is required. So FairLock1, which is basically the same as CLHLock, can reuse the presequence node as its current node in a similar way. In UnfairLock1, because the mode may be unfair, we do not know who the preorder node is, so it cannot be easily reused.

 

Continue to consider whether Node can be reused when acquiring a lock for a limited time. To be honest, I don’t know for sure, but even in fair mode, timed tryLock is much harder to reuse nodes than UnfairLock1. For example, what happens when thread B tries to acquire the lock again, but then thread C enters the queue? Firstly, B cannot reuse the pre-sequence node due to the acquisition failure. Secondly, C may hold the node pointer of B, or skip B and link to the prior node of B. Therefore, implementing tryLock with timeout generally considers creating a new Node each time to reduce complexity.

Probably because the sample code is Java, it doesn’t feel like a new Node comes out every time. However, if you implement tryLock with a timeout version in C++, you must consider who will reclaim Node memory. For those interested, check out papers on locks with timeouts, such as TOLock.

The second question to consider is, what is the effect of allowing time limits?

  1. The preceding node may have timed out
  2. Subsequent nodes may have timed out
  3. Head cannot be a timeout node (except sentinel node)

In the figure above, C’s previous node B has timed out. In order for A to wake itself up, C needs to set its previous pointer to A and A’s next pointer to itself. Of course the signal antecedent is also to be set. For A, the subsequent node B has timed out, and A needs to wake up the first node among its subsequent nodes that has not timed out, namely C. The third point is understandable: the head advance is handled by the thread that has acquired the lock, so the new head pointer must correspond to a non-timeout thread.

The first and second points need to be considered. When A and C are executed at the same time, how to design A mechanism including B that guarantees THAT C will definitely wake up (allowing multiple awakenings)?

UnfairTimedLock1

The following is the time-limited lock based on AQS transformation, and the specific analysis of how to ensure the wake up thread C.

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
 
@SuppressWarnings("Duplicates")
public class UnfairTimedLock1 implements Lock {
 
    private final Queue queue = new Queue();
    private final AtomicInteger reentrantTimes = new AtomicInteger(0);
    private Thread owner;
 
    public boolean tryLock(long time, @Nonnull TimeUnit unit) throws InterruptedException {
        if (owner == Thread.currentThread()) {
            // reentrant
            reentrantTimes.incrementAndGet();
            return true;
        }
        if (reentrantTimes.get() == 0 && reentrantTimes.compareAndSet(0, 1)) {
            owner = Thread.currentThread();
            return true;
        }
        final long deadline = unit.toNanos(time) + System.nanoTime();
        Node node = new Node(Thread.currentThread());
        Node predecessor = queue.enqueue(node);
        long nanos;
        while (true) {
            if (predecessor == queue.head.get() &&
                    reentrantTimes.get() == 0 && reentrantTimes.compareAndSet(0, 1)) {
                myTurn(predecessor, node);
                return true;
            }
            nanos = deadline - System.nanoTime();
            // timeout
            if (nanos <= 0L) {
                abort(predecessor, node);
                return false;
            }
            switch (predecessor.status.get()) {
                case Node.STATUS_ABORTED:
                    predecessor = queue.skipAbortedPredecessors(predecessor, node);
                    predecessor.successor.set(node);
                    break;
                case Node.STATUS_SIGNAL_SUCCESSOR:
                    LockSupport.parkNanos(this, nanos);
                    break;
                case Node.STATUS_NORMAL:
                    /*
                     * recheck is required after CAS
                     * 1. CAS failed
                     * 2. CAS successfully, but status changed to ABORTED before parking
                     * 3. predecessor unlock between first check and CAS(no unpark)
                     */
                    predecessor.status.compareAndSet(Node.STATUS_NORMAL, Node.STATUS_SIGNAL_SUCCESSOR);
                    break;
            }
            if (Thread.interrupted()) {
                abort(predecessor, node);
                throw new InterruptedException();
            }
        }
    }
 
    private void abort(@Nonnull Node predecessor, @Nonnull Node node) {
        node.clearThread();
 
        Node p = queue.skipAbortedPredecessors(predecessor, node);
        Node ps = p.successor.get();
 
        // linearization point
        node.status.set(Node.STATUS_ABORTED);
 
        /*
         * at end
         *
         * A   -> |<B>
         * ANY -> |ABORTED
         *
         * no lost-wakeup problem
         */
        if (queue.tail.get() == node && queue.tail.compareAndSet(node, p)) {
            /*
             * failure is ok, which means
             * new node may enqueue between removing and setting successor
             */
            p.successor.compareAndSet(ps, null);
            return;
        }
 
        /*
         * at beginning
         *
         * A   -> |<B>       -> C
         * ANY -> |ABORTED   -> ANY
         *
         * lost-wakeup problem may happen
         *
         * scenarios
         * 1. B didn't set flag of A
         *
         * 2. B was signaled
         * sequence
         *    a. B set flag
         *    b. A signaled B
         *    c. B aborted
         */
        if (p == queue.head.get()) {
            signalNormalSuccessor(node);
            return;
        }
 
        /*
         * in middle
         *
         * A   -> |B   -> <C>     -> D
         * ANY -> |ANY -> ABORTED -> ANY
         *
         * lost-wakeup problem may happen
         *
         * conditions
         * 1. no one set flag of B
         * 1.1 D set flag of C before C aborts
         * 1.2 C didn't set the flag of B
         *
         * 2. B acquired lock and finished processing after p == head check
         *
         * first, try to set flag of B, then recheck if predecessor finished(unlocked or aborted)
         */
        if (p.ensureSignalSuccessorStatus() && p.thread.get() != null) {
            Node s = node.successor.get();
            if (s != null && s.status.get() != Node.STATUS_ABORTED) {
                p.successor.compareAndSet(ps, s);
            }
        } else {
            signalNormalSuccessor(node);
        }
    }
 
    private void myTurn(@Nonnull Node predecessor, @Nonnull Node node) {
        owner = Thread.currentThread();
        node.clearThread();
        queue.head.set(node);
        node.predecessor.set(null);
        predecessor.successor.set(null);
    }
 
    private void signalNormalSuccessor(@Nonnull Node node) {
        Node successor = queue.findNormalSuccessor(node);
        if (successor != null) {
            LockSupport.unpark(successor.thread.get());
        }
    }
 
    public void unlock() {
        if (owner != Thread.currentThread()) {
            throw new IllegalStateException("not the thread holding lock");
        }
        int rt = reentrantTimes.get();
        if (rt < 1) {
            throw new IllegalStateException("reentrant times < 1 when try to unlock");
        }
        if (rt > 1) {
            reentrantTimes.set(rt - 1);
            return;
        }
        // rt == 1
        owner = null;
        // linearization point
        reentrantTimes.set(0);
 
        Node node = queue.head.get();
        if (node != null &&
                node.status.get() == Node.STATUS_SIGNAL_SUCCESSOR &&
                node.status.compareAndSet(Node.STATUS_SIGNAL_SUCCESSOR, Node.STATUS_NORMAL)) {
            signalNormalSuccessor(node);
        }
    }
 
    @Override
    public void lock() {
        throw new UnsupportedOperationException();
    }
 
    @Override
    public void lockInterruptibly() throws InterruptedException {
        throw new UnsupportedOperationException();
    }
 
    @Override
    public boolean tryLock() {
        throw new UnsupportedOperationException();
    }
 
    @Override
    @Nonnull
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }
 
    @SuppressWarnings("Duplicates")
    private static class Queue {
        final AtomicReference<Node> head = new AtomicReference<>();
        final AtomicReference<Node> tail = new AtomicReference<>();
 
        /**
         * Enqueue node.
         *
         * @param node new node
         * @return predecessor
         */
        @Nonnull
        Node enqueue(@Nonnull Node node) {
            Node t;
            while (true) {
                t = tail.get();
                if (t == null) {
                    // lazy initialization
                    Node sentinel = new Node();
                    if (head.get() == null && head.compareAndSet(null, sentinel)) {
                        tail.set(sentinel);
                    }
                } else {
                    node.predecessor.lazySet(t);
                    // linearization point
                    if (tail.compareAndSet(t, node)) {
                        t.successor.set(node);
                        return t;
                    }
                }
            }
        }
 
        @Nullable
        Node findNormalSuccessor(@Nonnull Node node) {
            Node n = node.successor.get();
            if (n != null && n.status.get() != Node.STATUS_ABORTED) {
                return n;
            }
 
            // find normal node from tail
            Node c = tail.get();
            n = null;
            // tail maybe null during lazy initialization
            while (c != null && c != node) {
                if (c.status.get() != Node.STATUS_ABORTED) {
                    n = c;
                }
                c = c.predecessor.get();
            }
            return n;
        }
 
        @Nonnull
        Node skipAbortedPredecessors(@Nonnull Node predecessor, @Nonnull Node node) {
            Node h = head.get();
            Node p = predecessor;
            while (p != h && p.status.get() != Node.STATUS_ABORTED) {
                p = p.predecessor.get();
                /*
                 * set predecessor every time to help successors of
                 * current node to find the normal predecessor more quickly
                 */
                node.predecessor.set(p);
            }
            return p;
        }
    }
 
    /**
     * Node.
     * <p>
     * Status change:
     * NORMAL -> ABORTED
     */
    private static class Node {
        static final int STATUS_NORMAL = 0;
        static final int STATUS_ABORTED = -1;
        static final int STATUS_SIGNAL_SUCCESSOR = 1;
 
        /**
         * thread will be null if
         * 1. abort
         * 2. enter mutual exclusion area
         */
        final AtomicReference<Thread> thread;
        final AtomicInteger status = new AtomicInteger(STATUS_NORMAL);
        final AtomicReference<Node> predecessor = new AtomicReference<>();
        // optimization
        final AtomicReference<Node> successor = new AtomicReference<>();
 
        Node() {
            this(null);
        }
 
        Node(@Nullable Thread thread) {
            this.thread = new AtomicReference<>(thread);
        }
 
        boolean ensureSignalSuccessorStatus() {
            int s = this.status.get();
            return s == STATUS_SIGNAL_SUCCESSOR ||
                    (s == STATUS_NORMAL && this.status.compareAndSet(s, STATUS_SIGNAL_SUCCESSOR));
        }
 
        void clearThread() {
            thread.set(null);
        }
    }
}Copy the code

First, Node added status, replacing the previous signal precursor. The status migration diagram is shown below

There are four types of migration. After a thread has created Node, it starts NORMAL. After a subsequent node hangs up, it is set to SIGNAL. ABORTED if the current thread times out. Except that the thread corresponding to the subsequent node modifies the node owned by the current thread through CAS, the other changes are made by the current thread itself.

Why is CAS required when it was set directly before? It’s easy to understand after looking at the migration diagram. To prevent subsequent nodes from changing the ABORTED state to SIGNAL.

On this basis, the mechanism of guaranteeing arousal is analyzed.

Suppose the thread starts to solve the wake up problem after setting its node to ABORTED.

Let’s start with the simplest one, the top of the diagram. B has no subsequent nodes, so there must be no wake up problem. This corresponds to the code in the ABORT method when node == tail. Then CAS sets the next pointer to the front node to null. The return value of CAS is not checked here. A CAS failure means that a new node is enqueued after the CAS tail pointer. On the other hand, CAS must be used to prevent the processing of B from overwriting the Settings of the new node.

Next, analyze the case where B is the first candidate node, which is the middle part of the figure above. To enable USER A to wake up, user B sets user A’s status to SIGNAL. When B abort, is A’s status SIGNAL or NORMAL? The answer is probably both. In the tryLock code, when the first time to check whether nanos is less than 0, there is A possibility that the timeout time is too short. If it is directly judged as timeout, A’s status is NORMAL. Alternatively, after setting A’s status to SIGNAL, the thread is interrupted. If the thread park is interrupted or the wait times out, the status of A is SIGNAL.

You may be asking if you can make the analysis easier by setting A’s state to SIGNAL all the time. The answer is, that’s probably not a good idea. Because why should A be responsible for waking UP B when IT is obvious that B has timed out too short to acquire the lock and has given up? Multiple awakenings are less of a problem than forgetting to wake up, but as a synchronizer you want to wake up as accurately as possible.

Let’s go ahead and analyze C.

  1. After B gives up, C tries to let B wake up. CAS fails and B finds that B has given up when it reads B’s status again. In this case, C will try to set A’s status over B and park after A’s status is set as SIGNAL
  2. C tries to wake B up before B gives up
    1. The status of B is ABORTED when read again, and then the same as that of 1
    2. If B reads THE status of B again, it will SIGNAL, that is, B has not given up, and C will park

In other words, when B and C are running at the same time, C may park without knowing that B gives up

On the other hand for A

  1. B has to give up
    1. If USER B does not set the status of USER A to SIGNAL, user A does not wake up user B and the wakeup is lost
    2. User B sets the status of USER A to SIGNAL, and user A skips USER B and wakes up user C
    3. Although B is not set, C sets THE status of A as SIGNAL. A skips B and wakes up C when searching for subsequent nodes
  2. B did not give up
    1. If USER B does not set the status of USER A to SIGNAL, user A does not wake up user B and the wakeup is lost
    2. User B sets the status of USER A to SIGNAL, and user A wakes up user B, but the wakeup is lost

In addition, if A is in unlock mode, C is unlocked in fair mode (there is no C node in unfair mode) and advances the head node (that is, C skips B and head becomes C), A mismatched wake may wake the node after C.

As can be seen, when B is the first candidate node, C may not be able to set A SIGNAL, and A may not be able to wake up C in several ways. So AQS ‘strategy is to always let B wake UP C. UnfairTimedLock1 basically refers to AQS, but the above analysis is all out of my own consideration.

The specific code in the abort method determines whether P (predecessor) is head, and if it is, it wakes up regardless of its state.

Incidentally, in a timed lock using the spin strategy, C has full power to skip the abandoned B, eliminating the above analysis.

Finally, there is the case where B is not the first candidate and is not at tail, which is the bottom part of the figure above. Intuitively, since it is not the first candidate node, A wants to wake up D, and C will also skip B and link to D, so the problem of wake loss is unlikely to occur. The key here is to make sure that D’s status is set to SIGNAL. Could D’s status be NORMAL?

After looking at the previous analysis of the middle section, it’s possible, as follows

  1. C park before B gives up (Thread C analysis 2.2)
  2. B If the timeout period is too short, it is abandoned

At this time requirement for B ensure D status of SIGNAL, so there will be a abort ensureSignalSuccessorStatus in this call.

Is it ok to simply set D’s status to SIGNAL?

The answer is: In addition to setting D’s status, make sure that D does not unlock. Suppose ensureSignalSuccessorStatus success, D may happen?

  1. D before ensureSignalSuccessorStatus calls for the lock and release the lock, C D won’t wake up at this time, wake up is missing
  2. D after ensureSignalSuccessorStatus calls to release the lock (access can be in before or after), D will wake C at this time
  3. D give up before ensureSignalSuccessorStatus calls, ensureSignalSuccessorStatus fail, C need A to awaken
  4. D after ensureSignalSuccessorStatus call to give up, because the D is the first candidate nodes, according to the instructions in the middle of the above, D will unconditionally awakens the subsequent nodes, namely, C

Thread == null; thread == null; thread == null; You can also add status. 3 because ensureSignalSuccessorStatus fail, don’t need to increase the judgment.

The above analysis corresponding to abort the final code, if ensureSignalSuccessorStatus success and thread! If = null, it can be considered that there is no problem of wakeup loss. In other cases, 1 and 3 need to wake up C unconditionally. In case 3, it is possible that A awakens C and B awakens C, but considering that A may wake D, B still needs to confirm that C is awakened.

If the condition is successful, B can select help AND D can set the next pointer to C. If CAS fails, there is no impact.

This concludes the most complex analysis of ABORT. The actual AQS code considers the middle and bottom of the diagram together.

To be honest, I think the wake analysis of ABORT is really complicated. Uniformly wake up is probably the simplest code, but AQS analyzes part of it and uses exclusion to solve part of the situation that does not need to wake up. Understanding this code is probably the most critical part of the design of UnfairTimedLock1.

Abort also has some code in it, such as setting Thread to NULL initially. Personally, I understand that this is to reduce the number of abandoned threads being used to wake up extra, such as when being the first candidate node. In the above analysis, status is basically used to determine whether the node is aborted. Thread == NULL is not used to determine whether the node is abort. Of course, if MY analysis is wrong, feel free to point it out.

Abort has a code that skips abandoned nodes before setting status to ABORTED. I understand that status is set before to prevent subsequent nodes from skipping their own analysis. It is theoretically possible to place it after setting status, or even skip it without causing too much of a problem.

24. With reference to skipping a abandoned node, Queue# skipabortedtoraise does not set its own previous pointer after finding the first unabandoned node, but sets it every time it traverses the trail. My understanding is that I want to do the helping in multi-threading, that is, when A is skip, the node behind A can find the node that has not been given up faster through A when SKIP is skip. It is only given that the skipabortedToraise node (including the abort method) has not yet set its status to ABORTED, and the subsequent nodes of A theoretically stop when they traverse A. Even if A skip abort immediately after, subsequent nodes do not need this helping. So I have reservations about this one (unless AQS ‘cancelAcquire method was originally skip after status was set). At least not in today’s ReentrantLock.

UnfairTimedLock2

After writing a time-limited version of tryLock, the other lock methods copy the final version.

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
 
@SuppressWarnings("Duplicates")
public class UnfairTimedLock2 implements Lock {
 
    private final Queue queue = new Queue();
    private final AtomicInteger reentrantTimes = new AtomicInteger(0);
    private Thread owner;
 
    public void lock() {
        if (tryLock()) {
            return;
        }
        Node node = new Node(Thread.currentThread());
        queue.enqueue(node);
        while (true) {
            if (queue.isNextCandidate(node) &&
                    reentrantTimes.get() == 0 && reentrantTimes.compareAndSet(0, 1)) {
                myTurn(node);
                return;
            }
            if (isReadyToPark(node)) {
                LockSupport.park(this);
            }
        }
    }
 
    public void lockInterruptibly() throws InterruptedException {
        if (tryLock()) {
            return;
        }
        Node node = new Node(Thread.currentThread());
        queue.enqueue(node);
        while (true) {
            if (queue.isNextCandidate(node) &&
                    reentrantTimes.get() == 0 && reentrantTimes.compareAndSet(0, 1)) {
                myTurn(node);
                return;
            }
            if (isReadyToPark(node)) {
                LockSupport.park(this);
            }
            if (Thread.interrupted()) {
                abort(node);
                throw new InterruptedException();
            }
        }
    }
 
    public boolean tryLock() {
        if (owner == Thread.currentThread()) {
            // reentrant
            reentrantTimes.incrementAndGet();
            return true;
        }
        if (reentrantTimes.get() == 0 && reentrantTimes.compareAndSet(0, 1)) {
            owner = Thread.currentThread();
            return true;
        }
        return false;
    }
 
    public boolean tryLock(long time, @Nonnull TimeUnit unit) throws InterruptedException {
        if (tryLock()) {
            return true;
        }
        final long deadline = unit.toNanos(time) + System.nanoTime();
        Node node = new Node(Thread.currentThread());
        queue.enqueue(node);
        long nanos;
        while (true) {
            if (queue.isNextCandidate(node) &&
                    reentrantTimes.get() == 0 && reentrantTimes.compareAndSet(0, 1)) {
                myTurn(node);
                return true;
            }
            nanos = deadline - System.nanoTime();
            // timeout
            if (nanos <= 0L) {
                abort(node);
                return false;
            }
            if (isReadyToPark(node)) {
                LockSupport.parkNanos(this, nanos);
            }
            if (Thread.interrupted()) {
                abort(node);
                throw new InterruptedException();
            }
        }
    }
 
    private boolean isReadyToPark(@Nonnull Node node) {
        Node p = node.predecessor.get();
        int s = p.status.get();
        if (s == Node.STATUS_SIGNAL_SUCCESSOR) {
            return true;
        }
        if (s == Node.STATUS_ABORTED) {
            p = queue.skipAbortedPredecessors(node);
            p.successor.set(node);
        } else if (s == Node.STATUS_NORMAL) {
            p.status.compareAndSet(Node.STATUS_NORMAL, Node.STATUS_SIGNAL_SUCCESSOR);
        }
        return false;
    }
 
    private void abort(@Nonnull Node node) {
        node.clearThread();
 
        Node p = queue.skipAbortedPredecessors(node);
        Node ps = p.successor.get();
 
        // linearization point
        node.status.set(Node.STATUS_ABORTED);
 
        if (queue.tail.get() == node && queue.tail.compareAndSet(node, p)) {
            p.successor.compareAndSet(ps, null);
            return;
        }
 
        if (p != queue.head.get() && p.ensureSignalSuccessorStatus() && p.thread.get() != null) {
            Node s = node.successor.get();
            if (s != null && s.status.get() != Node.STATUS_ABORTED) {
                p.successor.compareAndSet(ps, s);
            }
        } else {
            signalNormalSuccessor(node);
        }
    }
 
    private void myTurn(@Nonnull Node node) {
        owner = Thread.currentThread();
        node.clearThread();
        queue.head.set(node);
        reentrantTimes.set(1);
        
        Node predecessor = node.predecessor.get();
        node.predecessor.set(null);
        predecessor.successor.set(null);
    }
 
    private void signalNormalSuccessor(@Nonnull Node node) {
        if (node.status.get() == Node.STATUS_SIGNAL_SUCCESSOR) {
            node.status.compareAndSet(Node.STATUS_SIGNAL_SUCCESSOR, Node.STATUS_NORMAL);
        }
 
        Node successor = queue.findNormalSuccessor(node);
        if (successor != null) {
            LockSupport.unpark(successor.thread.get());
        }
    }
 
    public void unlock() {
        if (owner != Thread.currentThread()) {
            throw new IllegalStateException("not the thread holding lock");
        }
        int rt = reentrantTimes.get();
        if (rt < 1) {
            throw new IllegalStateException("reentrant times < 1 when try to unlock");
        }
        if (rt > 1) {
            reentrantTimes.set(rt - 1);
            return;
        }
        // rt == 1
        owner = null;
        // linearization point
        reentrantTimes.set(0);
 
        Node node = queue.head.get();
        if (node != null && node.status.get() == Node.STATUS_SIGNAL_SUCCESSOR) {
            signalNormalSuccessor(node);
        }
    }
 
    @Override
    @Nonnull
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }
 
    @SuppressWarnings("Duplicates")
    private static class Queue {
        final AtomicReference<Node> head = new AtomicReference<>();
        final AtomicReference<Node> tail = new AtomicReference<>();
 
        /**
         * Enqueue node.
         *
         * @param node new node
         * @return predecessor
         */
        @Nonnull
        Node enqueue(@Nonnull Node node) {
            Node t;
            while (true) {
                t = tail.get();
                if (t == null) {
                    // lazy initialization
                    Node sentinel = new Node();
                    if (head.get() == null && head.compareAndSet(null, sentinel)) {
                        tail.set(sentinel);
                    }
                } else {
                    node.predecessor.lazySet(t);
                    // linearization point
                    if (tail.compareAndSet(t, node)) {
                        t.successor.set(node);
                        return t;
                    }
                }
            }
        }
 
        @Nullable
        Node findNormalSuccessor(@Nonnull Node node) {
            Node n = node.successor.get();
            if (n != null && n.status.get() != Node.STATUS_ABORTED) {
                return n;
            }
 
            // find normal node from tail
            Node c = tail.get();
            n = null;
            // tail maybe null during lazy initialization
            while (c != null && c != node) {
                if (c.status.get() != Node.STATUS_ABORTED) {
                    n = c;
                }
                c = c.predecessor.get();
            }
            return n;
        }
 
        boolean isNextCandidate(@Nonnull Node node) {
            return node.predecessor.get() == head.get();
        }
 
        @Nonnull
        Node skipAbortedPredecessors(@Nonnull Node node) {
            Node h = head.get();
            Node p = node.predecessor.get();
            while (p != h && p.status.get() != Node.STATUS_ABORTED) {
                p = p.predecessor.get();
                /*
                 * set predecessor every time to help successors of
                 * current node to find the normal predecessor more quickly
                 */
                node.predecessor.set(p);
            }
            return p;
        }
    }
 
    /**
     * Node.
     * <p>
     * Status change:
     * NORMAL -> ABORTED
     */
    private static class Node {
        static final int STATUS_NORMAL = 0;
        static final int STATUS_ABORTED = -1;
        static final int STATUS_SIGNAL_SUCCESSOR = 1;
 
        /**
         * thread will be null if
         * 1. abort
         * 2. enter mutual exclusion area
         */
        final AtomicReference<Thread> thread;
        final AtomicInteger status = new AtomicInteger(STATUS_NORMAL);
        final AtomicReference<Node> predecessor = new AtomicReference<>();
        // optimization
        final AtomicReference<Node> successor = new AtomicReference<>();
 
        Node() {
            this(null);
        }
 
        Node(@Nullable Thread thread) {
            this.thread = new AtomicReference<>(thread);
        }
 
        boolean ensureSignalSuccessorStatus() {
            int s = this.status.get();
            return s == STATUS_SIGNAL_SUCCESSOR ||
                    (s == STATUS_NORMAL && this.status.compareAndSet(s, STATUS_SIGNAL_SUCCESSOR));
        }
 
        void clearThread() {
            thread.set(null);
        }
    }
}Copy the code

If you have seen AQS source code, you may notice the tryLock and tryAcquire AQS, isReadyToPark and shouldParkAfterFailedAcquire, abort and cancelAcquire close to. You can compare them if you’re interested.

summary

A time-limited lock is one that I personally consider difficult to implement, but can be useful in places such as deadlocks. If you understand the analysis of timed locks, you can write locks similar to ReentrantLock.

If you want to learn more about locking implementation, you are advised to read the section on locking in The Art of Multiprocessor Programming. Although much of it is spin lock, the actual park code is far from concrete, but it is very helpful for training multithreaded code analysis and design.

The next personal implementation is ReadWriteLock, which is explained in Article 3.