ReentrantLock
一. 概述
ReentrantLock底层基于AQS,其构造方法返回的就是NonfaireSync和faireSync;
两种同步器都继承自Sync,Sync又继承自AQS!
new ReentrantLock时,返回的就是同步器,默认是非公平的;
二. 原理分析
非公平锁、可重入锁、可中断锁、条件变量锁 都是基于底层 state属性(volatile修饰)和阻塞队列来配合实现的;
1. 非公平锁的加锁
1. 1 加锁成功
调用lock() 方法,使用CAS
机制尝试将AQS底层 volatile
修饰的 state 属性改为1,并将当前线程设为exclusiveOwnerThread
即占有锁;
若CAS修改失败即出现竞争,则进入acquire() 方法;
1. 2 加锁失败
- 当锁已经被占用,共享内存中的 state属性是1,compareAndSet(0,1) 会失败,CAS更改state属性失败,会进入 acquire() 方法;
- 进入else中 AQS的 acquire() 方法;
- 调用 tryAcquire() 再次尝试,如果还失败,再执行acquireQueued() ,如果是第一次会创造一个哨兵节点,然后再创造一个Node节点对象关联线程,添加到FIFO等待队列(双向链表)中;
- 节点进入队列后,会循环尝试加锁,失败后会被park阻塞,节点前驱节点的
waitStatus
会被置为 -1,-1表示可以去唤醒后继节点;
AQS中的acquire()方法:
假设多个线程多竞争失败,进入了FIFO等待队列:
1. 非公平锁的释放
1.1 unlock() 释放锁+唤醒线程
- 调用 unlock() 方法,底层调用了release() 方法,使用tryRelease() 将state置为0,并将exclusiveOwnerThread置为null,即释放锁;
- tryRelease() 返回true后,判断head哨兵节点是否为null ,如果否 且哨兵节点的waitStatus 也不会0(为-1),则哨兵节点的后继节点被
unParkSuccessor
唤醒 !线程就恢复允许了,就有机会去竞争锁;原来的节点就从队列中断开;
1.1 竞争失败
如果此时又来了一个新的线程,那么就会和队列中要被释放的线程一起竞争锁,
如果新的线程获取到了锁成为了exclusiveOwnerThread,则队列中的线程解锁失败,
在for中循环tryAcquire返回false,又被阻塞住;
2. 可重入锁
2.1 加锁(state自增)
加锁最终调用nonFairTryAcquire( 1 ), 先获取state值,
①如果state=0即无锁,并使用CAS将state改为1,并将当前线程设为exclusiveOwnerThread,即占有锁;
②如果state=1,则判断当前线程是否等于exclusiveOwnerThread,如果是即锁重入,就让state值++;
2.2 解锁(state自减)
调用tryRelease(1)方法,使state减去参数中的1,
如果减1之后state为0,则将exclusiveOwnerThread设为null,即释放锁,并返回true;
如果state减1之后不为0,则返回fasle;
3. 可中断机制(跳出阻塞)
【不中断的模式】下,即使它被打断,会仍然驻留在AQS的等待队列中被阻塞;
【可打断模式】下,如果没有获得锁,进入 doAcquireInterruptibly() 方法而不是acquireQueued(),在线程被park阻塞的状态时,如果被interrupt则会抛出异常,线程就不会在AQS中的等待队列继续等待;
4. 公平锁(加锁时 看排队位置)
非公平锁:
加锁时,最终调用nonFairAcquire(),如果state=0,则直接用CAS机制将state改为1,设置Owner线程,不去检查AQS的等待队列;
公平锁:
1.最终调用 tryAcqure() 方法,会用hasQueuePredecessors()方法检查当前线程在AQS等待队列中的位置;
2.hasQueuePredecessors即 检查当前这个线程在AQS队列中的位置是否处于head.next(最优先的位置,head是哨兵节点不关联线程),
如果线程是 就使用CAS机制将state设为1,并设置当前线程为exclusiveOwnerThread,
不是就不会往下执行,不会用当前线程占有锁;
5. 条件变量
ConditionObject条件变量的等待队列 类似Moniter的waitset队列 !
每个条件变量其实就对应着一个等待队列(双向链表),实现类是AQS中的ConditionObject;
5.1 await() 加入条件变量队列+fullRelease
前提:起初state=1,线程-0是Owner线程占用着锁,调用await()
:
- 进入addConditionWaiter()方法,会创建节点关联当前线程-0,并将节点加入到 ConditionObject条件变量的队列 中去,然后将节点的waitStatus状态设为 -2 ,并且执行
park()
阻塞该线程;
(-2在条件变量里是等待的状态);
(如果队列为空,就将节点作为firstNode,如果不为空就加到队列的尾部); - 执行fullyRelease():然后将当前这个线程占用的锁都释放掉,因为可能有锁重入;
- 释放锁都会在同步器的阻塞队列中head的后继节点唤醒,即unparkSuccesser(),后面的线程就能去竞争锁;
5.2 singal()
只有Owner线程才有资格唤醒条件变量中的线程;
假设Thread-1 唤醒Thread-0:
- 先判断当前线程是不是锁的持有者,如果不是则报错;
- 获取ConditionObject队列的头元素firstWaiter,如果不为空,就调用doSignale()将其移除等待队列,
然后再将这个节点转移到同步器的阻塞队列中并将waitStatues改为0(因为阻塞队列最后一个节点的waitStatues是0), 这样线程就有机会抢占锁了;
如果遇到中断等,就取消转移,将等待队列的下一个节点来唤醒;
synchronized和ReentrantLock的区别 ?
ReentrantLock底层基于AQS实现,即使用volatile修饰的state属性和阻塞队列来实现线程的串行执行,从而达到线程安全性的目的;
ReentrantLock是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(有更多的方法),能解决synchronized关键字在一些并发场景下不适用的问题;
-
synchronized 依赖于 JVM, 而 ReentrantLock 依赖于 juc的API;
-
ReentrantLock可以指定是公平锁还是非公平锁。⽽synchronized只能是非公平锁;
-
synchronized只能等待同步代码块执行结束,不可以中断,而reentrantlock可以调用线程的interrupt方法来中断等待;
-
可以支持多个变量:类似于调用wait方法时,不满足条件的线程进入waitset队列等待CPU随机调度,支持多个变量表示支持多个类似自定义waitset,这样就可以指定对象来唤醒了。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/89218.html