Fair locking process

ReentrantLock can be a fair lock or an unfair lock. Here we discuss the process of fair lock

public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock(true);
    reentrantLock.lock();
}
Copy the code

When we generate a ReentrantLock using an argument builder, an inner class object with a fair lock is generated because the pass is true.

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
Copy the code

Whether fair or not fair lock all inherited already in Sync the static inner class object, the object and inherited AbstractQueuedSynchronizer (hereinafter referred to as the AQS), so to analyze already locked principle, To analyze the abstract class AbstractQueuedSynchronizer part of the method. Now let’s analyze the implementation line by line, starting with the method calls.

When we call reentrantLock.lock, we actually call the lock method for the specific lock, which of course means fair lock. This method is the method that the abstract class AQS needs the concrete class to implement.

public void lock(a) {
    sync.lock();
}
Copy the code

The acquire method is then called by the lock method and passes an int (1), the meaning of which is discussed below

  final void lock(a) {
    acquire(1);
}
Copy the code

The acquire method in AQS is then called.

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
public final void acquire(int arg) {
    / / (1)
    if(! tryAcquire(arg) &&/ / (2)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        / / (3)
        selfInterrupt();
}
Copy the code

Let’s take a quick look at class annotations:

// Get the lock exclusively and ignore interrupts.
This means that this method does not respond to the thread's interrupt. If the thread is woken up by the interrupt while blocking, the blocking state will be restored.
/*Acquires in exclusive mode, ignoring interrupts.*/
Copy the code
// This method calls the tryAcquire method at least once, and if it returns success, the method ends without further execution. Otherwise, the thread is queued (queued).
The tryAcquire method attempts to acquire the lock, and if it succeeds, the current thread continues. Otherwise the thread is queued.
Implemented by invoking at least once {@link #tryAcquire}, returning on success.Otherwise the thread is queued*/
Copy the code
// It is possible to block and unblock repeatedly (choke-interrupt-blocking) until it succeeds.
/* possibly repeatedly blocking and unblocking, invoking {@link #tryAcquire} until success*/
Copy the code

Code (1) calls the tryAcquire(ARG) method to obtain the lock. If the lock is successfully obtained, it will be returned directly without subsequent operations due to non-operation.

AcquireQueued (addWaiter(Node.exclusive), ARG)) if the lock fails to be acquired and true is returned, the acquireQueued(addWaiter(Node.exclusive), ARG) method is executed and the current thread is queued according to the comment.

Finally if the two conditions are returns true, it is will interrupt selfInterrupt (), in the process of normal lock, actually both return true not happen, here is already. LockInterruptibly code reuse.

Summary: Different lock methods are called for reentrantLock depending on the type of lock generated by the constructor. If the lock is successfully acquired, the method returns and the thread continues, or if the lock fails to be acquired, the thread is queued. (However, if the thread does not acquire the lock, it is not as simple as queuing directly.)

TryAcquire method analysis.

Now let’s analyze (1), which is the tryAcauire method. In AQS, this method is abstract and requires concrete implementation. Here is the tryAcquire method of fair lock.

/** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */
protected final boolean tryAcquire(int acquires) {
  / / (1.1)
    final Thread current = Thread.currentThread();
  / / (1.2)
    int c = getState();
  / / (1.3)
    if (c == 0) {
        / / (1.4)
        if(! hasQueuedPredecessors() &&/ / (1.5)
            compareAndSetState(0, acquires)) {
          / / (1.6)
            setExclusiveOwnerThread(current);
            return true; }}/ / (1.7)
    else if (current == getExclusiveOwnerThread()) {
        / / (1.8)
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        / / (1.9)
        setState(nextc);
        return true;
    }
    return false;
}
Copy the code

Now let’s analyze it line by line (1.1) to get the current thread (1.2) getState() is a method in AQS that returns the number of times the thread that acquired the lock reentered (analyzed below). Because state is declared by volatile, visibility is guaranteed.

/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState(a) {
    return state;
}
Copy the code
/** * The synchronization state. */
private volatile int state;
Copy the code

(1.3) To check whether the thread state is 0, we assume that there is only one thread T1 calling the lock method, since the state is int, the default value is 0, so c == 0 returns true, enter the first if code block. Hasqueuedtoraise (), determines whether the queue is initialized and returns directly after the operation. If the queue is not initialized, execution continues.

/** * Queries whether any threads have been waiting to acquire longer * than the current thread. * * <p>An invocation of  this method is equivalent to (but may be * more efficient than): * <pre> {@code* getFirstQueuedThread() ! = Thread.currentThread() && * hasQueuedThreads()}</pre> * * <p>Note that because cancellations due to interrupts and * timeouts may occur at any time, a {@code true} return does not
 * guarantee that some other thread will acquire before the current
 * thread.  Likewise, it is possible for another thread to win a
 * race to enqueue after this method has returned {@code false},
 * due to the queue being empty.
 *
 * <p>This method is designed to be used by a fair synchronizer to
 * avoid <a href="AbstractQueuedSynchronizer#barging">barging</a>.
 * Such a synchronizer's {@link #tryAcquire} method should return
 * {@code false}, and its {@link #tryAcquireShared} method should
 * return a negative value, if this method returns {@code true}
 * (unless this is a reentrant acquire).  For example, the {@code
 * tryAcquire} method for a fair, reentrant, exclusive mode
 * synchronizer might look like this:
 *
 *  <pre> {@code
 * protected boolean tryAcquire(int arg) {
 *   if (isHeldExclusively()) {
 *     // A reentrant acquire; increment hold count
 *     return true;
 *   } else if (hasQueuedPredecessors()) {
 *     return false;
 *   } else {
 *     // try to acquire normally
 *   }
 * }}</pre>
 *
 * @return {@code true} if there is a queued thread preceding the
 *         current thread, and {@code false} if the current thread
 *         is at the head of the queue or the queue is empty
 * @since1.7 * /
public final boolean hasQueuedPredecessors(a) {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    returnh ! = t && ((s = h.next) ==null|| s.thread ! = Thread.currentThread()); }Copy the code

Briefly analyze the comments

// Check if any threads are waiting longer than the current thread.
// We are talking about fair locks here, so follow the first-in, first-out principle. It makes sense that the thread that comes first will acquire the lock
/*Queries whether any threads have been waiting to acquire longer than the current thread. */
Copy the code
// Calling this method is equivalent to calling getFirstQueuedThread()! = Thread.currentThread() && hasQueuedThreads()
// The first thread in the queue is the current thread, and the current queue is initialized, but the code written this way is efficient
/* * 

An invocation of this method is equivalent to (but may be * more efficient than): *

 {@code * getFirstQueuedThread() ! = Thread.currentThread() && * hasQueuedThreads()}

*/
Copy the code

Since interrupts and timeouts can occur at any time, there is no guarantee that another thread will acquire the lock before the current thread. When the queue is empty, there is no guarantee that the current thread will return the empty thread before the other thread.
// This code does not have a synchronization mechanism, so it is possible for both threads to return an empty column. However, the CAS operation in subsequent code ensures that only one holds the lock and initializes the queue.
/* * 

Note that because cancellations due to interrupts and * timeouts may occur at any time, a {@code true} return does not * guarantee that some other thread will acquire before the current * thread. Likewise, it is possible for another thread to win a * race to enqueue after this method has returned {@code false}, * due to the queue being empty. */

Copy the code

In short, this method is used to determine whether the queue is empty, and if not, the current thread is the first thread in the queue header. Now let’s analyze the code

        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
Copy the code

This is a simple assignment to get the first and last nodes of the queue, which is a bidirectional queue. A Node is a queued Node, each Node has a precursor and a successor, and holds the queued thread and the state of the queued thread.

static final class Node{
    volatile Node prev;/ / precursor
    volatile Node next;/ / the subsequent
    volatile Thread thread;/ / thread
    volatile int waitStatus;// The state of the thread
}
Copy the code

In AQS, head and tail are not initialized, so they are both null

    private transient volatile Node head;
    private transient volatile Node tail;
Copy the code

Now we assume that only one thread T1 enters the method, so for the first judgment tail! If head fails, this method returns false. The queue is not initialized. Now we return to (1.4) of tryAcquire. Since we have only one thread, (1.4) returns false.

Acquires = 1; acquires = 1; acquires = 1; acquires = 1; 24. Since hasqueuedToraise () is not a synchronous method to determine whether a queue is empty, multiple threads may change the state of state after executing code (1.4) believing it to be empty. At this point, the author uses the CAS operation in (1.5). This ensures that only one thread can successfully change the state of state.

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
Copy the code

The CAS operation in the Unsafe class is invoked here. The CAS operation principle can be understood by yourself. (1.6) Assign the current thread to the ‘exclusiveOwnerThread’ variable, which identifies the current thread that has acquired the lock.

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

The final method returns true. Summary: The tryAcquire method in the fair lock will first obtain the state of the current thread. If the state is 0 (0 is the initial state), it will determine whether the current queue is initialized or whether the first thread of the current queue is the current thread. If so, it attempts to change the state of state, marked as 1, to get the lock reentrant, and to record the current thread, returning true. Otherwise return false without attempting to change the state of state.

public final void acquire(int arg) {
    / / (1)
    if(! tryAcquire(arg) &&/ / (2)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        / / (3)
        selfInterrupt();
}
Copy the code

As can be seen in the flowchart, we only analyze the case where state == 0. Now let’s analyze state again! Theta is equal to 0.

    / / (1.7)
    else if (current == getExclusiveOwnerThread()) {
        / / (1.8)
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        / / (1.9)
        setState(nextc);
        return true;
    }
Copy the code

The getExclusiveOwnerThread() method is used to obtain the current thread that has acquired the lock. In (1.6) we have saved the current thread that has acquired the lock.

protected final Thread getExclusiveOwnerThread(a) {
    return exclusiveOwnerThread;
}
Copy the code

If not, tryAcquire(ARG) simply returns false. If so, update the state. So how do you update? Acquires (1); nexTC (state++); nexTC (state++) Under normal circumstances, nexTC is not less than 0, so we will not discuss it here. Finally, save the value of state and return true to indicate that the lock was acquired successfully.

ReentrantLock reentrantLock reentrantLock reentrantLock reentrantLock reentrantLock reentrantLock reentrantLock reentrantLock

public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock(true);
    for(int i = 0; i <= Integer.MAX_VALUE;i++){
        reentrantLock.lock();
    }
}
// This code raises the Maximum Lock count exceeded exception, indicating that the number of reentrants is limited
Copy the code

At this point tryAcquire analysis is complete and we can conclude that reentrantLock.lock does not initialize the queue (because tail and head are both null) and does not affect the performance of the code by locking (no operating system methods are called) if the threads are executing alternately.

AcquireQueued (addWaiter(Node.exclusive), ARG) method analysis

If the lock acquisition fails, this method is called to get the queue, so let’s examine addWaiter first.

/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
private Node addWaiter(Node mode) {
    / / (2.1)
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    / / (2.2)
    Node pred = tail;
    / / (2.3)
    if(pred ! =null) {
        / / (2.4)
        node.prev = pred;
        / / (2.5)
        if (compareAndSetTail(pred, node)) {
            / / (2.6)
            pred.next = node;
            returnnode; }}/ / (2.7)
    enq(node);
    return node;
}
Copy the code

This code looks complicated at first glance, but it’s actually a new insert operation for a bidirectional list. In code (2.1), we add a Node Node and pass in the current thread and mode. Static variables are defined in the Node class to identify these modes.

    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */
    static final int PROPAGATE = -3;
Copy the code

This mode is passed in node. EXCLUSIVE (null). Do not confuse state with this mode. State in AQS is the number of times the current thread reenters, starting with 0.

Now suppose that two threads successively acquired the lock. When T1 thread came, it found that the queue was empty through tryAcquire, so CAS successfully acquired the lock and returned to execute, but failed to release the lock after a long time of execution. At this time, T2 thread also acquired the lock, since state == 0 is not valid, it directly returned false. AddWaiter is called first to create a Node.

When both tail and head are initially empty, we create a new node T2, (2.2) and assign tail to prev. Since tail is not initialized to empty, prev is also empty, so execute (2.7).

This method inserts the new node into the queue and initializes the queue if it is not initialized. Where the parameter node passed in is the T2 node.

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
private Node enq(final Node node) {
    for (;;) {
        / / (2.7.1)
        Node t = tail;
        / / (2.7.2)
        if (t == null) { // Must initialize
            / / (2.7.3)
            if (compareAndSetHead(new Node()))
                / / (2.7.4)
                tail = head;
        } else {
            / / (2.7.5)
            node.prev = t;
            / / (2.7.6)
            if (compareAndSetTail(t, node)) {
                / / (2.7.7)
                t.next = node;
                returnt; }}}}Copy the code

In the first for loop tail is empty, so t = null. (2.7.1) We assign tail to the temporary node t. Tail is empty and meets the if condition, so we initialize the queue. Note that in (2.7.3) this is also a CAS operation, new an empty new node to the head (not T2, which we created with the new thread), and then point the tail to the head. In the second for loop, the last node is first assigned to t (2.7.1), then node T is not null and is an empty new node, so the condition of (2.7.2) is not met, and the node jumps to (2.7.5). The empty node T is used as the precursor of T2 node (2.7.6), and then T2 is tried as the tail. (2.7.7) The successor of the last empty node points to T2. Simply insert the newly created node to the end of the queue. ** finally returns t, that is, the empty node is returned.

If the CAS assignment fails, the loop will ensure that the node of the current thread is successfully added to the queue. Why create an empty node in the first place, we can briefly refer to the thread that acquired the lock, namely T1. We return to the code (2.7) and find that the return value of t is not used. Instead, we return the node we just created, which is the last node of the queue.

Section: The addWaiter method will connect the newly created node to the end of the queue. If the queue is not initialized, the queue will be initialized first. Note that the head node of the queue is an empty node.

So if it’s (2.3)pred! = null? Let’s assume again that there is now another thread T3,T1 has acquired the lock and is running, and T2 has initialized the completion queue and is queuing at the end of the queue. The queue size =2 and the head node is an empty node. Pred = tail T2,pred! = null enter (2.4) to point the precursor of T3 to T2 in an attempt to set T3 as the tail node. So you add T3 to the tail, if you have T4,T5… Same thing.

Now let’s analyze the acquireQueued(Final Node Node, int arg) method

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            / / (2.8)
            final Node p = node.predecessor();
            / / (2.9)
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            / / (2.10)
            if (shouldParkAfterFailedAcquire(p, node) &&
                / / (2.11)
                parkAndCheckInterrupt())
                / / (2.12)
                interrupted = true; }}finally {
        if(failed) cancelAcquire(node); }}Copy the code

