ReentranLock实现原理
原文链接:https://blog.csdn.net/jeffleo/article/details/56677425
一、ReentranLock
相信我们都使用过ReentranLock,ReentranLock是Concurrent包下一个用于实现并发的工具类(ReentrantReadWriteLock、Semaphore、CountDownLatch等),它和Synchronized一样都是独占锁,它们两个锁的比较如下:
1. ReentrantLock实现了Lock接口,提供了与synchronized同样的互斥性和可见性,也同样提供了可重入性。
2. synchronized存在一些功能限制:无法中断一个正在等待获取锁的线程,无法获取一个锁时无限得等待下去。ReentrantLock更加灵活,能提供更好的活跃性和性能,可以中断线程
3. 内置锁的释放时自动的,而ReentrantLock的释放必须在finally手动释放
4. 在大并发量的时候,ReentranLock的效率会比Synchronized好很多
5. Lock可以进行可中断的(lock.lockInterruptibly())、可超时的(tryLock(long time, TimeUnit unit))、非阻塞(tryLock())的方式获取锁
一个并发工具自然最基本的功能就是获取锁和释放锁,那么有没有想过,ReentranLock是如何来实现并发的?既然ReentranLock可以中断线程,所以内部自然不可能使用synchronized来实现。事实上,ReentranLock只是一个工具类,它内部的的实现都是通过一个AbstractQueuedSynchronizer(简称AQS)来实现的,AQS是整个Concurrent包中最核心的地方,其它的并发工具也都是使用AQS来实现的,因此,以下我们就通过ReentranLock来分析AQS是如何实现的!
二、AQS
站在使用者的角度,AQS的功能可以分为两类:独占功能和共享功能,它的所有子类中,要么实现并使用了它独占功能的API,要么使用了共享锁的功能,而不会同时使用两套API,即便是它最有名的子类ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别实现的两套API来实现的,为什么这么做,后面我们再分析,到目前为止,我们只需要明白AQS在功能上有独占控制和共享控制两种功能即可
AQS类中,有一个叫做state的成员变量,在ReentranLock他表示获取锁的线程数,假如state=0,表示还没有现成获取锁;1表示已经有现成获取了锁;大于1表示重入的数量
三、ReentranLock的源码
首先我们要对ReentranLock有一个大体的了解,ReentranLock分为公平锁和非公平锁,并且ReentranLock是AQS独占功能的体现
公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,就像排队一样
非公平锁:表示获取锁的线程是不定顺序的,谁运气好,谁就获取到锁
可以看到,两个锁都是继承了一个叫做Sync的类,并且都分别有两个方法lock和tryAcquire,那我们看看Sync这个类:
原来,Sync继承自AQS,并且公平锁和非公平锁的两个方法lock和tryAcquire都是重写了Sync的方法,这也就验证了ReentrantLock的实现原理就是AQS
到这里,我们已经有了基本的认识,那么我们就想想,公平锁和非公平锁该如何实现:
有那么一个被volatile修饰的标志位叫做key(其实就是上面所说的AQS中的state),用来表示有没有线程拿走了锁,还需要一个线程安全的队列,维护一堆被挂起的线程,以至于当锁被归还时,能通知到这些被挂起的线程,可以来竞争获取锁了。
因此,公平锁和非公平锁唯一的区别就是获取锁的时候,是先直接去获取锁还是先进入队列中等待
四、ReentranLock的加锁
我们来看看ReentranLock是如何加锁的:
公平锁
公平锁调用lock时,会直接调用父类AQS的acquire方法,这里传入1,很简单,就是告知有一个线程要获取锁,这里是定死的;因此,相反,在释放锁的时候,也是传入1
在acquire中,首先调用tryAcquire,目的尝试获取锁,如果获取不到,就调用addWaiter创建一个waiter(当前线程)防止到队列中,然后自身阻塞,那我们来看看如何尝试获取锁?(注意:两个锁都重写了AQS的tryAcquire方法)
- protected final boolean tryAcquire(int acquires) {
- //首先得到获取锁的当前线程
- final Thread current = Thread.currentThread();
- //获取当前state
- int c = getState();
- //如果当前没有线程获取锁
- if (c == 0) {
- //hasQueuedPredecessors表示当前队列是否有线程在等待
- //表示没有线程在等待,同时采用CAS更新state的状态
- if (!hasQueuedPredecessors() &&
- compareAndSetState(0, acquires)) {
- //然后设置一个属性exclusiveOwnerThread = current,记录锁被当前线程拿去
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- //如果c != 0,说明已经有线程获取锁,并且getExclusiveOwnerThread == current,表示当前正在获取锁的就是当前锁,所以这里是重入!
- else if (current == getExclusiveOwnerThread()) {
- //重入的话,让状态为state+1,表示多一次重入
- int nextc = c + acquires;
- //如果当前状态<0,说明出现异常
- if (nextc < 0)
- throw new Error("Maximum lock count exceeded");
- //设置当前标志位
- setState(nextc);
- return true;
- }
- //如果锁已经被获取,并且又不是重入,所以返回false,表明获取锁失败
- return false;
- }
- }
获取锁的逻辑上面说得很明白了,但是这里需要了解的是CAS操作和队列的数据结构,这个下面在说,我们接着看,回到tryAcquire中
如果获取锁成功,则不操作;如果获取锁失败,则调用addWaiter并采取Node.EXECLUSIVE模式把当前线程放到队列中去,mode是一个表示Node类型的字段,仅仅表示这个节点是独占的,还是共享的
- private Node addWaiter(Node mode) {
- //把当前线程按照Node.EXECLUSIVE模式包装成1个Node
- Node node = new Node(Thread.currentThread(), mode);
- //用pred表示队列中的尾节点
- Node pred = tail;
- //如果尾节点不为空
- if (pred != null) {
- node.prev = pred;
- //通过CAS操作把node插入到列表的尾部,并把尾节点指向node如果失败,说明有并发,此时调用enq
- if (compareAndSetTail(pred, node)) {
- pred.next = node;
- return node;
- }
- }
- //如果队列为空,或者CAS失败,进入enq中死循环,“自旋”方式修改。
- enq(node);
- return node;
- }
先看下AQS中队列的内存结构,我们知道,队列由Node类型的节点组成,其中至少有两个变量,一个封装线程,一个封装节点类型。
而实际上,它的内存结构是这样的(第一次节点插入时,第一个节点是一个空节点,代表有一个线程已经获取锁,事实上,队列的第一个节点就是代表持有锁的节点):
- private Node enq(final Node node) {
- //进入死循环
- for (;;) {
- Node t = tail;
- //如果尾节点为null,说明队列为空
- if (t == null) {
- //此时通过CAS增加一个头结点(即上图的黄色节点),并且tail也指向头结点,之后下一次循环
- if (compareAndSetHead(new Node()))
- tail = head;
- } else {//否则,把当前线程的node插入到尾节点的后面
- node.prev = t;
- if (compareAndSetTail(t, node)) {
- t.next = node;
- //并返回插入结点的前一个节点
- return t;
- }
- }
- }
- }
这就完成了线程节点的插入,还需要做一件事:将当前线程挂起!,这里在acquireQueued内通过parkAndCheckInterrupt将线程挂起
- final boolean acquireQueued(final Node node, int arg) {
- boolean failed = true;
- try {
- boolean interrupted = false;
- for (;;) {
- final Node p = node.predecessor();
- //如果当前的节点是head说明他是队列中第一个“有效的”节点,因此尝试获取,
- if (p == head && tryAcquire(arg)) {
- //成功后,将上图中的黄色节点移除,Node1变成头节点。
- setHead(node);
- p.next = null; // help GC
- failed = false;
- //返回true表示已经插入到队列中,且已经做好了挂起的准备
- return interrupted;
- }
- //否则,检查前一个节点的状态为,看当前获取锁失败的线程是否需要挂起。如果需要,借助JUC包下的LockSopport类的静态方法Park挂起当前线程。知道被唤醒。
- if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
- interrupted = true;
- }
- } finally {
- if (failed) //如果有异常
- cancelAcquire(node);// 取消请求,对应到队列操作,就是将当前节点从队列中移除。
- }
- }
这块代码有几点需要说明:
1. Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢?
原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点[G4] 代表了一个线程的状态,有的线程可能“等不及”获取锁了,需要放弃竞争,退出队列,有的线程在等待一些条件满足,满足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition同样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总需要一个变量来描述它,这个变量就叫waitStatus,它有四种状态:
节点取消
节点等待触发
节点等待条件
节点状态需要向后传播。
只有当前节点的前一个节点为SIGNAL时,才能当前节点才能被挂起。
2. 对线程的挂起及唤醒操作是通过使用UNSAFE类调用JNI方法实现的。当然,还提供了挂起指定时间后唤醒的API,在后面我们会讲到。
我们来理一理思路:
1. 调用lock方法获取锁,而lock方法内值调用了AQS的acquire(1)
2. 然后尝试获取锁,如果当前state标志==0,表示还没有线程获取锁,然后再判断是否有队列在等待获取该锁,如果没有队列,说明当前线程是第一个获取该锁的线程,然后修改标志位,并且用一个变量exclusiveOwnerThread来记录当前线程获取了锁
3. 如果是重入状态,也修改state+1
4. 如果锁已被占取,获取失败
5. 如果获取失败,则把当前线程包装成一个Node,插入到队列中,
6. 否则,检查前一个节点的状态为,看当前获取锁失败的线程是否需要挂起。如果需要,借助JUC包下的LockSopport类的静态方法Park挂起当前线程。知道被唤醒。
非公平锁
这里可以看到,非公平锁,首先是直接去获取锁,如果有并发获取失败,调用AQS的acquire(1),然后acquire中调用非公平锁的tryAcquire,进而调用nonfairTryAcquire
- final boolean nonfairTryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- //如果当前没有现成获取锁,直接获取锁,然后设置一个属性exclusiveOwnerThread = current,记录锁被当前线程拿去,这里和公平所有细微的差别,公平所还要判断hasQueuedPredecessors()
- if (c == 0) {
- if (compareAndSetState(0, acquires)) {
- setExclusiveOwnerThread(current);
- return true;
- }
- }//如果是重入
- else if (current == getExclusiveOwnerThread()) {
- int nextc = c + acquires;
- if (nextc < 0) // overflow
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- //如果当前锁获取失败,返回false
- return false;
- }
其它的都和公平锁一样了,如果到这里都获取失败了,就会插入到队列中阻塞起来
总结公平锁和非公平锁
- 公平锁获取锁时,会老老实实得走AQS的流程去获取锁
- 非公平锁获取锁是,首先会抢占锁,达到不排队的目的,如果抢占失败,只能老老实实排队了
五、ReentrantLock的释放锁
从上面我们可以知道,当锁已被占,获取锁的线程会一直在队列中排队(FIFO),那么我们想想,释放的时候该怎么做?
1. 首先锁的状态位要改变
2. 队列中的头结点去获取锁
我们来看看代码验证一下:
释放锁的时候调用unlock(),然后在方法中调用AQS的release方法
在release方法中,首先调用tryRelease方法,由于继承自AQS的Sync类重写了tryRelease方法,所以此时执行的是Sync的tryRelease方法
- protected final boolean tryRelease(int releases) {
- //这里传入的releases是1,跟获取锁时传入的1一致,更新state状态
- int c = getState() - releases;
- //如果当前占领锁的线程不是尝试释放锁的线程,会抛出非法异常
- if (Thread.currentThread() != getExclusiveOwnerThread())
- throw new IllegalMonitorStateException();
- boolean free = false;
- //如果释放成功,则修改获取锁的变量为null,但是因为是重入的关系,不是每次释放锁c都等于0,直到最后一次释放锁时,才通知AQS不需要再记录哪个线程正在获取锁
- if (c == 0) {
- free = true;
- setExclusiveOwnerThread(null);
- }
- setState(c);
- return free;
- }
此时已经释放了锁,然后便通知队列头部的线程去获取锁
寻找的顺序是从队列尾部开始往前去找的最前面的一个waitStatus小于0的节点,找到这个及节点后,利用LockSopport类将其唤醒,这个waitStatu前面说过了,不记得了到前面看看。
六、总结
在Concurrent包中,基本上并发工具都是使用了AQS作为核心,因此AQS也是并发编程中最重要的地方!我们从ReentrantLock出发,去探讨了AQS的实现原理,其实并不难,AQS中采用了一个state的状态位+一个FIFO的队列的方式,记录了锁的获取,释放等,这个state不一定用来代指锁,ReentrantLock用它来表示线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态(尚未开始,正在运行,已完成以及以取消)。同时,在AQS中也看到了很多CAS的操作。AQS有两个功能:独占功能和共享功能,而ReentranLock就是AQS独占功能的体现,而CountDownLatch则是共享功能的体现
ReentranLock实现原理的更多相关文章
- 分析ReentrantLock的实现原理
http://www.jianshu.com/p/fe027772e156 什么是AQS AQS即是AbstractQueuedSynchronizer,一个用来构建锁和同步工具的框架,包括常用的Re ...
- Java面试底层原理
面试发现经常有些重复的面试问题,自己也应该学会记录下来,最好自己能做成笔记,在下一次面的时候说得有条不紊,深入具体,面试官想必也很开心.以下是我个人总结,请参考: HashSet底层原理:(问了大几率 ...
- AbstractQueuedSynchronizer(AQS) 超详细原理解析
java.util.concurrent包中很多类都依赖于这个类AbstractQueuedSynchronizer所提供的队列式的同步器,比如说常用的ReentranLock,Semaphore和C ...
- 嘿嘿,我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了,看我怎么秀他
前言 上篇文章介绍了 HashMap 源码后,在博客平台广受好评,让本来己经不打算更新这个系列的我,仿佛被打了一顿鸡血.真的,被读者认可的感觉,就是这么奇妙. 然后,有读者希望我能出一版 Concur ...
- AbstractQueuedSynchronizer和ReentranLock基本原理
先把我主要学习参考的文章放上来先,这篇文章讲的挺好的,分析比较到位,最好是先看完这篇文章,在接下去看我写的.不然你会一脸懵逼,不过等你看完这篇文章,可能我的文章对你也用途不大了 深入分析Abstrac ...
- Java并发之AQS原理解读(二)
上一篇: Java并发之AQS原理解读(一) 前言 本文从源码角度分析AQS独占锁工作原理,并介绍ReentranLock如何应用. 独占锁工作原理 独占锁即每次只有一个线程可以获得同一个锁资源. 获 ...
- Java并发之AQS原理解读(一)
前言 本文简要介绍AQS以及其中两个重要概念:state和Node. AQS 抽象队列同步器AQS是java.util.concurrent.locks包下比较核心的类之一,包括AbstractQue ...
- 奇异值分解(SVD)原理与在降维中的应用
奇异值分解(Singular Value Decomposition,以下简称SVD)是在机器学习领域广泛应用的算法,它不光可以用于降维算法中的特征分解,还可以用于推荐系统,以及自然语言处理等领域.是 ...
- node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理
一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...
随机推荐
- mysql的默认隔离级别
原文:https://www.cnblogs.com/rjzheng/p/10510174.html 知识点总结 ------------------------------------------- ...
- hive与hbase整合方式和优劣
分别安装hive 和 hbase 1.在hive中创建与hbase关联的表 create table ganji_ranks (row string,num string) STORED BY 'or ...
- 【WebRTC】术语
WebRTC,名称源自网页实时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API.它于2011年6月1日开源并在Goog ...
- 每天一道算法题(15)——打印1到最大的n位数
题目: 打印1到最大的n位数.如n=4,打印1-9999. 思路: 由于直接使用循环会导致int或者long long都不够存储.因此使用字符串来存储数据,这里涉及到数字转换成字符串以及字符串的加法. ...
- 第2章 netty介绍与相关基础知识
NIO有一个零拷贝的特性.Java的内存有分为堆和栈,以及还有字符串常量池等等.如果有一些数据需要从IO里面读取并且放到堆里面,中间其实会经过一些缓冲区.我们要去读,它会分成两个步骤,第一块它会把我们 ...
- day36-hibernate检索和优化 02-Hibernate检索方式:简单查询及别名查询
Hibernate: insert into Customer (cname) values (?)Hibernate: in ...
- SQl Server 与数据库的第一次相遇
数据库就是 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库,简单说就是存储在硬盘上的文件. 市面上常见数据库有<关系数据库系统>: ORACLE(甲骨文).DB2.S ...
- Tensorflow手写数字识别训练(梯度下降法)
# coding: utf-8 import tensorflow as tffrom tensorflow.examples.tutorials.mnist import input_data #p ...
- 硬链接与软链接有什么不同(ln)
Linux建立的链接有两种方式 如Windows系统下的快捷方式(.lnk)相似的东东 分为硬链接(Hard Link)和软链接(Symbolic Link)也叫符号链接 默认情况下,ln命令产生硬链 ...
- EZOJ #82
传送门 分析 首先我们发现$k$位数实际就是一位的情况的$k$次方 考虑一开始的总方案数是$2^{nm}$ 我们每一次枚举其中有$i$行$j$列 对于这种情况的容斥系数为$(-1)^{i+j}$ 方案 ...