前言

本文是对之前AQS系列文章的一个小结,首先看看以下几个问题:

1、ReentrantLock和ReentrantReadWriteLock的可重入特性是如何实现的?

2、哪个变量控制着锁是否被占用?

3、多个线程竞争一个排它锁时,未抢到锁的线程是如何阻塞的?

4、读读真的可以一直共享不阻塞吗?

对于以上问题,你是否都能知道答案 ?是否都清楚其原理?如果是的话,就没必要阅读本文了,否则还请慢慢读来。

正文

1、可重入性的实现(针对排它锁)-问题1

可重入性是通过exclusiveOwnerThread和state一起实现的。在AQS的父类AbstractOwnableSynchronizer中有一个成员变量exclusiveOwnerThread来存放获取到独占锁时的线程,可重入的特性就是通过此变量实现的。如果根据state判断当前锁已被占用,那么再判断这个变量中存的是不是当前线程,如果是则获取到锁,锁计数+1,否则获取不到锁。(此处针对的是排它锁,共享锁不属于这个范畴)

2、AQS中state的值-问题2

state为volatile int类型,用于记录加锁状态和重入次数,但针对不同的实现类其记录方式又不同,下面分别进行说明:

【ReentrantLock】state用于记录锁的持有状态和重入次数,state=0表示没有线程持有锁;state=1表示有一个线程持有锁;state=N表示exclusiveOwnerThread这个线程N次重入了这个锁。

【ReentrantReadWriteLock】state用于记录读写锁的占用状态和持有线程数量(读锁)、重入次数(写锁),state的高16位记录持有读锁的线程数量,低16位记录写锁线程重入次数,如果这16位的值是0,表示没有线程占用锁,否则表示有线程持有锁。另外针对读锁,每个线程获取到的读锁次数由本地线程变量中的HoldCounter记录。

【Semaphore】:state用于计数。state=N表示还有N个信号量可以分配出去,state=0表示没有信号量了,此时所有需要acquire信号量的线程都等着;

【CountDownLatch】:state也用于计数,每次countDown都减一,减到0的时候唤醒被await阻塞的线程。

3、关于Node中waitStatus的值

waitStatus为volatile int 类型,有5种值,作用分别为:

1: CANCEL,即取消状态,这种状态的节点是无效节点,在执行时会直接略过,一般只有在特殊的异常场景中才会出现这种状态;

0:初始化状态,新建的Node节点都是这个状态;

-1:SIGNAL,即可唤醒状态,如果一个节点加锁失败需进入阻塞状态,必须先将它的前一个节点置为-1,自己才会进入park状态,在AQS中搜parkAndCheckInterrupt()可以发现,只要是这个方法出现的地方,前面一定有shouldParkAfterFailedAcquire(p, node)方法先将p的waitStatus置为-1;

-2:CONDITION,跟条件队列相关的状态,ReentrantReadWriteLock和ReentrantLock中暂未涉及;

-3:PROPAGATE,可传播状态,没看出来有什么用处。

4、关于head节点和tail节点

当第一个线程过来获取锁时,是不会初始化队列的,只是将exclusiveOwnerThread这个变量变为当前线程(独占锁的情况下),此时head和tail都是null;

第一个线程没执行完,第二个线程又来了,这时会初始化队列,new一个空Node赋值给head和tail,然后在tail(也就是head)后面拼上这个要排队的线程(详见下面的enq方法),此时形成了一个有两个节点的双向队列,head是new Node(),tail是新加入需排队的Node;然后再获取不到锁就会将head的waitStatus置为-1,自身挂起,等待unparkSuccessor(head)来唤醒。

addWaiter方法:如果tail不为空,则将当前的node节点赋值给tail,原先的tail变为新tail在链表上的前置节点。由此可知此链表的新增方式是后入式。这也解释了为什么唤醒线程时是从tail往前遍历找排在最前面符合条件的node节点。

 private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

在AQS类中的enq方法代码如下:跟addWaiter类似,只是多了一步初始化head/tail的步骤。

 1 private Node enq(final Node node) {
2 for (;;) {
3 Node t = tail;
4 if (t == null) { // Must initialize
5 if (compareAndSetHead(new Node())) // 初始化头节点,这时head指针指向的就是new Node()对象
6 tail = head; // 将tail也一起初始化了,这时会继续再走一遍for循环
7 } else {
8 node.prev = t; // 先将node的prev指针指向局部变量t;
9 if (compareAndSetTail(t, node)) { // 将tail所在内存地址的对象值换成node
10 t.next = node; // 将tail的next指针指向node节点,这时node节点就成了新的队尾了
11 return t; // 返回旧的队尾t
12 }
13 }
14 }
15 }

4、关于读读锁的有条件共享问题-问题4

ReentrantReadWriteLock中,读锁和读锁并不是永远都可以同时进行,如果当前运行的是读锁,而后面第一个排队的是写锁,那么再来新的读锁会进入链表排队阻塞而不是直接获取读锁,因为不这样设定则可能存在由于一直有读锁过来导致后面的写锁总是处于阻塞状态获取不到锁。

可参见readShouldBlock方法的非公平读锁实现-apparentlyFirstQueuedIsExclusive()方法

 final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}

