ReentrantLock 中的加锁操作都是通过Syn这个抽象类来完成,具体解析在之前得博客已经分析过了,请参考:ReentrantLock AQS操作解析

得不到锁的线程,如何排队?

JUC中锁的排队策略,是基于CLH队列的变种实现的。因此,我们先看看啥是CLH队列

CLH队列

如上图所示,获取不到锁的线程,会进入队尾,然后自旋,直到其前驱线程释放锁。可能会有同学问为啥有2种指针在node上面,这个之后会在解释
这样做的好处:假设有1000个线程等待获取锁,锁释放后,只会通知队列中的第一个线程去竞争锁,减少了并发冲突。(ZK的分布式锁,为了避免惊群效应,也使用了类似的方式:获取不到锁的线程只监听前一个节点)

为什么说JUC中的实现是基于CLH的“变种”,因为原始CLH队列,一般用于实现自旋锁。而JUC中的实现,获取不到锁的线程,一般会时而阻塞,时而唤醒。

UC中的CLH队列实现

我们来看看AbstractQueuedSynchronizer类中的acquire方法实现

    public final void acquire(int arg) {//尝试获取锁if (!tryAcquire(arg) &&//获取不到,则进入等待队列,返回是否中断acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果返回中断,则调用当前线程的interrupt()方法selfInterrupt();}

入队

如果线程调用tryAcquire(其最终实现是调用上面分析过的nonfairTryAcquire方法)获取锁失败。则首先调用addWaiter(Node.EXCLUSIVE)方法,将自己加入CLH队列的尾部。

    private Node addWaiter(Node mode) {//线程对应的NodeNode node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;//尾节点不为空if (pred != null) {//当前node的前驱指向尾节点node.prev = pred;//将当前node设置为新的尾节点//如果cas操作失败,说明线程竞争if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//lockfree的方式插入队尾enq(node);return node;}private Node enq(final Node node) {//经典的lockfree算法:循环+CASfor (;;) {Node t = tail;//尾节点为空if (t == null) { // Must initialize//初始化头节点if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

入队过程,入下图所示

1 T0持有锁时,其CLH队列的头尾指针为NULL (没有任何node来获取锁时,锁会给到默认得head-node持有,也就是T0)

2 线程T1,此时请求锁,由于锁被T0占有。因此加入队列尾部。具体过程如下所示:
(1) 初始化头节点
(2) 初始化T1节点,入队,尾指针指向T1

3 此时如果有一个T10线程先于T1入队,则T1执行compareAndSetTail(t, node)会失败,然后回到for循环开始处,重新入队。

由自旋到阻塞

入队后,调用acquireQueued方法,时而自旋,时而阻塞,直到获取锁(或被取消)。

  final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//其前驱是头结点,并且再次调用tryAcquire成功获取锁if (p == head && tryAcquire(arg)) {//将自己设为头结点setHead(node);p.next = null; // help GCfailed = false;//成功获取锁,返回return interrupted;}//没有得到锁时://shouldParkAfterFailedAcquire方法:返回是否需要阻塞当前线程//parkAndCheckInterrupt方法:阻塞当前线程,当线程再次唤醒时,返回是否被中断if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//修改中断标志位interrupted = true;}} finally {if (failed)//获取锁失败,则将此线程对应的node的waitStatus改为CANCELcancelAcquire(node);}}private void setHead(Node node) {head = node;node.thread = null;node.prev = null;}/*** 获取锁失败时,检查并更新node的waitStatus。* 如果线程需要阻塞,返回true。*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;//前驱节点的waitStatus是SIGNAL。if (ws == Node.SIGNAL)/* * SIGNAL状态的节点,释放锁后,会唤醒其后继节点。* 因此,此线程可以安全的阻塞(前驱节点释放锁时,会唤醒此线程)。*/return true;//前驱节点对应的线程被取消if (ws > 0) {do {//跳过此前驱节点node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/*此时,需要将前驱节点的状态设置为SIGNAL。* waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}//当shouldParkAfterFailedAcquire方法返回true,则调用parkAndCheckInterrupt方法阻塞当前线程private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

自旋过程,入下图所示

然后线程T2,加入了请求锁的队列,尾指针后移。

终上所述,每个得不到锁的线程,都会讲自己封装成Node,加入队尾,或自旋或阻塞,直到获取锁

锁的释放

前文提到,T1,T2在阻塞之前,都回去修改其前驱节点的waitStatus=-1。这是为什么?
我们看下锁释放的代码,便一目了然

    public final boolean release(int arg) {//修改锁计数器,如果计数器为0,说明锁被释放if (tryRelease(arg)) {Node h = head;//head节点的waitStatus不等于0,说明head节点的后继节点对应的线程,正在阻塞,等待被唤醒if (h != null && h.waitStatus != 0)//唤醒后继节点unparkSuccessor(h);return true;}return false;}private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);//后继节点Node s = node.next;//如果s被取消,跳过被取消节点if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)//唤醒后继节点LockSupport.unpark(s.thread);}

如代码所示,waitStatus=-1的作用,主要是告诉释放锁的线程:后面还有排队等待获取锁的线程,请唤醒他!

释放锁的过程,如图所示:


面试经典必问:ReentrantLock 中CLH队列相关推荐

  1. 2020年Java面试经典100问,进入BAT不是梦

    本文由公众号「Java旅途」整理,设计到的内容由java基础.数据库.SSM框架.redis.消息队列.spring boot.spring cloud.git及一些前端知识.整理时间为2019-11 ...

  2. stm32经典笔试题_嵌入式面试经典30问

    嵌入式面试经典30问 在经过4个多月的学习后,学员们最关心的问题莫过于如何拿到高薪offer问题了. 但是很多同学说很害怕面试,看见面试官会露怯,怕自己的知识体系不完整,怕面试官考的问题回答不上了,所 ...

  3. 随笔 - 《阿里巴巴产品经理面试之必问列表》- 20201210

    <阿里巴巴产品经理面试之必问列表> 说明: • 以下问题是拷贝而来,经过修改,但基本是我和我的同事在面试中都会问到的问题,大多数是针对产品新人. • 所有问题本身是没有标准答案,故没有提供 ...

  4. 面试必备:高频算法题汇总「图文解析 + 教学视频 + 范例代码」必问之 链表 + 栈 + 队列 部分!

    链表 链表是最基本的数据结构,面试官也常常用链表来考察面试者的基本能力,而且链表相关的操作相对而言比较简单,也适合考察写代码的能力.链表的操作也离不开指针,指针又很容易导致出错. 综合多方面的原因,链 ...

  5. 校招面试真题 | 面试官必问面试题之你有什么想问我的?

    面试必问问题:你有什么想问我的? 很多同学面试时遇到这个问题,尤其是刚刚去面试,绝对是一脸懵逼.很多同学心里想,是不是可以问一问能拿多少钱?好像我对于薪水这个问题,我也想不出来有啥问题能问了呀~ 如果 ...

  6. 前端主流面试官必问超详细面试题(整理完以秃头)持续更新中

    前端优化 我们可以使用以下几种方式做前端优化 CDN: CDN利用最靠近每位用户的服务器,更快.更可靠地将音乐.图片.视频.应用程序及其他文件发送给用户,来提供高性能.可扩展性及低成本的网络内容传递给 ...

  7. java综合面试题_综合性18道面试官必问经典Java面试题!

    Java具有简单性.面向对象.分布式.健壮性.安全性.平台独立与可移植性.多线程.动态性等特点 .Java可以编写桌面应用程序.Web应用程序.分布式系统和嵌入式系统应用程序等. 线程的概念 线程进程 ...

  8. 一文详解:字节面试官必问的Mysql锁机制

    一面 1 自我介绍和项目 2 Java的内存分区 3 Java对象的回收方式,回收算法. 4 CMS和G1了解么,CMS解决什么问题,说一下回收的过程. 5 CMS回收停顿了几次,为什么要停顿两次. ...

  9. 嵌入式邻域面试官必问的问题

    简介:本文汇总了嵌入式行业在校招或者社招中,笔试和面试常问或常考的题目,很多都是基础知识,但需要你对问题有个深刻的认识和理解.话不多说,请看汇总: 本文参考:小米嵌入式研发工程师校招面试总结 1.C语 ...

  10. 大厂面试官必问的Mysql锁机制

    前言 前几天有粉丝和我聊到他找工作面试大厂时被问的问题,因为现在疫情期间,找工作也特别难找.他说面试的题目也比较难,都偏向于一两年的工作经验的面试题. 他说在一面的时候被问到Mysql的面试题,索引那 ...

最新文章

  1. 加密日记 android,深挖Android加密到崩溃
  2. html的body内标签之图片及表格
  3. 360 开源企业级 Kubernetes 多集群管理平台 Wayne
  4. iframe实现页面无刷新上传文件(PHP)----备忘
  5. 蒙特 卡罗方法matlab,蒙特·卡罗方法中的数学之美,你一定不想错过
  6. ShardingSphere(八) 分库分表的多种分片策略
  7. elasticsearch6 php,elasticsearch 6.x php-client
  8. php写ssh命令行_php使用ssh2来操作服务器执行命令
  9. 形容时间过得快的句子,一些表示时间过得快的句子
  10. Python 处理各种编码的字符串
  11. MATLAB 创建不定长数组
  12. 通达OA v12流程中心
  13. Chrome和edge浏览器书签本地路径以及批量修改书签
  14. Python字符串和列表常用的方法和操作
  15. Crime and Punishment
  16. postgresql 的json 和jsonb 的使用
  17. 如何让ipad成为电脑的扩展屏
  18. CAD参数绘制文字(网页版)
  19. 【随笔记】我与Java的故事
  20. 天翼数字生活C++客户端实习

热门文章

  1. (已解决)网页不显示数学公式||只显示源码
  2. 即时通讯IM 与系统集成
  3. Android+SpringBoot+Vue实现安装包前台上传,后台管理,移动端检测自动更新
  4. Excel常用技巧笔记
  5. MATLAB--数字图像处理 车牌识别之分离字符
  6. Excel如何批量添加邮箱后缀
  7. 计算机与信息技术基础读书笔记,信息技术读书笔记
  8. [全程动图]解决Offline Explorer崩溃闪退的问题和一些小技巧(如何下载js、100线程下载)
  9. 为什么认真自学了NLP,面试还是回答不出问题
  10. 机器人之自动回归原点方法实现