Concurrent programming-ReentrantLock principle explained

table of Contents

 

I. Overview

Second, what is a reentrant lock

Three, fair lock and unfair lock

Four, ReentrantLock implementation principle

1. ReentrantLock class inheritance hierarchy

2. Basic realization principle

 3. Implementation of ReentrantLock

3.1. Implementation differences between fair locks and unfair locks

3.2. Blocking and wake-up

3.3, release lock principle

3.4, acquireInterruptibly() analysis

3.5, tryLock() implementation analysis

Five, summary


I. Overview

ReentrantLock is one of the more important reentrant, mutually exclusive and support fair and unfair locks in the JUC package introduced by JDK1.5. It implements the Lock interface, and its internal implementation is achieved through the AQS + CAS principle. It has all the functions of the synchronized keyword locking, and also has some functions that the synchronized lock does not have, such as: try to get the lock, support interruption, support timeout waiting, etc.

Second, what is a reentrant lock

Reentrant locks, Gu Ming thought, are locks that support reentry, which means that the lock can support repeated locking of resources by the same thread. In layman's terms, if thread A has already acquired the lock, when thread A does not release the lock, thread A applies for the lock again, then it will not be blocked at this time, but can be entered repeatedly, but the lock counter is incremented by 1.

If the mutex does not support reentrancy, for example, it will cause a deadlock in a recursive scenario.

Three, fair lock and unfair lock

Fair lock: Multiple threads acquire locks in the order in which they apply for locks. The threads will directly enter the queue to queue up, and they will always be the first in the queue to get the lock.

  • Advantages: All threads can get resources and will not starve to death in the queue.

  • Disadvantages: throughput will drop a lot, except for the first thread in the queue, other threads will be blocked, and the overhead of cpu waking up blocked threads will be very large.

Unfair lock: When multiple threads acquire a lock, they will directly try to acquire it. If they cannot acquire it, they will enter the waiting queue. If they can acquire the lock, they will directly acquire the lock.

  • Advantages: It can reduce the overhead of CPU waking up threads, and the overall throughput efficiency will be higher. The CPU does not have to wake up all threads, which will reduce the number of threads to be called.

  • Disadvantages: You may have also discovered that this may cause the thread in the middle of the queue to be unable to acquire the lock or acquire the lock for a long time, resulting in starvation.

Four, ReentrantLock implementation principle

1. ReentrantLock class inheritance hierarchy

Before introducing the implementation principle of ReentrantLock, let's take a look at the inheritance hierarchy of the ReentrantLock class.

ReentrantLock inheritance hierarchy diagram

Description of the diagrams: I: indicates the interface, A: indicates the abstract class, C: indicates the class, and $ indicates the internal class. The solid line represents the inheritance relationship, and the dashed line represents the reference relationship. As can be seen from the above figure, ReentrantLock is also implemented internally based on AQS .

2. Basic realization principle

The parent class of Sync, AbstractQueuedSynchronizer, is often called Queue Synchronizer (AQS), and the parent class of this class is AbstractOwnableSynchronizer. In order to realize a lock with blocking or wake-up function, several core elements are needed:
(1) A state variable is needed to mark the state of the lock. The state variable has at least two values: 0, 1. The operation of state variables must ensure thread safety, that is, CAS will be used.
(2) Need to record which thread currently holds the lock.
(3) The bottom layer is required to support blocking or waking up a thread.
(4) There needs to be a queue to maintain all blocked threads. This queue must also be a thread-safe lock-free queue, and CAS is also required.

For (1) and (2), it is satisfied by the AbstractQueuedSynchronizer and AbstractOwnableSynchronizer classes. As follows:

public abstract class AbstractOwnableSynchronizer{
   .....
      private transient Thread exclusiveOwnerThread; // 记录持有锁的线程
   .....
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
   
    ......

    private volatile int state; // 记录当前的锁的同步状态,是通过 CAS 操作。

    ......
}

The source code of AbstractQueuedSynchronizer shows that it can record the thread currently holding the lock (the value recorded by exclusiveOwnerThread), the synchronization state of the current lock (the value of state), where a state of 0 means that no lock is currently occupied, and a state of 1 means a lock Has been occupied, the state can also be greater than 1, at this time, it means that the lock has been reentered. For example, state is equal to 3, which means that the thread has reentered 3 times. It also needs to be released three times during the release.

