🌹 The open source project is recommended as 🌹

Pepper Metrics is an open source tool I developed with my colleagues (github.com/zrbcool/pep…) , by collecting jedis mybatis/httpservlet/dubbo/motan performance statistics, and exposure to Prometheus and other mainstream temporal database compatible data, through grafana show the trend. Its plug-in architecture also makes it easy for users to extend and integrate other open source components. Please give us a STAR, and we welcome you to become developers to submit PR and improve the project together.


Get down to business…

ReentrantLock, a ReentrantLock, is a high performance tool added in JDK5 to send and send packages. As the name implies, ReentrantLock allows the same thread to repeatedly acquire a lock without releasing it.

Everything that comes along must be valuable. Why would Doug Lea write a ReentrantLock when you already have the patriarch of synchronized, and synchronized supports ReentrantLock?

0 ReentrantLock compared with synchronized

0.1 Performance comparison

First, ReentrantLock performs better than synchronized. Let’s compare prices with two pieces of code. Synchronized:

public class LockDemo2 {
    private static final Object lock = new Object(); // Define the lock object
    private static int count = 0; / / accumulation
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        CountDownLatch cdl = new CountDownLatch(100);
        // Start 100 threads to accumulate count 1000000 times per thread
        // Add is synchronized to ensure synchronization between multiple threads
        for (int i=0; i<100; i++) {new Thread(() -> {
                for (int i1 = 0; i1 <1000000; i1++) {
                    add();
                }
                cdl.countDown();
            }).start();
        }
        cdl.await();
        System.out.println("Time cost: " + (System.currentTimeMillis() - start) + ", count = " + count);
    }

    private static void add(a) {
        synchronized(lock) { count++; }}}Copy the code

Then there’s ReentrantLock:

public class LockDemo3 {
    private static Lock lock = new ReentrantLock(); / / reentrant lock
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        CountDownLatch cdl = new CountDownLatch(100);
        for (int i=0; i<100; i++) {new Thread(() -> {
                for (int i1 = 0; i1 <1000000; i1++) {
                    add();
                }
                cdl.countDown();
            }).start();
        }
        cdl.await();
        System.out.println("Time cost: " + (System.currentTimeMillis() - start) + ", count = " + count);
    }
    // Ensure synchronization between threads through ReentrantLock
    private static void add(a) { lock.lock(); count++; lock.unlock(); }}Copy the code

Here is a comparison of the results of multiple runs:

synchronized ReentrantLock
For the first time, 4620 ms 3360 ms
The second time 4086 ms 3138 ms
The third time 4650 ms 3408 ms

Overall, ReentrantLock performed, on average, about 20% better than synchronized.

PS: Thanks @wild seventh uncle for correcting. To describe this performance comparison more precisely, ReentrantLock performs better than synchronized in most cases when there are a large number of threads competing for the lock.

Because synchronized has been optimized in JDK6, when lock competition is not fierce, the lock will mostly stay in the biased lock and lightweight lock phases, which have good performance. When there is a lot of contention, ReentrantLock may swell to heavyweight locks and degrade performance, which should be superior to synchronized.

0.2 Comparison of fairness of obtaining locks

What is fairness? If it is a fair lock acquisition, that is, multiple threads should queue up to acquire the lock in sequence. If it is unfair to acquire the lock, it means that multiple threads will rush to acquire the lock, and whoever grabs it will be the one who gets it.

Since synchronized is implemented based on the monitor mechanism, it only supports unfair locking; But ReentrantLock supports both fair and unfair locks.

0.3 review

In addition to the above, ReentrantLock has a few other features that synchronized does not, summarized here.

synchronized ReentrantLock
performance Relatively poor About 20% better than synchronized
fairness Only unfair locks are supported Both fair and unfair locks are supported
Try to get lock support Not supported. Once a synchronized block is reached and no lock is acquired, it is blocked here This is supported by the tryLock method, which returns a value to determine whether the lock was successfully acquired, so it will not block if the lock fails to be acquired
Timeout to obtain the lock Not supported. If the lock cannot be obtained, it will wait forever Support, through the tryLock(time, TimeUnit) method, if the timeout has not obtained the lock, give up, will not block forever
Whether interrupts can be responded to Interrupt signals are not supported and cannot respond to threads Supported by the lockInterruptibly method, which allows the thread to respond to the interrupt signal and throw InterruptedException after acquiring the lock
Wait for the condition to be supported Supports wait, notify, and notifyAll Support, through the Conditon interface implementation, support multiple conditions, more flexible than synchronized

