前言

本文是对之前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. 跟面试官侃半小时MySQL事务隔离性,从基本概念深入到实现

    提到MySQL的事务,我相信对MySQL有了解的同学都能聊上几句,无论是面试求职,还是日常开发,MySQL的事务都跟我们息息相关. 而事务的ACID(即原子性Atomicity.一致性Consiste ...

  2. Smallest Range II

    2020-01-21 21:43:52 问题描述: 问题求解: 这个题目还是有点难度的,感觉很巧妙也很难想到. 整体的思路如下: 1. 首先原问题等价于 +0 / + 2*K 2. 那么res = M ...

  3. NLP interview

    2019-08-26 17:19:58 1)聊实习项目 2)代码题,二维数组中的查找某个target 3)讲一些最能体现创新能力的工作,而不是一些工程上的实现 4)讲论文可以从哪些方面做创新点,文本生 ...

  4. [图中找环] Codeforces 659E New Reform

    New Reform time limit per test 1 second memory limit per test 256 megabytes input standard input out ...

  5. 强化学习之七:Visualizing an Agent’s Thoughts and Actions

    本文是对Arthur Juliani在Medium平台发布的强化学习系列教程的个人中文翻译,该翻译是基于个人分享知识的目的进行的,欢迎交流!(This article is my personal t ...

  6. SFDC 401 最新考试真题

    上周通过了SFDC 401 考试,一下是对考试题的回忆. 1. Using a formula field how would a developer calculate the number of ...

  7. swift 网络请求中含有特殊字符的解决方案

    在网络请求时,URL出现中文或特殊字符时会造成请求失败,通常可使用  addingPercentEncoding(withAllowedCharacters: CharacterSet) 方法进行解决 ...

  8. 【深度强化学习】Curriculum-guided Hindsight Experience Replay读后感

    目录 导读 目录 正文 Abstract[摘要] Introduction[介绍] 导读 看任何一个领域的文章,一定要看第一手资料.学习他们的思考方式,论述逻辑,得出一点自己的感悟.因此,通过阅读pa ...

  9. 《自拍教程52》Python_adb运行Shell脚本

    Android作为一款Linux终端,肯定是支持.sh后缀的Shell脚本的运行的, 有时候测试环境准备或者长时间截取复杂的日志等,开发会给到一些Shell脚本. Shell脚本的执行的优势: 快捷高 ...

  10. iOS 内存分配与分区

    RAM ROM RAM:运行内存,不能掉电存储. ROM:存储性内存,可以掉电存储,例如内存卡.Flash. 由于 RAM 类型不具备掉电存储能力(即一掉电数据消失),所以 app 程序一般存放于 R ...