ReentrantLock and Condition of java lock

foreword

Recently, when looking at the LinkedBlockingQueue data structure of java, I found that ReentrantLock is used in it. In order to better understand the thread safety principle of LinkedBlockingQueue, we have to figure out the principle behind ReentrantLock. This article introduces the locking, unlocking and fairness of ReentrantLock in detail. Behind the scenes of locks and unfair locks.

1、ReentrantLock

1.1, ReentrantLock data structure

        There is a Sync object sync in ReentrantLock, and Sync inherits from AbstractQueuedSynchronizer (AQS). The specific implementation of Sync has two NonfairSync (unfair lock) and FairSync (fair lock), the inheritance relationship is as follows:

Because both are inherited from AQS, the data structures of the two locks, NofairSync and FairSync, are the same. The main class members are as follows:

-- head, tail: save a linked list queue waiting to acquire a lock, head points to the head of the linked list, tail points to the end of the linked list

-- state: state, if it is greater than 0, it means that the lock has been used.

-- exclusiveOwnerThread: save the thread that occupies the lock

1.2 Initialization of ReentrantLock

 ReentrantLock has two constructors. The default constructor uses an unfair lock; if the setting parameter is true, it is a fair lock. The code is as follows:

public ReentrantLock() {
	sync = new NonfairSync(); //作者注:默认非公平锁
}

public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync(); //作者注:通过fair决定使用公平锁还是非公平锁
}

2. Fair lock

        When the lock method of ReentrantLock is called, in fact, the lock method of Sync is finally called. When the lock is a fair lock, the lock method of FairSync is called. The entire code call process of acquiring the lock is as follows:

//作者注:FairSync的lock方法
final void lock() {
    acquire(1);
}

//作者注:AQS的acquire方法
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

//作者注:FairSync的tryAcquire
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;
}

1. Call the tryAcquire method to try to acquire the lock:

        -- If the current lock is not used by threads (state=0) and there are no waiting threads in the queue (!hasQueuedPredecessors()), acquire the lock, set state to 1 and exclusiveOwnerThread to the current thread, and return true if the acquisition succeeds;

        -- If the lock has been acquired by the current thread, set state=state+1 and return true if the lock is acquired successfully. (This is called a reentrant lock, that is, the lock can be acquired multiple times in the same thread. Of course, a thread acquires the lock several times, and finally releases it several times),

        -- Failed to acquire the lock, return false.

2. If the lock acquisition fails, call the addWaiter(Node.EXCLUSIVE), arg) method, put the current thread in the waiting queue, and wait for other threads to release the lock. If thread-0 acquires the lock, in the unreleased state, subsequent threads that want to acquire the lock are placed in the linked list. The specific data structure is as follows:

(This picture is an approximate situation, and some details involved in it are not shown. For example, what the head points to is actually an empty node, and the actual thread node is followed by the empty node)

3. Unfair lock

The only difference between the locking process of unfair locks and fair locks is as follows:

//作者注:NonFairSync的lock方法
final void lock() {
	if (compareAndSetState(0, 1)) //作者注:直接尝试获取锁,有可能插队成功。
		setExclusiveOwnerThread(Thread.currentThread());
	else
		acquire(1); //作者注:如果获取失败,也要乖乖的去队列排队去。
}

It is known from 2 that before the fair lock acquires the lock, it first judges whether there are other threads occupying the lock (whether the state is equal to 0) and whether there are waiting threads in the queue. If so, the current thread is added to the waiting queue.

However, the unfair lock does not judge whether there are waiting threads in the waiting queue, but directly tries to reset the state of the lock (compareAndSetState). If the setting is successful, it means that the lock has been released and the thread directly occupies the lock. So my understanding is:

Fair lock: All threads must first determine whether there are waiting threads in the queue, and if so, they must queue up obediently in the queue.

Unfair lock: Do not judge whether there are waiting threads in the queue, and directly try to acquire the lock (that is, jump in the queue), which may obtain the lock first than the threads in the queue.

Of course, if the current thread does not acquire the lock, it will eventually go to the queue obediently (the subsequent execution process of acquire(1) is exactly the same as the fair lock).

4. Release of the lock

The lock release entry function is as follows:

//作者注:AbstractQueuedSynchronizer里的方法
public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}

//作者注:Sync里的方法
protected final boolean tryRelease(int releases) {
	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;
}

1. In tryRealease(arg), set state to 0, and set exclusiveOwnerThread to null,

2. Where does the thread in the specific queue call to reacquire the lock, I can't find it. (not the unparkSuccessor method)

5、Condition

