欢迎光临散文网 会员登陆 & 注册

Java并发编程-浅谈ReentranLock

2021-04-21 09:50 作者:光耀三十洲  | 我要投稿

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()中的三个重点方法:

  1. tryAcquire():尝试获取锁,由子类重写(这里实现了可重入、公平锁特性);

  2. addWaiter():AQS中维护了一个链表,将当前线程包装成一个Node节点,入队;

  3. 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)。

image

其中,红色方块的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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


Java并发编程-浅谈ReentranLock的评论 (共 条)

分享到微博请遵守国家法律