For (3) blocking and waking up, the primitives are simply sealed in the LockSupport tool class, as shown below:

public class LockSupport {
     
     //禁用当前线程的线程调度
     public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L); // 最终调用的是 Unsafe类中的 park 方法
        setBlocker(t, null);
    }
    
   // 唤醒 指定的线程
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread); //最终调用的是 Unsafe类中的 unpark 方法
    }

}

For element (4), it is achieved by maintaining a two-way queue in AQS through CAS operation, as shown below:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
   
   static final class Node { // 节点

    volatile Thread thread; // 当前线程引用
          
    volatile Node prev;   // 前置节点

    volatile Node next;  // 后续节点
    
   }
  
    private transient volatile Node head; // 头结点


    private transient volatile Node tail; // 尾结点

    ......
}

The blocking queue is the core of the entire AQS core. The head points to the head of the doubly linked list, and the tail points to the tail of the doubly linked list. When the lock has been occupied, the thread that subsequently applies for the lock will be constructed as a Node and inserted into the queue. Enqueuing means adding the new Node to the tail, and then performing CAS operations on the tail; leaving the queue means performing the head CAS operation, move the head one position back. The queue structure is as follows:

Two-way queue diagram in AQS

 3. Implementation of ReentrantLock

ReentrantLock is the phenomenon of the Lock interface. Its internal implementation is very simple. The operations of acquiring and releasing the lock are all delegated to its internal class Sync (which inherits AbstractQueuedSynchronizer and implements its template method). The specific source code can be checked by yourself, which is very simple. Next, I will mainly analyze the implementation of Sync.

Sync is also an abstract class, which has two subclasses, namely NonfairSync (non-fair lock) and FairSync (fair lock), both of which implement the abstract method lock() of Sync;

3.1. Implementation differences between fair locks and unfair locks

Unfair lock:

    static final class NonfairSync extends Sync {
       
        final void lock() {
            // 上来就更改 state 状态,也就是抢锁
            // 不考虑队列中有没有其他线程在排队,体现了非公平锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());// 如果抢成功则设置锁的拥有者为当前线程
            else
                acquire(1); // 如果抢占失败则进行重新申请
        }
    }

Fair lock:

static final class FairSync extends Sync {
      
   final void lock() {
      acquire(1); // 没有上了就抢锁,而是调用此方法,进行排队。
   }
}

According to the source code of the two implementations, it can be seen that the state of the state is updated when the unfair lock "does not speak morality", that is, the lock is grabbed, and the queue is queued when it can't be grabbed, while the fair lock is queued when it comes. The acquire() is a template method in AQS, as follows:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && 
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

The internal call tryAcquire(int arg) method tries to get the lock, but the tryAcquire() method is an empty method and needs to be implemented by subclasses. Then take a look at the implementation differences of this method between NonfairSync (non-fair lock) and FairSync (fair lock),

//非公平锁

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {// 如果当前 state 为0,就更新state的状态不考虑有没有线程排队
		//如果更新成功则设置锁的所有者为当前线程
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	//如果 state 不为0 表示锁已被占用,则判断锁的拥有者是否为当前线程
	//如果是当前线程 则 state 加 1;
	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;
}

//公平锁
protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		// 先判断等待队列中是否存在等待的线程,
		// 只有当前没有线程排队在排队,才去更新状态抢锁
		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;
}

 The two pieces of code are very similar, the only difference is that there is an additional if (!hasQueuedPredecessors()) in the fair lock. What does that mean? That is, only when c==0 (no thread holds the lock) and is ranked first in the queue (that is, when there are no other threads in the queue), the lock is grabbed, otherwise the queue is continued. This is martial arts Virtue (fair)!

3.2. Blocking and wake-up

As you can see in the acquire() method above, when the tryAcquire() method returns false, acquireQueued(addWaiter(Node.EXCLUSIVE), arg) will be called back. The function of this method is to construct the current thread into a Node node, and then proceed block.

Let me talk about the addWaiter(..) function first, which is to generate a Node for the current thread, and then put the Node at the end of the doubly linked list. It should be noted that this is just putting the Thread object into a queue, and the thread itself is not blocked ( Node.EXCLUSIVE means constructing an exclusive node ).