Here too is an infinite loop, with code (2.8) calling fossil Skull (). This method returns the precursor of the current node. Again, let’s assume there are only two threads, T1,T2, and node refers to the T2 thread. Here we return the precursor of T2. So p= empty node (as analyzed above, the head node of the queue is an empty node).

final Node predecessor(a) throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}
Copy the code

(2.9)p is not the head node, obviously it is, and tryAcquire is executed again. What that means is that T2 spins once here, trying to get the lock, because maybe T1 would have released the lock when T2 created the queue. If p == head is successful, then tryAcquire can be executed, because only the first thread in the queue is eligible to attempt to acquire the lock, and the next thread is not required to do so.

If the lock was successfully acquired, return false,T2 acquires the lock and continues, or if the spin fails, execute (2.10). This method is used to update the node state and determine whether the current thread should be blocked or suspended.

/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    / / (2.10.1)
    int ws = pred.waitStatus;
    / / (2.10.2)
    if (ws == Node.SIGNAL)
        /* * This node has already set status asking a release * to signal it, so it can safely park. */
        return true;
    / / (2.10.3)
    if (ws > 0) {
        /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */
        / / (2.10.4)
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */
        / / (2.10.5)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
Copy the code

(2.10.1) ws is used to obtain the waiting state of the current thread. We did not assign an initial value when we created the Node, so ws =0. Node.SIGNAL = -1, by the way, means that if the Node is in this state, its successors will or have been blocked (queued pending). That is, if WS = -1 for T1, then T2 will be blocked, or already blocked.

(2.10.5) Obviously ws = 0 does not meet the conditions to enter else,pred the node state, which is -1. The pred node is the empty node representing T1, which also indicates that T2 thread will enter the blocked pending state. Finally return false. Why doesn’t the CAS spin here, because the current thread CAS, if it fails, will return to the acquireQueued method loop and execute (2.10) again to change the state.

Back to the acquireQueue method,(2.10) returns false and the first loop ends.

(2.9) p==head returns true and spins again! If you still can’t get the lock, enter (2.10.1). At this point, ws has changed once, and returns true. ** So the first queued thread spins twice. ** Why not just spin once, or spin 3 times or more? Because it is expensive to block a thread, if the thread can not block queueing, it is better not to, so you need to give it one more chance, and Node state changes also need two loops. Others are welcome to discuss.

(2.11.1) Here the thread is blocked. The whole locking process ends until the thread is woken up again or interrupted. If the thread is interrupted, execute (2.11.2) to reset the interrupt state and return true, so (2.12) assigns the interrupt value to true and continues the loop without responding, blocking the thread again. So reentrantLock.lock does not respond to interrupts.

    private final boolean parkAndCheckInterrupt(a) {
        / / (2.11.1)
        LockSupport.park(this);
        / / (2.11.2)
        return Thread.interrupted();
    }
Copy the code

Fair lock summary:

  1. In a fair lock, if a thread acquires a lock, it first determines the state of the state in the ReentrantLock (which is also the number of times the lock has been re-entered), then checks if the lock is not held, and if not, attempts to acquire the lock. If the lock has been initialized, or the queue has been initialized, the current thread is queued. Refer to the flow chart above.
  2. The queue is initialized before the thread is queued, and the first node of the queue is empty, indicating the thread that has currently acquired the lock (not null).
  3. After a thread is enqueued, two spin attempts are made to acquire the lock if the thread is the first to queue (a thread that is not an empty node, which can be understood as T2 above).
  4. If a thread is interrupted while park is blocked, it does not throw an exception or otherwise respond. Instead, it resets the interrupted state and continues blocking.

Unfair locking locking process

ReenTrantLock is an unfair lock when created as follows

    ReentrantLock reentrantLock = new ReentrantLock();
    ReentrantLock reentrantLock1 = new ReentrantLock(false);
Copy the code

Now we call the reentrantLock. lock() method. The method now calls an unfair lock.

    public void lock(a) {
        sync.lock();
    }
Copy the code

We can start by reviewing the fair lock method, which displays between locks to determine whether the state is 0, that is, whether the current lock is available. If it is 0, it also determines whether the status of the queue is empty before it is eligible to attempt to acquire the lock. In an unfair lock, each thread will try to acquire the lock first, no matter what happens. This will result in the queuing thread being preempted by the incoming thread.

/** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */
final void lock(a) {
    / / (1)
    if (compareAndSetState(0.1))
        / / (2)
        setExclusiveOwnerThread(Thread.currentThread());
    else
        / / (3)
        acquire(1);
}
Copy the code

(1) CAS preempts the lock. If the preemption succeeds, the current thread will be recorded. If (3) fails, this is also the acquire method in AQS.

public final void acquire(int arg) {
    / / (3.1)
    if(! tryAcquire(arg) &&/ / (3.2)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
Copy the code

The tryAcquire method in the unfair lock is then called.

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
Copy the code

The nonfairTryAcquire(int Acquires) method in sync is then called. In a fair lock, the method is implemented directly. In an unfair lock, the method is written in the parent class.

    /** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            / / (3.1.1)
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true; }}else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
Copy the code

The tryAcaquire for a fair lock is almost the same, except that before executing (3.1.1) it determines whether the queue is already initialized or whether the rival thread is the current thread. All other code is the same, so there is no repetition here for the reader to understand.

The final queue action acquireQueued(addWaiter(Node.exclusive), ARG) is the same as a fair lock. Not to belabor too much.

Summary of unfair locking

  1. An unfair lock preempts the lock before it is added.
  2. After a non-fair lock fails to preempt the lock, the tryAcquire method will be executed just like a fair lock. However, after a non-fair lock tryAcquire finds that the current lock is available, it will not continue to determine whether the queue is initialized or whether the head is the current thread. Instead, it will directly try to acquire the lock and join the queue if the lock fails.