5、Object的wait/notify和LockSupport的park/unpark方法-问题3

LockSupport对线程的唤醒和挂起,是基于Thread直接信号量进行的,而wait/notify是基于对象的,需要依赖于Monitor对象。

park/unpark的语义更容易理解,park是针对当前线程阻塞,unpark是针对指定线程唤醒,不像wait/notify那样违反常识。

unpark为指定线程提供一个许可,有这个许可线程就能继续执行;而park是等待一个许可。unpark可以在park之前执行,线程不会阻塞。但unpark不能重入叠加,多个unpark也会被一个park直接用掉。

在底层,UNSAFE的park/unpark方法操作的是Paker对象(每个线程都有一个Parker实例),对象中维护了一个_counter变量,调用unpark方法后变量从0变为1,表示拥有继续执行的许可,执行之后恢复为0,调用park方法后变量如果不是1则线程阻塞,直到变为1再唤醒继续执行。

关于AQS暂时就写这些,后面还会针对JUC包里的其他工具类进行学习。

AQS系列(七)- 终篇:AQS总结的更多相关文章

  1. 死磕 java线程系列之终篇

    (手机横屏看源码更方便) 简介 线程系列我们基本就学完了,这一个系列我们基本都是围绕着线程池在讲,其实关于线程还有很多东西可以讲,后面有机会我们再补充进来.当然,如果你有什么好的想法,也可以公从号右下 ...

  2. AQS系列(一)- ReentrantLock的加锁

    前言 AQS即AbstractQueuedSynchronizer,是JUC包中的一个核心抽象类,JUC包中的绝大多数功能都是直接或间接通过它来实现的.本文是AQS系列的第一篇,后面会持续更新多篇,争 ...

  3. 跟我学SpringCloud | 终篇:文章汇总(持续更新)

    SpringCloud系列教程 | 终篇:文章汇总(持续更新) 我为什么这些文章?一是巩固自己的知识,二是希望有更加开放和与人分享的心态,三是接受各位大神的批评指教,有任何问题可以联系我: inwsy ...

  4. jackson学习之十(终篇):springboot整合(配置类)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  5. JUnit5学习之八:综合进阶(终篇)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  6. OpenFaaS实战之九:终篇,自制模板(springboot+maven+jdk8)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  7. disruptor笔记之八:知识点补充(终篇)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  8. 支持JDK19虚拟线程的web框架,之五(终篇):兴风作浪的ThreadLocal

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos <支持JDK19虚拟线程的web框架>系列 ...

  9. AQS系列(二)- ReentrantLock的释放锁

    前言 在AQS系列(一)中我们一起看了ReentrantLock加锁的过程,今天我们看释放锁,看看老Lea那冷峻的思维是如何在代码中笔走龙蛇的. 正文 追踪unlock方法: public void ...

随机推荐

  1. 10个python爬虫入门实例

    昨天和伙伴萌一块学习,写了几个简单的入门实例 涉及主要知识点: web是如何交互的 requests库的get.post函数的应用 response对象的相关函数,属性 python文件的打开,保存 ...

  2. [剑指offer]3.数组中的重复数字

    3.数组中的重复数字 题目 找出数组中重复的数字. 在一个长度为 n 的数组 nums 里的所有数字都在 0-n-1 的范围内.数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了 ...

  3. 您知道SASS吗?

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文出处:https://blog.bitsrc.io/4-reasons-to-use-sass-in-y ...

  4. 破解WIFI教程

    今日主题:如何破解WIFI 准备工具 笔记本一台 usb无线网卡[我用的是小米的] kali系统[可以在虚拟机里装,建议用2019年及以下版本] VMware Workstation15虚拟机安装 可 ...

  5. wr720n v4 折腾笔记(二):刷入不死Uboot

    0x01 前言 接着上节刷入Openwrt开始说起,此次开始刷入不死Uboot,刷入之后就可以在Uboot里面随便刷机,再也不怕成砖了. 固件附件地址: 下载地址1(还是之前一的包) flash文件地 ...

  6. [简单路径] Useful Decomposition

    Ramesses knows a lot about problems involving trees (undirected connected graphs without cycles)! He ...

  7. vscode快速生成html模板(vscode快捷键"!"生成html模板)

    问题: 在vscode中新建test.html, 内容是空白的,输入"!",然后按tap键 ,没有生成常见的html模板,也就是如下: 输入! html html:5 DOCTYP ...

  8. 如何定时备份Mysql数据库

    1.创建备份数据库存储目录 cd data/db mkdir backup #创建存储目录 2.添加备份脚本 vim backupdb.sh #创建脚本文件 脚本内容如下: #!/bin/sh db_ ...

  9. python分布式接口,参数化实战二

    1,先看一下接口测试用例 2,文件1:写get和post模板 import requestsclass PostGetModels: def isMethod(self,url,data,method ...

  10. link与@import区别整理,一个表格带你了解

    网上有许多link和@import的文章,不过大多比较零散,个人觉得一个表格的话看起来能够直观的表达. 于是制作了如下表格: 关于权重这个存在着一些争议,这次碰巧看到了一篇的博客很好的解释了这个问题, ...