“This is the 24th day of my participation in the First Challenge 2022. For details: First Challenge 2022”

ReentrantLock

1.1 the characteristics of

ReentrantLock has the following features:

  • reentrant
  • interruptible
  • You can set the timeout period
  • Can be set to fair lock
  • Conditional variable support

The realization principle of its characteristics will be explained in detail later.

1.2 Code Structure

Its code structure is shown as follows:

There are three inner classes: Sync, FairSync, and NonfairSync.

Sync inherited from AbstractQueuedSynchronizer.

AbstractQueuedSynchronizer Node and ConditionObject of two inner classes.

The literal meaning of the above class should give you a sense of where the aforementioned features are implemented.

Two, principle analysis

2.1 reentrant

Reentrant means that if the same thread first acquires the lock, it has the right to acquire it again because it is the owner of the lock. If it is a non-reentrant lock, then the second attempt to acquire it will itself be blocked by the lock.

Synchronized is also reentrant.

Examples of reentrant applications are as follows:

public class Test {

    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        method1();
    }
    public static void method1(a) {
        lock.lock();
        try {
            System.out.println("method1");
            method2();
        } finally {
            lock.unlock();
            System.out.println("method1 unlock"); }}public static void method2(a) {
        lock.lock();
        try {
            System.out.println("method2");
            method3();
        } finally {
            lock.unlock();
            System.out.println("method2 unlock"); }}public static void method3(a) {
        lock.lock();
        try {
            System.out.println("method3");
        } finally {
            lock.unlock();
            System.out.println("method3 unlock"); }}}Copy the code

Results:

method1
method2
method3
method3 unlock
method2 unlock
method1 unlock
Copy the code

Note that lock.unlock() must be on the first line of the finally block.

  • Source code analysis

    Step by step, using the previous code:

    Acquiring a lock

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

    Sync’s lock method has two implementation classes, fair and unfair:

    An unfair lock is used here because NonfairLock is used by default when ReetrantLock is initialized:

        public ReentrantLock(a) {
            sync = new NonfairSync();
        }
    Copy the code

    To continue source tracking, the lock() method in an unfair lock:

            final void lock(a) {
                // Use the spin lock here to determine whether the current thread holds the lock, and replace the value with 1 if it is 0
                if (compareAndSetState(0.1))
                    // Set the lock to be exclusive to this thread
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    // The comparison is not valid, try to obtain the lock
                    acquire(1);
            }
    Copy the code

    When a thread reenters or another thread attempts to acquire the lock, acquire(1) : setExclusiveOwnerThread () : setExclusiveOwnerThread () : setExclusiveOwnerThread () : setExclusiveOwnerThread () : setExclusiveOwnerThread ()

        public final void acquire(int arg) {
            // Try to get the lock,
            if(! tryAcquire(arg) &&// With the short-circuit logic operator, when the fetch fails, the thread is added to the wait queue
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                // Interrupt the current thread, actually set the interrupt flag
                selfInterrupt();
        }
    Copy the code

    Let’s focus on tryAcquire and see how to implement lock reentrant. Skip the intermediate process and look directly at the following code:

            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                // Get the current thread synchronization status. State is volatile
                int c = getState();
                // Indicates that no thread holds the lock
                if (c == 0) {
                    // Spin lock, same process as before
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true; }}// The current thread is the thread holding the lock
                else if (current == getExclusiveOwnerThread()) {
                    // Add 1 to the current state
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    // Set the status value
                    setState(nextc);
                    return true;
                }
                return false;
            }
    Copy the code

    Unlock: Unlock unlock: Unlock unlock

            protected final boolean tryRelease(int releases) {
                // Get the current state minus 1
                int c = getState() - releases;
                if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
                boolean free = false;
                // The lock can be reentrant multiple times. The lock can only be released when the state is reduced to 0.
                if (c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }
    Copy the code

The principle of lock reentrant is fairly simple, so that’s it.

2.2 can disrupt

In addition to the regular lock() method, ReetrantLock provides a interruptible method, lockInterruptibly(), that will throw an exception if the thread holding the lock is interrupted while obtaining the lock:

public class InterruptTest {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("Start...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("An interruption in the process of waiting for a lock.");
                return;
            }
            try {
                System.out.println("Got the lock.");
            } finally{ lock.unlock(); }},"t1");
        lock.lock();
        System.out.println("Got the lock.");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
            System.out.println("Executive interruption");
        } finally{ lock.unlock(); }}}Copy the code