private Node addWaiter(Node mode) {
	// 把申请资源的当前线程 构造成一个 Node 节点 mode 为模式( 独占 或 共享 )
	Node node = new Node(Thread.currentThread(), mode);
	// Try the fast path of enq; backup to full enq on failure
	Node pred = tail; // 取到尾结点
	if (pred != null) { //如果尾结点不为空 则尝试把新构建节点 同 CAS 操作插入到尾部
		node.prev = pred;
		if (compareAndSetTail(pred, node)) { // 先尝试把节点加入到队列尾部,如果不成功则调用下
			pred.next = node;                // 面的 enq 方法通过 CAS + 自旋的方式加入到队列尾部
			return node;
		}
	}
	
	enq(node); // 如果 插入失败,则进行“自旋 + CAS” 操作插入尾部。

	return node;
}

After calling the addWaiter(mode) method to add the node containing the Thread object to the blocking queue, the acquireQueued() method is called to complete the thread blocking function. Once a thread enters acquireQueued(), it will be blocked indefinitely. Even if other threads call the interrupt() method, it cannot be awakened. Unless another thread releases the lock and the thread acquires the lock, it will return from acquireQueued() . Note meaning: enter acquireQueued (), the thread is blocked. The moment the function returns, that is, the moment when the lock is obtained, that is, the moment when it is awakened, the first element of the queue is deleted (the head pointer moves forward by 1 node).

final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true; // 标记是否失败
	try {
		boolean interrupted = false; // 标记是否被中断
		for (;;) {
			final Node p = node.predecessor(); //获取到当前节点的前驱节点

			//第一次进入或者被唤醒,如果其前驱节点为头结点,则会尝试拿锁。
			// 如果拿锁成功则会出队,即把自己设置为头结点,
			// 如果拿锁失败则阻塞等待
			if (p == head && tryAcquire(arg)) {
				setHead(node); // 果是头结点 且获取资源成功, 把当前节点设置为头结点
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。
			// 如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

 The return value of the acquireQueued() method indicates whether other threads have issued interrupt signals to it during its blocking period. If there is, the function returns true; otherwise, it returns false.

Let's look at the acquire() method again

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && 
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt(); // 如果 acquireQueued() 方法返回true,则进行中断补偿
    }

    static void selfInterrupt() {
        Thread.currentThread().interrupt(); // 重新发出中断信号
    }

With the above code, we can see that when the acquireQueued() method returns true, the callback selfInterrupt() method sends an interrupt signal to itself, that is, sets its interrupt flag bit to true. The reason for this is that during the blocking period, I received interrupt signals from other threads and did not respond in time, and now I have to compensate. In this way, if the thread calls a blocking method like sleep() inside the lock code block, it can throw an exception in response to the interrupt signal.

Next we look at the blocking part of the thread in the acquireQueued() method. Before the thread is blocked, it is judged whether the thread needs to be blocked. As follows:

  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 前驱节点状态
        if (ws == Node.SIGNAL) //如果前驱节点的状态为 SIGNAL 返回出则进行阻塞
            return true;
        if (ws > 0) { // 如果前驱节点已取消(超时或者被中断),则节点往前移,把已取消的节点
                      // 都移除调用
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 否则把前驱节点的状态更新为 SIGNAL  也就是当前驱节点释放
           // 锁后唤醒当前线程去抢锁。            
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

If the call shouldParkAfterFailedAcquire() method returns true, the current thread will sleep and wait. The specific operations are as follows:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 进行休眠
        return Thread.interrupted(); // 当被唤醒后返回当前线程是否被中断过
    }

The park() method is called in the parkAndCheckInterrupt method to make the current thread sleep, that is, it blocks itself until it is awakened by other threads, and the function returns. There are two cases when the park() function returns.

Situation 1: Unpark() is called by another thread.

Situation 2: Other threads call t.interrupt().

It should be noted that lock() cannot respond to interrupts, but LockSupport.park() will respond to interrupts. It is also because LockSupport.park() may be awakened by an interrupt, so the acquireQueued() method writes a for infinite loop. After waking up, if you find yourself at the head of the queue, you will get the lock; if you can't get the lock, you will block yourself again. Repeat this process until you get the lock. After being awakened, use Thread.interrupted() to determine whether it is awakened by an interrupt. If it is case 1, it will return false; if it is case 2, it will return true.

3.3, release lock principle

After talking about lock, let's analyze the realization of unlock. Unlock does not distinguish between fair and unfair.

    // 释放锁 ReentrantLock 中的方法
    public void unlock() {
        sync.release(1); // 释放锁 AbstractQueuedSynchronizer 中的方法
    }

   // 释放锁 AbstractQueuedSynchronizer 中的模板方法
    public final boolean release(int arg) {
        //通过 tryRelease 尝试释放资源
        if (tryRelease(arg)) {
            Node h = head; //获取头结点
            //判断头结点不null,为非初始状态,则执行唤醒操作
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); // 唤醒后续节点
            return true;
        }
        return false;
    }

 // ReentrantLock.$Sync 中的方法
protected final boolean tryRelease(int releases) {
	int c = getState() - releases;
    // 只有拥有锁的线程才能释放锁,否则 抛出异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) { //没调用1次tryRelease,state 减 1 直到 为0才才表示锁释放成功
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c); // 关键点:因为是排他锁,只有获取锁的线程才可以调用tryRelease方法进行释放锁,
                 // 所以此处 没有使用 CAS 操作。
	return free;
}

