Java并发编程-浅谈ReentranLock
1. 产生背景
在Java中已经有内置锁synchronized的情况下,为什么还需要引入ReentranLock呢?
synchronized是基于 JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh
基于这样一个背景,Doug Lea在JDK1.5中提供了API层面的互斥锁ReentranLock,实现了可重入、可中断、公平锁等特性。在synchronized优化之前,synchronized的性能比起ReentranLock还是有差距的。
2. 简单使用

3. 源码分析
3.1 ReentrantLock结构
首先,RentrantLock对象结构,有个大致的了解。
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh

3.2 加锁 lock.lock()
其实,ReentrantLock中的方法都是由成员对象sync完成的。
所以,我们直接进入 NonfairSync.lock()
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh

接下来,分析 acquire()中的三个重点方法:
tryAcquire():尝试获取锁,由子类重写(这里实现了可重入、公平锁特性);
addWaiter():AQS中维护了一个链表,将当前线程包装成一个Node节点,入队;
acquireQueued():以独占不可中断模式获取已经在队列中的线程。
3.2.1 tryAcquire

当前方法无论是获取到了锁,还是重复加锁,都是返回true。上面的 acquire()就会直接结束,不会继续执行后续的入队操作。
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh
非公平锁的特性就是在这个方法中体现出来的(其实lock方法中也是直接获取锁,更直接)。
3.2.2 addWaiter
注意,当前方法调用时,传入了一个参数:Node.EXCLUSIVE(独占模式)。这个参数实际上是起到了一个标识的作用,有两种类型:独占、共享。

addWaiter()的职责很简单,就是将当前线程包装成一个Node节点,然后设置为队尾(必要时初始化队列),通过死循环的形式保证一定入队成功。
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh
下面,大致了解一下Node的结构(AbstractQueuedSynchronizer的内部类)

3.2.3 acquireQueued

代码执行到这里,说明节点(当前线程)已经成功入队。如果当前节点就是第一位候选者,就会尝试去获取锁。但是锁有可能还没有释放掉(state != 0),获取锁失败,就会阻塞当前线程。
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh

相信大家也看到了,这个方法中有段代码的写法太(bi)过(jiao)精(e)简(xin)。

其中,红色方块的pred和N1状态都是已取消的(状态值为1)。
首先,执行if判断时,发现pred的等候状态是已取消的,执行do代码块,将pred指向N1节点。执行while判断,发现N1节点的状态也是已取消的,再次执行do代码块,将pred指向N2节点。执行while判断,N2节点状态并不是已取消,循环结束。而在循环结束之前node的前驱指针已经指向了N2,循环结束后再将N2的后驱指针指向node,两者就建立了双向关联。
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh
细心的你一定发现了,在acquireQueued()方法中的死循环上注释了一般只会执行三次,那么为什么说会执行三次呢?
假设现在lock锁已经被某个线程获取了,并且还在执行同步代码块,没有来得及释放锁。这时,线程A也来执行了业务方法,然后尝试获取锁,必然获取失败进而执行入队操作。此时,由当前线程包装的Node节点占据队尾,队头是初始化的一个“空”节点。
参数pred 实际上就是head节点(状态值为0)。第一次循环,执行else逻辑,将前驱节点pred的状态值设置为SIGNAL,注意返回的是false,这将导致当前方法所在if判断直接结束;第二次循环,首个if判断生效,返回true,执行parkAndCheckInterrupt(),阻塞线程;直到线程被唤醒后,开启第三次循环,获取锁成功,直接返回,结束死循环。
注意:以上描述,队列中不存在已取消的节点并且锁不会被争抢,故而说是一般情况下。

不得不说,AQS的方法名取的还是很贴切的,基本上见名知意了。阻塞当前线程并返回线程中断状态!
我们先来了解一下 LockSupport.park(this)的功能点:
阻塞当前线程的执行,且不会释放当前线程占有的锁资源;
可以被另一个线程调用 LockSupport.unpark()方法唤醒;
底层调用 Unsafe的native方法。

3.2.4 总结
ReentranLock的一套加锁流程总结下来,就是尝试获取锁,获取成功,更新锁状态、设置独占线程;获取失败,将当前线程包装成一个Node节点,加入到AQS内部维护的一个链表的尾部,最后阻塞当前线程,直到被唤醒,再次尝试获取锁(非公平锁,存在被争抢的可能性)。
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh
3.3 解锁 lock.unlock()
加锁时自我阻塞的线程是如何被唤醒的,触发的机制又是怎样的?
接下来,让我们一步步的分析ReentranLock的另一个重要组件。

需要注意的是加锁和解锁的次数要一致,不然将会导致队列中的等候线程无法被唤醒。
3.3.2 unparkSuccessor

预唤醒的节点s 是head的后驱节点(FIFO)。
但是如果s 为空或者已取消时,就需要从队列中找出一个正常的节点。怎么找呢? 以队尾节点为起始点,向前遍历,最终s指向的是队列中最靠近队头的某个正常节点。
随后,通过调用 LockSupport.unpark()的方式,唤醒 s节点持有的线程。
至此,ReentranLock一套完整的加锁解锁流程就分析完毕了~
学习更多,请点击:https://www.bilibili.com/video/BV1Qb4y1D75J
https://www.bilibili.com/video/BV1qo4y1f7Uw
https://www.bilibili.com/video/BV1s64y1i77s
https://www.bilibili.com/video/BV1g84y1F7vS
https://www.bilibili.com/video/BV1a54y1b7hh
作者:Hey Max!
链接:https://juejin.cn/post/6953154315603640351
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。