1 Implementation principle of the reentrant function

Already implemented based on the queue synchronizer behind (AbstractQueuedSynchronizer, referred to as “AQS”), about the implementation principle of AQS, we can see the author’s another article: Java queue synchronizer (AQS) what is going on

ReentrantLock’s reentrant capability is based on the AQS synchronization state: state.

Its principle is roughly as follows: when a thread acquires the lock, the state value is +1, and the thread currently holding the lock is recorded. When another thread acquires the lock, it judges whether the thread and the thread holding the lock are the same thread. If so, the state value is +1; if not, the thread is blocked. When the thread releases the lock, the state value is -1. When the state value is reduced to 0, the current thread releases the lock completely. Then, the field that records the current thread holding the lock is set to NULL and other threads are awakened to re-contest the lock.

Acquires has the value 1
final boolean nonfairTryAcquire(int acquires) {
	// Get the current thread
	final Thread current = Thread.currentThread();
	// Get the value of state
	int c = getState();
	// If the value of state is 0, no thread currently holds the lock
	// Try changing the value of state to 1. If the change succeeds, the lock is successfully acquired and the current thread is set as the thread holding the lock, returning true
	if (c == 0) {
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true; }}// The value of state does not equal 0, indicating that another thread already holds the lock
	// Determine whether the current thread is equal to the thread holding the lock. If so, set the value of state to 1 and obtain the lock successfully. Return true
	// If not the current thread, failed to acquire the lock, return false
	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

2 The realization principle of unfair lock

ReentrantLock has two constructors:

// NonfairSync is used by default.
public ReentrantLock(a) {
	sync = new NonfairSync();
}

// Use the fair argument to specify whether to use FairSync or NonfairSync
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}
Copy the code

Sync is a member variable of ReentrantLock and an instance of its internal class sync. NonfairSync and FairSync are subclasses of the Sync class. You can refer to the following class diagram:

Sync inherits AQS, so it has AQS functionality. Similarly, NonfairSync and FairSync are subclasses of AQS.

When we get an instance of ReentrantLock through the no-argument constructor, we default to an unfair lock.

The implementation principle of an unfair lock is described in the following scenario: Assume that one thread (T1) has acquired the lock, and many other threads that have not acquired the lock (others_t) join the synchronization queue of AQS to wait. When this thread finishes executing, the lock is released, and other threads compete for the lock unfairly again.

Let’s describe how to acquire locks:

