透过ReentrantLock窥探AQS
背景
JDK1.5引入的并发包提供了一系列支持中等并发的类,这些组件是一系列的同步器,几乎任一同步器都可以实现其他形式的同步器,例如,可以用可重入锁实现信号量或者用信号量实现可重入锁。但是,这样做带来的复杂性,开销,不灵活使其至多只能是个二流工程,且缺乏吸引力。如果任何这样的构造方式不能在本质上比其他形式更简洁,那么开发者就不应该随意地选择其中的某个来构建另一个同步器,所以JSR166建立了一个小框架-AQS(由Doug Lea设计),对这些同步器做了统一的抽象,为构造同步器提供了通用的机制,之后并发包中大部分同步器都基于AQS来实现。
注:本文通过ReentrantLock来窥探AQS的结构以及运行原理,因为AQS是并发包实现大部分同步器的框架,所以本文只对ReentrantLock相关方法做了解释说明,其他的方法在后面的文章中会继续做深入的解释
AQS设计
这是ReentrantLock中的内部类Sync的类图,图中可以看出Sync抽象类实现了AbstractQueuedSynchronizer。
ReentrantLock实现
ReentrantLock提供了非公平锁以及公平锁的能力,实现Lock接口,通过把功能实现委托给Sync同步器来实现。下面以非公平锁为例子,开始图解ReentrantLock类在调用lock方法时候的过程:
首先看下AQS的数据结构以及Node节点的结构
AQS的数据结构中state是最核心的变量,用来判断当前同步器是否有被线程占用,以及被同一个线程重入了多少次(重入锁实现的关键);
exclusiveOwerThread表示当前是哪个线程占用着同步器;
head是一个指向空的头结点的引用地址;
tail是一个指向等待同步器的最后一个节点的引用地址;
Node节点中最核心的是waitStatus,此处waitStatus的取值分别可以为:
- 1表示等待的线程已经取消或者中断;
- -1表示后一个节点需要唤醒,当前节点如果释放锁,则需要唤醒后继节点;
- -2表示当前的节点是一个条件等待,即需要等待其他的条件满足才能够被加入到同步队列,等待被唤醒
- -3表示下一个acquireShared应无条件传播(在读写锁中会遇到,后面会专门写文章分析读写锁)
- 0表示初始状态
看完AQS的数据结构之后,我们再图解ReentrantLock非公平锁的lock方法,看下代码
整个lock流程如下(这里只画了大概的流程,细节太多了,后面对着代码实现图解里面会有体现):
图解ReentrantLock非公平锁lock方法
下面代码是我写这个图解例子用的,有兴趣可以自己尝试下,其中Thread.sleep(60*60*1000)为了让线程一致占有锁(即同步器),这样后面增加的对该同步器的抢占才会形成同步队列,方便分析。
1. 初始状态,没有线程获取到AQS同步器
2. 按照上面的代码线程thread5第一个发起了lock,所以同步器的state变为1,exclusiveOwnerThread=thread5,此时还没有竞争同步器,所以head以及tail都是null。
3. 由于Thread.sleep方法是不会释放锁的,所以thread5会一直抢占着锁。当线程thread6执行lock的时候,由于同步器的state=1,所以抢占失败,执行acquires(1)方法
进入acquire(1)方法之后,其实还会再尝试抢一次锁,不管有没有等待节点在排队,所以非公平锁其实一个线程进来之后有两次机会抢占锁,如果抢不到就乖乖去排队,下图中选中的代码就是第二次抢占机会。
如果两次抢占都失败以后就只能增加一个等待节点,然后添加到同步队列的尾部。
非公平锁是独占模式,所以创建等待节点的时候会传入Node.EXCLUSIVE,设置到nextWaiter中
而这个Node.EXCLUSIVE的值其实是null,nextWaiter在AQS中其实有三种含义
- NULL:独占模式
- SHARD:共享模式
- 其他非空值:条件等待节点(调用Condition的await方法的时候)
节点创建成功之后需要把新创建的等待节点加入到同步队列的尾部
选中代码的意思就是如果已经有等待节点,那么直接插入到等待节点链表尾部(认为大部分情况下竞争其实并没有那么激烈,所以是可以直接插入成功的,所以代码如此设计),当然如果在高并发情况下插入失败了,那就执行常规的插入等待节点尾部的方法enq(node)(当没有等待队列的时候也需要执行enq方法,因为要初始化head以及tail节点)。
此处选中的代码就是当AQS的head以及tail为空的时候,初始化一个空节点,执行完以后是这样的结构
因为enq是在for的死循环里面的,所以会继续执行插入,直到成功插入到等待队列的尾部,再返回前继节点,那么线程thread6插入成功之后结构是这样的
到这里还没有结束,那么再继续再看下面的acquireQueued方法,代码如下
选中代码是一个死循环,可以认为是自旋,这里面可以分成两部分内容,如果node节点的前继节点是head节点(Empty Node),并且尝试把state从0设置为1,如果成功,就把当前节点设置为head节点(Empty Node),并且清空thread以及prev的值,这是在setHead方法中处理的。选中代码中的p.next=null,其实用意是前一个节点已经没有用了,把链接信息清空,再下一次垃圾回收的时候可以回收掉。
如果抢占锁没有成功,则会执行shouldParkAfterFailedAcquire方法,这个方法主要是用来设置前继节点的状态以及拿掉等待队列中已经取消的节点
新创建的节点加入到等待队列以后,其实还有一个事情没有做,就是要设置前继节点的waitStatus。
尾节点的waitStatus为默认值0,因为waitStatus的意义是为了标记后继节点的状态以及行为的。
所以for循环第一次进入shouldParkAfterFailedAcquire方法的时候,前继节点的waitStatus为0,会设置成-1,当再一次进入的时候会判断该值为-1,直接返回true。
中间的这段,就是从尾部开始往前,直到找到第一个小于等于0的等待节点,如下图:
大于0的值只有1,就是取消状态的节点,节点状态有4中,中间的节点状态不可能为0,因为每次添加进来之后都会被设置成-1,也不可能是-2,因为waitStatus值为-2的节点会进入条件等待队列,只有条件满足之后才会进入到同步队列,等待获取锁,同时把前继节点的waitStatus设置为-1,-3也是不可能的,因为-3是共享模式下才有,所以非公平锁独占模式下前继节点的值只可能为-1,0,1,最后的那段逻辑,直接设置前继节点的waitStatus为Node.SIGNAL(-1)就没有问题。
按照上面的逻辑处理完成之后,AQS的状态变成下面的样子
如果成功把新创建的线程加入到等待队列,那么需要让当前线程进入阻塞状态,执行方法parkAndCheckInterrupt,LockSupport就是前面文章写得AQS的基础
当该线程被唤醒的时候,会返回线程是否被中断,并清空中断标志,从这里就可以知道acquireQueued方法中的局部变量interrupted是干嘛用的了,就是判断线程被阻塞的时候有没有被中断,如果中断了,则返回之后执行selfInterrupt方法中断当前线程。
按照测试代码,最终形成的等待同步队列如下:
此时通过debug模式查看head以及后继节点如下:
其中线程thread5是在exclusiveOwnerThread变量中,如下图:
ReentrantLock公平锁
公平锁相对于非公平锁,其实就只有lock方法的区别,看下面的代码
lock方法中直接使用了acquire方法,相比于非公平锁的lock实现,公平锁少了第一次先尝试把state的值从0变1的过程。
再看tryAcquire方法也有点小区别,如果state=0,说明前一个执行的线程刚好执行完,但是后面还需要检查下是否后节点在同步队列排队,如果有节点在排队,那就不抢占了,直接加到同步队列尾部。
以上两点是公平锁实现和非公平锁实现的细微差别。
后续文章
AQS条件队列和同步队列的关系
透过ReentrantReadWriteLock窥探AQS
透过CountDownLatch窥探AQS
通过Semaphore窥探AQS
透过ReentrantLock窥探AQS的更多相关文章
- 透过CountDownLatch窥探AQS
本文来自公众号“Kahuna”,可搜索Alitaba119,欢迎关注,转载请注明出处,非常感谢 “ A synchronization aid that allows one or more thre ...
- 透过 ReentrantLock 分析 AQS 的实现原理
对于 Java 开发者来说,都会碰到多线程访问公共资源的情况,这时候,往往都是通过加锁来保证访问资源结果的正确性.在 java 中通常采用下面两种方式来解决加锁得问题: synchronized 关键 ...
- ReentrantLock 与 AQS 源码分析
ReentrantLock 与 AQS 源码分析 1. 基本结构 重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized ...
- 多线程学习笔记三之ReentrantLock与AQS实现分析
目录 简介 AQS同步状态 AQS同步队列 ReentrantLock数据结构 公平锁的获取 tryAcquire(arg) addWaiter(Node mode) acquireQueued(fi ...
- 面经手册 · 第17篇《码农会锁,ReentrantLock之AQS原理分析和实践使用》
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 如果你相信你做什么都能成,你会自信的多! 千万不要总自我否定,尤其是职场的打工人.如 ...
- 从ReentrantLock看AQS (AbstractQueuedSynchronizer) 运行流程
从ReentrantLock看AQS (AbstractQueuedSynchronizer) 运行流程 概述 本文将以ReentrantLock为例来讲解AbstractQueuedSynchron ...
- 基于ReentrantLock的AQS的源码分析(独占、非中断、不超时部分)
刚刚看完了并发实践这本书,算是理论具备了,看到了AQS的介绍,再看看源码,发现要想把并发理解透还是很难得,花了几个小时细分析了一下把可能出现的场景尽可能的往代码中去套,还是有些收获,但是真的很费脑,还 ...
- 扒一扒ReentrantLock以及AQS实现原理
提到JAVA加锁,我们通常会想到synchronized关键字或者是Java Concurrent Util(后面简称JCU)包下面的Lock,今天就来扒一扒Lock是如何实现的,比如我们可以先提出一 ...
- ReentrantLock 以及 AQS 实现原理
什么是可重入锁? ReentrantLock是可重入锁,什么是可重入锁呢?可重入锁就是当前持有该锁的线程能够多次获取该锁,无需等待.可重入锁是如何实现的呢?这要从ReentrantLock ...
随机推荐
- 3 View视图 URLconf
1.视图 视图接受Web请求并且返回Web响应 视图就是一个python函数,被定义在views.py中 响应可以是一张网页的HTML内容,一个重定向,一个404错误等等 响应处理过程如下图: 2 准 ...
- datagrid的formatter
1.formatter函数 formatter:function(value,rowData,rowIndex){ return 'xxx'; } 注意: (1)formatter一定要有返回,且返回 ...
- Shell脚本编程
1.linux中的变量 linux中的变量分为环境变量和普通变量,其中环境变量可以理解为全局变量,在所有shell的子程序中都可以引用,普通变量只能在自己的shell程序中使用,程序结束后变量无法保留 ...
- 精通CSS高级Web标准解决方案(3-1 背景图像与图像替换)
3.1背景图像基础 3.2图像替换 使用文本的图像并保留文本的方法.
- Leetcode 491.递增子序列
递增子序列 给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2. 示例: 输入: [4, 6, 7, 7] 输出: [[4, 6], [4, 7], [4, 6, 7] ...
- DFS和BFS遍历的问题
来自https://github.com/soulmachine/leetcode 广度优先搜索 输入数据:没有什么特征,不像dfs需要有递归的性质.如果是树/图,概率更大. 状态转换图:数或者DAG ...
- DownloadManager的使用
DownloadManager是系统开放给第三方应用使用的类,包含两个静态内部类DownloadManager.Query和DownloadManager.Request.DownloadManage ...
- MySql数据库 - 1.安装
下载: 官网:www.mysql.com 打开官网之后依次点击:DOWNLOADS - Windows - MySql Installer MySql Installer 包含的功能,使用C#连接数据 ...
- BZOJ 2190:[SDOI2008]仪仗队(欧拉函数)
[SDOI2008]仪仗队 Description 作为体育委员,C君负责这次运动会仪仗队的训练.仪仗队是由学生组成的N * N的方阵,为了保证队伍在行进中整齐划一,C君会跟在仪仗队的左后方,根据其视 ...
- [LOJ#2328]「清华集训 2017」避难所
[LOJ#2328]「清华集训 2017」避难所 试题描述 "B君啊,你当年的伙伴都不在北京了,为什么你还在北京呢?" "大概是因为出了一些事故吧,否则这道题就不叫避难所 ...