This article takes you to understand how the underlying AQS of the Lock lock in Java is implemented

I believe that everyone is familiar with Lock locks in Java, such as ReentrantLock. Locks are mainly used to solve the thread safety problem when multi-threaded operation accesses shared resources. Are you curious about how these Lock APIs are implemented? This article is to discuss how the underlying AQS (AbstractQueuedSynchronizer) of these Locks is implemented.

This article is based on ReentrantLock. ReentrantLock is just a call to AQS's api. The state of the underlying lock and the process of waiting for other threads (Node doubly linked list) are actually maintained by AQS.

lock

Let's first look at the locking process, first look at the source code, and then simulate the process of two threads to lock.

The figure above is a partial implementation of ReentrantLock. There is an instance variable of the inner class of Sync. This inner class of Sync inherits from AQS. The Sync subclass includes the implementation of fair locks and unfair locks. To put it bluntly, ReentrantLock is a subclass of Sync to achieve locking.

Let's take a look at NonfairSync, the implementation of Sync's unfair lock.

It has rewritten its lock locking method. Because it is unfair in the implementation, it will first try to change the state parameter of the AQS class to 1 through cas, and try to lock directly. If the attempt to lock fails, the acquire method of AQS will be called to continue to try to lock.

Suppose there is a thread 1 here to call the lock method first, then no one locks at this time, then through the CAS operation, the variable in the state in AQS is changed from 0 to 1, which means that someone comes to lock, and then the lock is added. The threads are set to themselves as shown.

Then there is another thread 2 to lock at this time, and it is found that the CAS operation will fail, because the state has been set to 1, thread 2 will fail to set, then it will go else and call the acquire method of AQS to continue Try locking.

进入到acquire会先调用tryAcquire再次尝试加锁,而这个tryAcquire方法AQS其实是没有什么实现的,会调用到NonfairSync里面的tryAcquire,而tryAcquire实际会调用到Sync内部类里面的nonfairTryAcquire非公平尝试加锁方法。

先获取锁的状态,判断锁的状态是不是等于0,等于0说明没人加锁,可以尝试去加,如果被加锁了,就会走else if,else if会判断加锁的线程是不是当前线程,是的话就给state 加 1,代表当前线程加了2次锁,就是可重入锁的意思(所谓的可重入就是代表一个线程可以多次获取到锁,只是将state 设置为多次,当线程多次释放锁之后,将state 设置为0才代表当前线程完全释放了锁)。

这里所有的条件假设都不成立。也就是线程2尝试加锁的时候,线程1并没有释放锁,那么这个方法就会返回false。

接下来就会走到addWaiter方法,这个方法很重要,就是将当前线程封装成一个Node,然后将这个Node放入双向链表中。addWaiter先根据指定模式创建指定的node节点,因为ReentrantLock是独占模式,所以传进去的EXCLUSIVE,这里通过当前线程和模式传入,初始化一个双向node节点,获取最后一个节点,根据最后一个节点是否存在来操作当前节点的父级。如果尾节点不存在会去调用enq去初始化

放入链表中之后如图。

然后调用acquireQueued方法

这个方法一进来也会尝试将当前节点去加锁,然后如果加锁成功就将当前节点设置为头节点,最后将当前线程中断,等待唤醒。

线程2进来的时候,刚好线程2的前一个节点是头节点,但是不巧的是调用tryAcquire方法,还是失败,那么此时就会走shouldParkAfterFailedAcquire方法,这个方法是在线程休眠之前调用的,很重要,我们来看看干了什么事。

判断当前节点的父级节点的状态,如果父级状态是-1,则代表当前线程可以被唤醒了。如果父级的状态为取消状态(什么叫非取消状态,就是tryLock方法等待了一些时间没获取到锁的线程就处于取消状态)就跳过父级,寻找下一个可以被唤醒的父级,然后绑定上节点关系,最后将父级的状态更改为-1。也就说,线程(Node)加入队列之后,如果没有获取到锁,在睡眠之前,会将当前节点的前一个节点设置为非取消状态的节点,然后将前一个节点的waitStatus设置为-1,代表前一个节点在释放锁的时候需要唤醒下一个节点。这一步骤主要是防止当前休眠的线程无法被唤醒。这一切设置成功之后,就会返回true。

接下来就会调用parkAndCheckInterrupt

,这个方法内部调用LockSupport.park方法,此时当前线程就会休眠。

到这一步线程2由于没有获取到锁,就会在这里休眠等待被唤醒。

来总结一下加锁的过程。

线程1先过来,发现没人加锁,那么此时就会加上锁。此时线程2过来,在线程2加锁的过程中,线程1始终没有释放锁,那么线程2就不会加锁成功(如果在线程2加锁的过程中线程1始终释放锁,那么线程2就会加锁成功),线程2没有加锁成功,就会将自己当前线程加入等待队列中(如果没有队列就先初始化一个),然后设置前一个节点的状态,最后通过LockSupport.park方法,将自己这个线程休眠。

如果后面还有线程3,线程4等等诸多的先过来,那么这些线程都会按照前面线程2的步骤,将自己插入链表后面再休眠。

释放锁

ok,说完加锁的过程之后,我们来看看释放锁干了什么。

ReentrantLock的unlock其实是调用AQS的release方法,我们直接进入release方法,看看是如何实现的

进入tryRelease方法,看一下Sync的实现

其实很简单,就是判断锁的状态,也就是加了几次锁,然后减去释放的,最后判断释放之后,锁的状态是不是0(因为可能线程加了多次锁,所以得判断一下),是的话说明当前这个锁已经释放完了,然后将占有锁的线程设置为null,然后返回true,

然后就会走接下来的代码。

就是判断当前链表头节点是不是需要唤醒队列中的线程。如果有链表的话,头结点的waitStatus肯定不是0,因为线程休眠之前,会将前一个节点的状态设置为-1,上面加锁的过程中有提到过。

接下来就会走unparkSuccessor方法,successor代表继承者的意思,见名知意,这个方法其实就会唤醒当前线程中离头节点最近的没有状态为非取消的线程。然后调用LockSupport.unpark,唤醒等待的线

然后线程就会从阻塞的那里苏醒过来,继续尝试获取锁。

我再次贴出这段代码。

获取到锁之后,就将头节点设置成自己。

对应我们的例子,就是线程1释放锁之后,就会唤醒在队列中线程2,先成2获取到锁之后,就会将自己前一个节点(也就是头节点)从链表中移除,将自己设置成头节点。该方法就会跳出死循环。

到这里,释放锁的过程就讲完了,其实很简单,就是当线程完完全全释放了锁,会唤醒当前链表中的没有取消的,离头结点最近的节点(一般就是链表中的第二个节点),然后被唤醒的节点就会获取到锁,将头节点设置为自己。

总结

相信看完这篇文章,大家对AQS的底层有了更深层次的了解。AQS其实就是内部维护一个锁的状态变量state和一个双向链表,加锁成功就将state的值加1,加锁失败就将自己当前线程放入链表的尾部,然后休眠,等待其他线程完完全全释放锁之后将自己唤醒,唤醒之后会尝试加锁,加锁成功就会执行业务代码了。

到这里本文就结束了,如果你有什么疑问欢迎私信告诉我。

如果觉得这篇文章对你有所帮助,还请帮忙点赞、在看、转发一下,码字不易,非常感谢!

如果你想联系我,欢迎关注我的个人的微信公众号三友的java日记,每天都会发布技术性的文章,同时也可以添加我的微信ZZYNKXJH与我取得联系。期待与你一起进步。

Guess you like

Origin juejin.im/post/7079678735619719181