6K words to analyze Java's AQS source code

In recent interviews, many candidates have been asked about knowledge about Java locks. It can be felt that everyone's understanding is basically at the stage of "stereotyped essays". In essence, the underlying principles of Java locks and multi-threaded synchronization mechanisms, Not very well understood. There are already many articles of this kind on the Internet, but after reading many articles, they are outdated. Typical ones, such as the addWaiter method in AQS, have not been seen in JDK16. Maybe the code has been refactored.

Through the article, I also sort out my general habit of reading source code. The full name of AQS is AbstractQueuedSynchronizer. First of all, it is an abstract class. Secondly, it uses queues for queuing, and then it is used for synchronization between threads. He is the basis of all locks in Java, including CountDownLatch, read-write locks, reentrant locks, etc. are all implemented based on AQS. Let's start with ReentrantLock to get a glimpse of the leopard. We probably have to look at the source code of AQS.

The first thing you need to understand is the usage method. A typical usage method of ReentrantLock is written in the comments of this class.

image.png

Next, you have to have a bird's-eye view and look at its implementation from a large level. ReentrantLock is actually just an outer layer of packaging. In fact, the interior is actually implemented by the Sync class, and this class has two subclasses. The classes are FairSync and NonfairSync respectively. It can be seen that these two subclasses override the two methods of initialTryLock and trayAcquire, which shows that there will be some differences in the implementation of these two methods, and there are only some differences in this.

image.png

So now we start with two methods, namely the lock and tryLock methods. The difference between the two is that the one with try is just a try. If you can get the best, if you can’t get it, forget it. Return a false to tell you that you didn’t get it. Lock. The implementation of lock is as follows:

image.png

You can see that what he calls is the initialTryLock method of the subclass, and then look at the initialTryLock method. We take NoFairSync, which is his default implementation. The method is also very simple. Try to acquire the lock through CAS. If it succeeds, set the current owner to the cost thread. Otherwise, if it fails, check whether the current owner is the thread. If yes, directly state+1, Counting is implemented, that is, the ability to be reentrant. If it is not, return false, and the acquisition of the lock has failed. On failure, acquire(1) is called.

image.png

This method is provided by the abstract class AQS. The method is as follows. The tryAcquire method of the subclass is called. Let's take a look at the implementation of NoFiairSync.

image.png

Here the CAS will be called again to try again.

image.png

If it still fails, it will enter the implementation of AQS acquire. This method is the most complicated and difficult to understand. It is the core of AQS. It looks very complicated from the parameters at a glance. It is compatible with share or not share, whether it can be interrupted , timeout, etc., so the complexity comes up, we still focus on the current scene, that is, acquire(null, arg, false, false, false, 0L); no share, no Interrupt, no timeout....

image.png

The body of the method is as follows, a part of it is pasted, but not all of it.

 
 

This

copy code