Results:

Got the lock start... In the process of implementing interrupt lock was broken. Java lang. InterruptedException ats java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at com.cloud.bssp.juc.reetrantlock.InterruptTest.lambda$main$0(InterruptTest.java:19) at java.lang.Thread.run(Thread.java:748)Copy the code

If the lock() method is used, even if the thread is interrupted, the lock can still be acquired and no exceptions will be thrown.

2.3 You can set the timeout period

ReetrantLock provides two methods to acquire the lock and return quickly, without waiting forever and returning immediately regardless of success or failure:

  • TryLock () When the lock is not held, the tryLock() method will immediately acquire the lock even if it is a fair lock, which is unfair but useful.

  • TryLock (long timeout, TimeUnit Unit) allows you to set a timeout period. Unlike tryLock, this method attempts to acquire the lock at the end of the timeout period. If it succeeds, it holds the lock and returns immediately. Cannot acquire a lock, in contrast to tryLock.

TryLock () test:

public class TryLockTest {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            if(! lock.tryLock()) { System.out.println("Lock acquisition failed");
                return;
            } else {
                try {
                    System.out.println("Lock obtained successfully");
                } finally{ lock.unlock(); }}}); lock.lock();try {
            t1.start();
            TimeUnit.SECONDS.sleep(1);
        } finally{ lock.unlock(); }}}Copy the code

Results:

Failed to obtain the lockCopy the code

Timed tryLock() looks like this:

public class TryLockTimeTest {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            try {
                if(! lock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println("Waiting one second to acquire lock failed");
                    return;
                } else {
                    try {
                        System.out.println("Wait one second to obtain lock success");
                    } finally{ lock.unlock(); }}}catch(InterruptedException e) { e.printStackTrace(); }}); lock.lock();try {
            t1.start();
            System.out.println("Wait two seconds.");
            TimeUnit.SECONDS.sleep(2);
        } finally{ lock.unlock(); }}}Copy the code

Results:

Wait two seconds Wait one second failed to obtain the lockCopy the code

2.4 Setting a Fair lock

As mentioned earlier, ReentrantLock is unfair by default. Unfair is used because fair locks are generally unnecessary and reduce concurrency.

Create a fair lock as follows:

ReentrantLock lock = new ReentrantLock(true);
Copy the code

Tracker constructor:

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

Focus on its fair lock implementation:

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock(a) {
            // Inherit from AQS method, internal first call tryAcquire to obtain the lock, if the acquisition fails, add downtown to wait queue
            acquire(1);
        }

        /** * Fair lock version of tryAcquire */
        protected final boolean tryAcquire(int acquires) {
            // Get the current thread
            final Thread current = Thread.currentThread();
            // Get the lock status
            int c = getState();
            // 0 indicates that the lock is not held
            if (c == 0) {
                // Check whether there are nodes waiting in the current wait queue
                if(! hasQueuedPredecessors() &&// Compare and replace states
                    compareAndSetState(0, acquires)) {
                    // Set the current thread to an exclusive thread
                    setExclusiveOwnerThread(current);
                    return true; }}else if (current == getExclusiveOwnerThread()) {
                // Lock reentrant
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false; }}Copy the code

2.5 Conditional Variables

ReentrantLock supports multiple condition variables.

What to make of the above sentence? When we learned synchronized, we introduced its wait method. When a thread calls its wait method, it changes from the owner of the thread to the wait state and is added to the WaitSet of Monitor. When other threads call wait again, it is still added to the wait state. It’s like a common room.