By analyzing the source code, we know that calling the unlock() method actually calls the release() method in AQS. Two things are done in release(): one is to call the tryRelease() method to release the lock; the other is to call the unparkSuccessor() method to wake up the successors in the queue.

Key note:

Because ReentrantLock is an exclusive lock, only the thread that already holds the lock is eligible to call releas(), which means that no other threads compete with it. Therefore, in the above tryRelease() method, the modification of the state value does not require a CAS operation, just subtract 1 directly.

Let's look at the unparkSuccessor method:

    private void unparkSuccessor(Node node) {

        int ws = node.waitStatus; // 获取节点的状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

         //unpark的线程被保存在后续节点中,后者通常只是下一个节点。
         // 但如果取消或明显为空,则从尾部向后遍历,以找到实际未取消的后继项

        Node s = node.next; // 获取头结点的后续节点

        // 如果后续节点为null,或者 状态为已取消 (超时 或 被中断)
        // 侧从 尾结点开始变量,找到第一个未放弃的线程
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null) // 如果找到未放弃的线程 则进行唤醒操作
            LockSupport.unpark(s.thread);
    }

The unparkSuccessor() method is also very simple. Its main logic is to get the successor node of the current node and then wake it up. It is worth noting that when the successor node of the current node is null or canceled, it traverses from the end of the queue until the first uncancelled node is found and wake up.

3.4, acquireInterruptibly() analysis

  Lock cannot be interrupted, while lockInterruptibly() can be interrupted. Let's see what is the difference between the two in implementation.

   // ReentrantLock 中方法
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    // AbstractQueuedSynchronizer 中方法
    public final void acquireInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();

        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

From the above code, we can find that acquireInterruptibly() is also a template method in AQS, and its internal call to tryAcquire() method is the same as the lock method above, so I won’t repeat the sequence here. Let's focus on the doAcquireInterruptibly() method.

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException(); // 关键点这里收到中断信号后,不再进行阻塞
                                                      // 而是直接抛出中断异常
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

The implementation principle is very similar to the accquireQueued() method. The main difference is that when parkAndCheckInterrupt() returns true, it means that another thread sends an interrupt signal, throws InterruptedException directly, jumps out of the for loop, and the entire function returns.

3.5, tryLock() implementation analysis

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

The tryLock() implementation is based on calling tryAcquire() of an unfair lock, and performs a CAS operation on the state. If the operation is successful, the lock is obtained; if the operation is unsuccessful, it directly returns false without blocking.

Five, summary

We talked about the advantages and disadvantages of reentrant locks, fair locks, and unfair locks. Analyzed the implementation principle of ReentrantLock through the source code. Its internal implementation is very simple. The main functions are implemented based on AQS. After understanding the principle of AQS, not only can the implementation principle of ReentrantLock be clear, but many tool classes in the JUC package are based Realized by AQS.

references:

"Java Concurrency Implementation Principle: Analysis of JDK Source Code"

"The Art of Concurrent Programming in Java"

"Java Concurrent Programming Practice"

https://www.imooc.com/article/302143/

 

Guess you like

Origin blog.csdn.net/small_love/article/details/111402148