final void lock(a) {
	Thread T1 successfully changed state from 0 to 1, indicating that the lock was acquired successfully
	if (compareAndSetState(0.1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
	    // others_t Threads did not acquire the lock
		acquire(1);
}
Copy the code

If the lock fails to be acquired, the AQS acquire method is called

public final void acquire(int arg) {
    // tryAcquire is a template method implemented in NonfairSync. If the tryAcquire method still fails to acquire the lock, the current thread will be added to the synchronization queue (addWaiter)
    if(! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }Copy the code

TryAcquire is implemented as follows, calling the nonfairTryAcquire method above

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

OK, now T1 has acquired the lock, and the others_T thread is waiting in the synchronization queue.

At some point, T1 completes its task and calls unlock.

public void unlock(a) {
    // Call AQS 'release method to release resources
    sync.release(1);
}
Copy the code
public final boolean release(int arg) {
    // tryRelease is also a template method implemented in Sync
    if (tryRelease(arg)) {
        Node h = head;
        if(h ! =null&& h.waitStatus ! =0)
            // After successfully releasing the lock, wake up the next node in the synchronization queue so that it can re-compete for the lock
            // Note that nodes after the first node in the queue are not awakened, and they still cannot compete for the lock
            unparkSuccessor(h);
        return true;
    }
    return false;
}
Copy the code
protected final boolean tryRelease(int releases) {
    // Set state to -1. If -1 equals 0, the lock is released successfully
    int c = getState() - releases;
    if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
Copy the code

The lock is released, and the awakened thread recontests the lock with the new thread (excluding those behind the synchronized queue).

Going back to the lock method, since all threads can acquire the lock through CAS, there is no guarantee that the awakened thread can compete with the new thread, so it is unfair. This is the implementation of an unfair lock.

The process can be roughly described as follows:

3 Implementation principle of fair lock

Unlock a fair lock uses the same logic as an unfair lock. The key is unlocked.

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    // Acquire the lock, which differs from the unfair lock in that the acquire method of AQS is called directly without first attempting to acquire the lock
    // Acquire calls the following tryAcquire method, which is the core of this method
    final void lock(a) {
        acquire(1);
    }

    /** * This method differs from the nonfairTryAcquire method only in that #1 is used to determine hasqueuedToraise, which determines whether there are waiting threads on the current AQS synchronization queue * if there are, returns true, Otherwise return false. * The current thread can only attempt to acquire the lock through CAS if there are no threads waiting in the queue. * Otherwise, let the thread queue up behind the queue. * /
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            / / # 1
            if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true; }}else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false; }}Copy the code

As can be seen from the comment, under the mechanism of fair lock, any thread that wants to obtain the lock must queue, and it is impossible to jump the queue. This is how fair locking works.

The process can be roughly described as follows:

Principle 4 tryLock

What tryLock does is simple: make the current thread try to acquire the lock once, returning true if it succeeds, false otherwise.

The implementation simply calls the nonfairTryAcquire method to acquire the lock.

public boolean tryLock(a) {
    return sync.nonfairTryAcquire(1);
}
Copy the code

If the fetch fails, it does not add itself to the synchronous queue, returns false and lets the business calling code take care of itself.

5 Interruptible lock acquisition

Interrupts, or interrupts a Thread with the Threadinterrupt method. Interrupting a blocked Thread throws an InterruptedException.

If the lock acquisition is interruptible, when a thread does not acquire the lock for a long time, we can take the initiative to break it, to avoid the generation of deadlock.

Its implementation is as follows:

public void lockInterruptibly(a) throws InterruptedException {
    sync.acquireInterruptibly(1);
}
Copy the code

The AQS acquireInterruptibly method is called

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    // Determine if the current thread is interrupted and throw InterruptedException
    if (Thread.interrupted())
        throw new InterruptedException();
    if(! tryAcquire(arg)) doAcquireInterruptibly(arg); }Copy the code

In this case, tryAcquire will try to acquire the lock first. If it fails to acquire the lock, it will join itself in the queue and wait, and can respond to the interruption at any time.

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    // Add yourself to the queue to wait
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        // Get the spin lock
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            // Failed to acquire the lock. In parkAndCheckInterrupt, block the current thread with locksupport.park (),
            // Call thread.interrupted () to see if the current Thread has been interrupted
            // If interrupted, InterruptedException is thrown to exit the contention queue
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                / / # 1
                throw newInterruptedException(); }}finally {
        if(failed) cancelAcquire(node); }}Copy the code

PS: In an uninterruptible manner, code #1 does not throw InterruptedException, but simply records that the current thread is interrupted.

6 Obtain a lock that can time out

You can do this by using the following method: timeout is the timeout period, and unit is the unit of time (milliseconds, seconds…).

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
Copy the code

As you can see, this is also a method that can respond to interrupts. Then call AQS ‘tryAcquireNanos method:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
Copy the code

The doAcquireNanos method is the same as the interrupt method. The differences are explained in the following comments:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    // Calculate the timeout expiration time
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // Calculate the time remaining to the cutoff time
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L) // Failed to fetch
                return false;
            // Block only when the timeout is greater than 1000 nanoseconds
            // Because if it is less than 1000 nanoseconds, it is basically a timeout (system calls can take longer than that).
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // The response is interrupted
            if (Thread.interrupted())
                throw newInterruptedException(); }}finally {
        if(failed) cancelAcquire(node); }}Copy the code

7 summary

This paper first compares the difference between synchronized and ReentrantLock. ReentrantLock has the following advantages:

  • Both fair and unfair locks are supported
  • Support: Try to acquire a lock once without blocking
  • Timeout lock acquisition is supported
  • Support for interruptible lock acquisition
  • Support for more wait conditions

It then introduces the implementation principles of several main features, which are based on AQS.

At its core, ReentrantLock synchronizes the lock state by changing the value of state in AQS. In this way, reentrant is achieved.

ReentrantLock has fair and unfair locks. By default, unfair locks are used. Its implementation principle mainly depends on the synchronous queue in AQS.

Finally, the interruptible mechanism is implemented internally by determining whether the current Thread has been interrupted with Thread.interrupted() and throwing InterruptedException if it has been interrupted.