for (;;) { if (!first && (pred = (node == null) ? null : node.prev) != null && !(first = (head == pred))) { if (pred.status < 0) { cleanQueue(); // predecessor cancelled continue; } else if (pred.prev == null) { Thread.onSpinWait(); // ensure serialization continue; } } if (first || pred == null) { boolean acquired; try { if (shared) acquired = (tryAcquireShared(arg) >= 0); else acquired = tryAcquire(arg); } catch (Throwable ex) { cancelAcquire(node, interrupted, false); throw ex; } if (acquired) { if (first) { node.prev = null; head = node; pred.next = null; node.waiter = null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } return 1; } } if (node == null) { // allocate; retry before enqueue if (shared) node = new SharedNode(); else node = new ExclusiveNode(); } else if (pred == null) { // try to enqueue node.waiter = current; Node t = tail; node.setPrevRelaxed(t); // avoid unnecessary fence if (t == null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); // back out else t.next = node; } else if (first && spins != 0) { --spins; // reduce unfairness on rewaits Thread.onSpinWait(); } else if (node.status == 0) { node.status = WAITING; // enable signal and recheck } else { long nanos; spins = postSpins = (byte)((postSpins << 1) | 1); if (!timed) LockSupport.park(this); else if ((nanos = time - System.nanoTime()) > 0L) LockSupport.parkNanos(this, nanos); else break; node.clearStatus(); if ((interrupted |= Thread.interrupted()) && interruptible) break; } }

He made an infinite loop to achieve the purpose of multiple functions of a function through multiple loops + if conditions.

The first loop goes here first, and initializes a node first.

 
 

This

copy code

if (node == null) { // allocate; retry before enqueue if (shared) node = new SharedNode(); else node = new ExclusiveNode(); }

The second cycle here, add the current node to the tail, and point the prev to the previous node, and point the next of the previous node to the current node. You can see that this implements a linked list.

 
 

This

copy code

else if (pred == null) { // try to enqueue node.waiter = current; Node t = tail; node.setPrevRelaxed(t); // avoid unnecessary fence if (t == null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); // back out else t.next = node; }

Insert a picture on the network here to illustrate, which is more helpful to understand this code. There is a data structure Node in AQS. This data structure has a prev and next attribute, which realizes the function of a doubly linked list. Each call lock The thread comes in, if it can't get the lock, it will join the queue and wait, then the operation process of this queue is in the above code.

image.png

If there is no node in the queue, it is currently the head node, and it is trying to acquire the lock again, which is the following code. Otherwise, call park to suspend the current thread.

 
 

This

copy code

if (first || pred == null) { boolean acquired; try { if (shared) acquired = (tryAcquireShared(arg) >= 0); else acquired = tryAcquire(arg); } catch (Throwable ex) { cancelAcquire(node, interrupted, false); throw ex; } if (acquired) { if (first) { node.prev = null; head = node; pred.next = null; node.waiter = null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } return 1; } }

The above is the core content in it. If you write it, is it very simple? If you write it, how would you write it? The criticism of this code is that the same function does many things, including initializing node nodes and updating queues, and it is implemented through multiple loops + if, which seems difficult to understand.

Finally, we asked ourselves a few questions:

1. What happens when the lock is released? The code is also relatively simple, as follows, when unLocking, pass in 1.

image.png

Each time release(1) is called, it will be deducted once, which corresponds to the reduction of the counter of the reentrant lock. At the same time, exception calls are considered. When other threads try to release the current lock, an error is thrown. At the same time, if the lock is released, set the state to 0, set the current thread to empty, and completely release the lock.

image.png

Next is SinalNext(head), which uses LockSupport.unpark(s.waiter); to wake up our head node, which is the first node inserted into the queue.

2. What is the difference between a fair lock and an unfair lock? After watching for a long time, we found that whether it is a fair lock or an unfair lock, the logic seems to be the same. They must be queued and the queue is maintained. So where is the good unfair lock reflected?

image.png

It turns out that in the process of acquiring a lock for a fair lock, it is always judged in advance whether the queue is empty. If it is not empty, it will join the waiting queue. The non-fair lock is different. No matter whether your queue is empty or not, grab one first and then talk , there is a high probability that the current thread grabs the lock directly, and it does not enter the waiting queue, which is certainly unfair to the thread that arrived first. So many candidates have answered that it is obviously inaccurate that your fair lock has worse performance because you need to maintain a queue. Even if there is an unfair lock, they will most likely enter the queue and maintain the queue. The only difference is that the waiting time for obtaining the lock is different, and the opportunities are different.

To sum up, the content of the whole article mainly introduces part of the implementation mechanism of AQS, and briefly explains the source code of AQS through the implementation of ReentrantLock. Java's lock mechanism also implements the lock function through a volatile modified state variable, and setting this value through the underlying CAS operation. At the same time, threads that cannot acquire locks are maintained through a two-way queue, and these threads are set to wait with the help of Java's own park. When the lock is released, go to the head of the queue to wake up the thread to continue working through unpark. It's that simple, there's nothing mysterious about it.

Finally, use the code comments generated by chatgpt to help understand.

 
 

This

copy code

/** * 尝试获取锁或信号量,如果成功获取则返回1,否则继续尝试获取直至成功或被中断或超时 * * @param node 当前节点 * @param arg 获取锁或信号量时的参数 * @param shared 是否是共享模式 * @param interruptible 是否允许被中断 * @param timed 是否使用定时等待 * @param time 定时等待的超时时间 * @return 成功获取返回1,超时返回0,被中断返回负数 */ final int acquire(Node node, int arg, boolean shared, boolean interruptible, boolean timed, long time) { // 获取当前线程 Thread current = Thread.currentThread(); // 用于重试计数的变量 byte spins = 0, postSpins = 0; // 用于标记当前线程是否被中断以及当前节点是否是队列中的首节点 boolean interrupted = false, first = false; // 当前节点的前驱节点 Node pred = null; // 循环尝试获取锁或信号量 for (;;) { // 检查是否是队列中的首节点 if (!first && (pred = (node == null) ? null : node.prev) != null && !(first = (head == pred))) { // 非首节点,检查前驱节点是否已取消或是有新的前驱节点 if (pred.status < 0) { // 前驱节点已取消,清理队列 cleanQueue(); continue; // 重新开始循环尝试获取锁或信号量 } else if (pred.prev == null) { // 确保串行化,避免过度自旋 Thread.onSpinWait(); continue; // 重新开始循环尝试获取锁或信号量 } } // 尝试获取锁或信号量 if (first || pred == null) { // 首节点或前驱节点为null,说明当前节点还未入队列 boolean acquired; try { // 尝试获取锁或信号量 if (shared) acquired = (tryAcquireShared(arg) >= 0); else acquired = tryAcquire(arg); } catch (Throwable ex) { // 取消获取操作,抛出异常 cancelAcquire(node, interrupted, false); throw ex; } // 如果成功获取锁或信号量 if (acquired) { // 更新头节点和前驱节点的引用,设置节点状态,唤醒其他等待线程 if (first) { node.prev = null; head = node; pred.next = null; node.waiter = null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } // 返回成功 return 1; } } // 尝试入队或重试 if (node == null) { // 未分配节点,分配新节点并重试 if (shared) node = new SharedNode(); else node = new ExclusiveNode(); } else if (pred == null) { // 前驱节点为null,说明当前节点还未入队列,尝试将当前节点入队列 node.waiter = current; Node t = tail; node.setPrevRelaxed(t); // 避免不必要的内存屏障 if (t == null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); // 入队失败,回退 else t.next = node; } else if (first && spins != 0) { // 重试次数限制,减少对先前线程的不公平竞争 --spins; // 进行自旋等待 Thread.onSpinWait(); } else if (node.status == 0) { // 设置状态为等待中,以便其他线程进行唤醒 node.status = WAITING; } else { // 需要定时等待 long nanos; spins = postSpins = (byte) ((postSpins << 1) | 1); if (!timed) LockSupport.park(this); // 非定时等待 else if ((nanos = time - System.nanoTime()) > 0L) LockSupport.parkNanos(this, nanos); // 定时等待 else break; // 超时,结束等待 // 清除节点的状态 node.clearStatus(); // 检查线程是否被中断,并根据需要退出循环 if ((interrupted |= Thread.interrupted()) && interruptible) break; } } // 取消获取操作,并根据中断状态返回相应结果 return cancelAcquire(node, interrupted, interruptible); }

Guess you like

Origin blog.csdn.net/wdj_yyds/article/details/131982091