ReentrantLock’s multiple condition variables are like multiple lounges.

ReentrantLock returns conditional variables using await()/signal() methods and conditionObject queues.

public class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();

    static Condition Tom = lock.newCondition();

    static Condition Jerry = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {

            try {
                lock.lock();
                Tom.await();
                System.out.println("Eat the fish.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {

            try {
                lock.lock();
                Jerry.await();
                System.out.println("Got the cheese.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        TimeUnit.SECONDS.sleep(1);

        try {
            lock.lock();
            System.out.println("Here comes the fish.");
            Tom.signal();
        } finally {
            lock.unlock();
        }

        TimeUnit.SECONDS.sleep(1);
        try {
            lock.lock();
            System.out.println("Here comes the cheese");
            Jerry.signal();
        } finally{ lock.unlock(); }}}Copy the code

Results:

Fish came, fish came, cheese came, cheese cameCopy the code

As shown above, there are several key points:

  • Need to get lock before await
  • The lock is released after await
  • Call signal to wake up the thread, but also need to acquire the lock, otherwise an error will be reported. The awakened thread races again and continues execution from behind await.
  • Remember unlock.

Now let’s focus on how to do that? Only the key methods are covered

Await method:

        public final void await(a) throws InterruptedException {
            // If the thread status is interrupted, an exception is thrown
            if (Thread.interrupted())
                throw new InterruptedException();
            // Add the current thread to the conditional wait queue
            Node node = addConditionWaiter();
            // Release the lock
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // When the node is not in a synchronous wait queue
            while(! isOnSyncQueue(node)) {// Block the current thread
                LockSupport.park(this);
                if((interruptMode = checkInterruptWhileWaiting(node)) ! =0)
                    break;
            }
            // Get the wait queue lock without throwing an interrupt exception
            if(acquireQueued(node, savedState) && interruptMode ! = THROW_IE)// Reset the interrupt flag
                interruptMode = REINTERRUPT;
            // Clear the cancelled node
            if(node.nextWaiter ! =null) 
                unlinkCancelledWaiters();
            // If the interrupt mode is not 0, the state determines whether to throw an exception, interrupt the thread or do nothing at all
            if(interruptMode ! =0)
                reportInterruptAfterWait(interruptMode);
        }
Copy the code

AddConditionWaiter method:

        private Node addConditionWaiter(a) {
            // The last person waiting in conditionObject
            Node t = lastWaiter;
            // If the last wait is cancelled, clear it (not null, and the state is not waiting)
            if(t ! =null&& t.waitStatus ! = Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; }// The node that sets the current thread to wait
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
Copy the code

Method of signal:

        public final void signal(a) {
            // Determine if the thread holds the lock, and raise an exception if it does not
            if(! isHeldExclusively())throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            // If the first wait of the conditional queue is not null, signal wake up is performed
            if(first ! =null)
                doSignal(first);
        }
Copy the code

DoSignal method

        private void doSignal(Node first) {
            do{if the next wait for the first node isnull
                if ( (firstWaiter = first.nextWaiter) = =null)
                     // The last person waiting for the conditional queue is set to NULL
                    lastWaiter = null;
                first.nextWaiter = null;
            } while(! transferForSignal(first) && (first = firstWaiter) ! =null);
        }
Copy the code

TransferForSignal method

    final boolean transferForSignal(Node node) {
        // Check whether the node status is condition, if it is 0, otherwise return false
        if(! compareAndSetWaitStatus(node, Node.CONDITION,0))
            return false;

        /* * Add the node to the synchronous wait queue */
        Node p = enq(node);
        int ws = p.waitStatus;
        // Here the wait state is 0, compare and replace the status with SIGNAL
        if (ws > 0| |! compareAndSetWaitStatus(p, ws, Node.SIGNAL))// Unblock the thread
            LockSupport.unpark(node.thread);
        return true;
    }
Copy the code

About ReentrantLock briefly introduced these, in fact, should first learn AQS, otherwise may not understand the source code.