Starting from the source code, provide a detailed explanation of ReentrantLock, a more powerful reen

While browsing casually, I realized that I’ve already posted dozens of blogs about learning concurrent multithreading in Java, which is a core part of Java fundamentals! Thus, it might take dozens more posts to approximately cover these knowledge points. Beginners must pay close attention to this content and cannot afford to be careless! Today, we continue to learn an important concept: ReentrantLock.

ReentrantLock

ReentrantLock is an exclusive, reentrant lock located in java.util.concurrent.locks. It is the default implementation of the Lock interface. Its underlying synchronization features are based on AQS, similar to the synchronized keyword but more flexible, more powerful, and very frequently used in real-world applications.

Definitions of Various Locks

Before studying ReentrantLock, let’s first review the definitions of several types of locks, which I have already thoroughly organized in an earlier blog. Here, we list them briefly to better understand ReentrantLock.

Exclusive Locks and Shared Locks

  • Exclusive Lock: At any given time, a lock can only be obtained by one thread.
  • Shared Lock: At the same time, a lock can be obtained by multiple threads.

Fair Locks and Unfair Locks

  • Fair Lock: Locks are redistributed based on the order of application time, which often results in slightly poorer performance because it maintains the sequence of application times.
  • Unfair Lock: After a lock is released, the chance of subsequent threads obtaining the lock is random, or it may be preemptively obtained based on a set priority.

Reentrant Locks

A reentrant lock allows a thread that obtains an object lock to obtain it again within the thread, even if the held lock has not been released, unlike non-reentrant locks which can lead to a deadlock!

It is important to note that because the lock is acquired n times, it is only completely released after it has been released n times.

Interruptible and Non-interruptible Locks

  • Interruptible Lock: The lock acquisition process can be interrupted, and it does not have to wait until the lock is obtained before proceeding with other logic.
  • Non-interruptible Lock: Once a thread has applied for a lock, it must wait until the lock is obtained before it can execute other logic processes.

ReentrantLock is a synchronizer that possesses exclusive, reentrant, interruptible, and fair/unfair characteristics!

Deeper Dive into ReentrantLock

Based on the characteristics summarized above, let’s verify the accuracy of these conclusions by starting from the underlying source code. First, we will outline the internal structure of ReentrantLock through a relational graph.

ReentrantLock implements the Lock and Serializable interfaces:

public class ReentrantLock implements Lock, java.io.Serializable {}

It has three internal classes, namely Sync, FairSync, and NonfariSync, where FairSync and NonfariSync inherit the parent class Sync. Sync inherits AQS (AbstractQueuedSynchronizer), and most of the operations of adding and releasing locks are actually implemented in Sync.

Question 1: How can ReentrantLock implement internal fair locks and non fair locks?

Internally, fair and non fair locks can be set through a constructor, which defaults to non fair locks and can also be set to fair locks through parameter passing. The underlying implementation is actually achieved through two internal classes, FairSync and NonfariSync. The source code is as follows:

// No-argument constructor, defaults to an unfair lock
public ReentrantLock() {
    sync = new NonfairSync();
}

// Constructor with a boolean parameter; when true, it creates a fair lock, when false, an unfair lock
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

Question 2: How to implement exclusive locks?

In the source code, whether it is the internal class Sync or its subclass, the setExclusiveOwnerThread (current) method is called, which is a method in AQS’s parent class AOS (AbstractOwnableSynchronizer) to mark the lock holder as exclusive mode.

Question 3: How ReentrantLock obtains and releases a lock

Since ReentrantLock is the default non fair lock, let’s take the non fair mode as an example to see how it implements the acquisition and release of locks at the underlying level.

  • Lock acquisition
    The core method is the non fair TryAcquire method of the Sync internal class. The source code is as follows: first, obtain the current lock state. If it is 0, it indicates that it has not been obtained by any thread. At this point, it can be obtained directly; When the other type of state is not 0, it is necessary to determine whether the owning thread is the current thread. If it is, it can be obtained and the state value will be returned by adding one. Otherwise, the acquisition will fail.

[Note]: In fair mode, when obtaining a lock, an additional step will be called to check the logic of hasQueuedPredecessors, which is used to determine whether the node corresponding to the current thread has a predecessor node in the waiting queue. After all, fair lock competition is strictly allocated based on the time of obtaining the lock.

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 1. If the lock is not held by any thread, the current thread can acquire it
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 2. If the lock is held, check if the holding thread is the current thread
    else if (current == getExclusiveOwnerThread()) {
        // 3. If reacquiring, increment the count
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  • Release of lock
    Taking the release of non fair locks as an example, we can see from the source code that for each call, the synchronization status is reduced by 1 until the synchronization status is 0, and the lock is fully released. Otherwise, it returns false.
protected final boolean tryRelease(int releases) {
    // 1. Decrease the synchronization state by 1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 2. Only when the synchronization state is 0, the lock is successfully released, return true
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 3. If the lock is not completely released, return false
    setState(c);
    return free;
}
  • Summary
    After studying the source code above, we can confirm that ReentrantLock is a synchronizer that simultaneously has exclusive, reentrant, interruptible, and fair/unfair features! Let’s continue to learn about its usage.

Question 4: Use of ReentrantLock

Let’s experience the use of ReentrantLock based on unfair lock mode through a small demo

public class Test {
    // Initialize a static lock object
    private static final ReentrantLock lock = new ReentrantLock();
    // Initialize the counter value
    private static int count;
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        // Print the result of the count after both threads are complete
        System.out.println("result: " + count);
    }
}

The expected output result of the above program is 2000, with thread1 and thread2 performing 1000 operations respectively. As ReentrantLock is an exclusive reentrant lock, the expected result can be successfully printed in the end!

Title of this article:<Starting from the source code, provide a detailed explanation of ReentrantLock, a more powerful reen>Author:minimini
Original link:https://www.xxmjw.com/post/29.html
Unless otherwise specified, all content is original. Please indicate when reprinting.

Related

minimini

minimini