Condition only appeared in java 1.5. It is used to replace the traditional Object's wait() and notify() to realize the cooperation between threads. Compared with using Object's wait() and notify(), using Condition's await() , signal() is a safer and more efficient way to achieve inter-thread collaboration. Therefore, it is generally recommended to use Condition. Blocking queues actually use Condition to simulate inter-thread collaboration.

In actual use, Condition is used together with Sync. In order to understand the relationship between Condition and Sync, I have established two Conditions in the following way, and after running, debug to see what is in the two Conditions.

ReentrantLock lock = new ReentrantLock();
Condition cond1 = lock.newCondition();
Condition cond2 = lock.newCondition();

debug Look at the information in the object:

You can see that cond1 and cond2 share a Sync. Therefore, through the above method of establishing Condition, we know that the generated data structure is as follows:

The difference between a waiting thread and a blocked thread is:

Waiting thread: has the right to acquire the lock.

Blocked thread: does not have the right to acquire the lock, and needs to be awakened and joined the AQS waiting queue to have the right to acquire the lock.

5.1, await and signal: Manually block threads and evoke threads

Call the await method of condition to indicate that the current thread should be blocked, and the lock held by the current thread should be released, and the current thread should be added to the blocking queue of the condition. The calling code is as follows:

public final void await() throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
    //作者注:将当前线程添加到Condition的阻塞线程队里的末尾
	Node node = addConditionWaiter();
    //作者注:释放当前线程持有的锁
	int savedState = fullyRelease(node);
	int interruptMode = 0;
	while (!isOnSyncQueue(node)) {
        //作者注:将当前线程挂起
		LockSupport.park(this);
		if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
			break;
	}
    //作者注:如果当前线程被唤起,尝试去获取锁
	if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
		interruptMode = REINTERRUPT;
	if (node.nextWaiter != null) // clean up if cancelled
		unlinkCancelledWaiters();
	if (interruptMode != 0)
		reportInterruptAfterWait(interruptMode);
}

In order to see the execution process more intuitively, we use simple code for debug testing and write a simple code as follows:

public class ReentrantLockTest {

    public static final ReentrantLock lock = new ReentrantLock();

    public static final Condition condition = lock.newCondition();

    public static class Td1 extends Thread {
        ReentrantLock lock ;
        Condition condition ;
        public Td1(ReentrantLock lock,Condition condition){
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {

            try {
                lock.lock();      // 代码1

                condition.await(); //代码2

                System.out.println("thread-0");

                lock.unlock(); //代码3
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class Td2 extends Thread {
        ReentrantLock lock ;
        Condition condition ;
        public Td2(ReentrantLock lock,Condition condition){
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {

            try {
                Thread.sleep(5000); //为了保证先运行线程1,后运行线程2,在这里暂停5秒

                lock.lock(); //代码4

                condition.signal(); //代码5

                System.out.println("thread-1");

                lock.unlock(); //代码6
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Td1 td1 = new Td1(lock,condition);
        Td2 td2 = new Td2(lock,condition);

        td1.start();
        td2.start();
    }
}

1. Run to code 1 : You can see the debug data of the lock as follows, the lock is occupied by Thread-0 (exclusiveOwnerThread=thread-0, state=1):

 2. Run to code 2 : Because the thread is suspended at this time, in order to see the specific data, we debug to the line of int interruptMode = 0 inside the await code, and the debug data can be seen as follows: thread-0 is added to the condition In the queue, while the lock is released (exclusiveOwnerThread=null, state=0)

 3. Run to code 4 : At this time, thread-0 is still in the condition queue, and the lock is occupied by thread-1

4. Run to code 5 : wake up the condition, clear thread-0 from the condition queue, and add it to the lock waiting queue; but because thread-1 has not released the lock at this time, the lock is still held by thread-1.

 5. Run to code 6 : At this time, thread-1 releases the lock, because the running speed is fast, and it has been captured by thread-0 without seeing the lock idle period:

 6. At this time, thread-0 has been awakened, the code runs to code 3 , thread-0 releases the lock, and the entire code runs to end.

The above is the entire process of using ReentrantLock and Condition together. From the debugging process, you can clearly see the changes in the entire internal data structure and lock holding.

6. Explain again

This article provides a framework description of the overall lock holding changes and the internal data structure of ReentrantLock and Condition. In fact, there are many details in the internal lock implementation mechanism, which are not shown here, because the author's purpose is to let everyone see clearly How locks are acquired and released. If you want to know the specific details of the mechanism, you can refer to the following author's writing, which is very detailed, but requires careful taste, and it is impossible to understand it after thinking about it.

Detailed internal implementation mechanism of ReentrantLock lock

Guess you like

Origin blog.csdn.net/chenzhiang1/article/details/126739613