ReentrantLock 源代码之我见
ReentrantLock,英文意思是可重入锁。从实际代码实现来说,ReentrantLock也是互斥锁(Node.EXCLUSIVE)。与互斥锁对应的的,还有共享锁Node.SHARED
ReentrantLock 集成了Lock接口,Lock接口主要功能有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()。
ReentrantLock有个内部的抽象类Sync,这个Sync类继承了AbstractQueuedSynchronizer类,内部定义了抽象上锁方法lock(),还有非公平尝试上锁nonfairTryAcquire(int acquires),
尝试释放锁tryRelease(int releases) 、是否持有互斥锁isHeldExclusively()等方法。
Sync 有两个子类,非公平锁NonfairSync和公平锁FairSync。两个子类,都实现了抽象的方法上锁lock(),同时还有一个尝试上锁tryAcquire(int acquires)。在Sync的子类实现中,这个
tryAcquire(int acquires)的形参acquires都是1,表示加锁1次。这个加锁次数,维护在AQS里面的变量state中,这个后面会讲。
ReentrantLock 类内部,还有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()、获取加锁次数getHoldCount()
获取等待的条件hasWaiters()等。其中,最重要,也是最常用的,是lock()、unlock()、tryLock()这些。
----------------------------------------------------------------------------------------------------------------
挑主要的方法来讲。
先介绍上锁lock()。
public void lock() {
sync.lock();
}
在这个方法中,sync.lock(),是一个策略模式,由子类的实现而确定。如果子类是公平锁FairSync,则调用FairSync的lock()方法;否则,调用非公平锁
NonfairSync的lock()方法。
先看公平锁的lock(),代码如下
final void lock() {
acquire(1);
}
//加锁
public final void acquire(int arg) {
//如果尝试上锁上锁,并且获取队列成功,则当前线程自中断。
if (!tryAcquire(arg) && //这里的tryAcquire,由子类实现,如下面的代码2。从这里可以看出,非公平所,acquire获取锁的时候,会直接尝试获取锁。失败则加入资源队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 自中断
}
//通过自旋的方式获取同步状态。所谓自旋,说白了,就是死循环for(;;)。这个方法返回中断状态
//代码1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { //如果前驱节点是头节点并且获取锁成功,则把头节点设置成当前的节点。并且把前驱节点的next设置为null,方便gc。这里再次调用了tryAcquire
setHead(node);
p.next = null; // help GC //注意这里的写法。因为当前节点已经成为头部节点,当前节点的线程关闭后,当前节点也会被回收。那么当前节点的前驱节点的next,需要设置成null,否则gc不会回收当前节点。
failed = false;
return interrupted; //返回当前的节点的中断状态:false
}
if (shouldParkAfterFailedAcquire(p, node) && //是否应该挂起失败的线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//是否应该挂起失败的线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前驱节点,已经是SIGNAL,也就是-1,那么直接返回true,表示可以挂起。因为,前驱节点释放锁后,会唤醒后续的的节点
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { //如果前驱节点已经被注销,也就是waitStatus > 0(大于0 的只有被注销的状态),则执行下面的循环
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do { //这里不断循环,直到前驱节点的状态<=0。当等于0的时候,表示没有状态。当小于0的时候,有-1 -2 -3三种情况。其中,-3是共享模式才有,表示节点可传播。-2则是表示节点是处于条件Condition队列。-1才表示节点处于等待队列。
node.prev = pred = pred.prev; //其实就是常用的for循环的变种而已
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //采用CAS操作,设置前驱节点的状态为-1,表示释放锁后,会唤醒后驱节点
}
return false;
}
/挂起并设置中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//代码4
//节点取消获取锁
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
//这里跳过前驱节点。这里的实现挺巧妙的
Node pred = node.prev;
while (pred.waitStatus > 0) //如果前驱节点的状态>0,也就是已经被取消了,则循环向前查找前驱节点,直到前驱节点的状态 < = 0,也就是SIGNAL -1状态或者PROPAGATE -2传播状态。传播状态只在共享模式下才有用
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next; //前驱节点的后驱节点
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED; //设置当前节点状态为取消,也就是1
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) { //如果当前节点是末尾节点,当前节点n会被设置成CANCEL,则把等待队列的末尾节点设置成当前节点的前驱节点,也就是第n-1个节点被设置成了末尾节点
compareAndSetNext(pred, predNext, null); //将前驱节点的后驱节点设置为null,因为当前节点已经设置成了CANCELLED了,前驱节点正式成为末尾节点,也就不会再由后驱节点
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head && //这里的说明,在下面说明1处详述
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next; //当前节点的后驱节的成下一个节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next); //前驱节点的后驱节点本来是当前节点,现在将前驱节点的后驱节点,设置成当前节点的下一个节点。
} else { //直接后激活驱节点。park是挂起unpark是激活
unparkSuccessor(node);
}
node.next = node; // help GC //将节点的后驱节点设置成自身,方便gc。这里要注意的是,不同于其他变量,设置成null
}
}
//说明1:如果当前节点不是头节点且线程不是空,有以下几种场景:1、前驱节点的状态已经是唤醒状态SIGNAL -1, 2、如果前驱节点不是SINGAL,会可能=0(没有状态) 或者=-3(共享模式的传播状态),则设置前驱节点的状态为SIGNAL
//代码5
//激活后驱节点(使后驱节点可用)
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0) //如果当前节点的状态<0,即唤醒SINGAL -1状态 ,或者等待条件CONDITION -2状态 ,或者PROPAGATE -3 传播状态(共享模式),则将节点的状态设置成0(没有状态)
compareAndSetWaitStatus(node, ws, 0);
/*要激活的线程通常是在后驱节点上持有(这句话怎么意思?我的理解是,当前节点的后驱节点持有的线程,会被激活。也就是后驱节点的线程,会变成可用状态)。
*如果后驱节点已经被取消或者被设置成null,则从末尾节点开始往前搜索,直接找到一个不是null又不是取消的节点。
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) //这里从末尾节点开始循环,直到当前节点的下一个非CANCEL&非null的节点,可以参考下图
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //使后驱节点持有的线程可用。但是只是使线程可用,不保证线程会被执行。
}
//代码2
protected final boolean tryAcquire(int acquires) {
//先拿到当前线程
final Thread current = Thread.currentThread();
//获取上锁的次数
int c = getState();
if (c == 0) { //如果上锁次数为0,则证明锁空闲
if (!hasQueuedPredecessors() && //如果没有前驱节点Node,则证明当前节点是头节点。使用CAS方法,设置上锁次数。这个的次数,保存在AQS的state里面
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); //设置锁的持有者为当前线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //如果锁由当前线程持有,则上锁次数+acquires。这个acquires总是1
int nextc = c + acquires;
if (nextc < 0) //校验参数合法行
throw new Error("Maximum lock count exceeded");
setState(nextc); //设置加锁次数
return true;
}
return false;
}
公平锁,总是先选择第一个节点加锁。如果锁已经被当前线程持有,当前线程再次获取锁的时候,会成功,加锁次数+1。这里体现的,就是ReenTrantLock的可重入性。
下面介绍释放锁的方法。事实上,公平锁和非公平锁的释放,都是调用了父类Sync的方法
public void unlock() {
sync.release(1); //这里调用的是父类AQS的释放锁的方法
}
//释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head; //因为是公平锁,永远是头节点获取到锁,也就永远从头节点开始释放锁
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //代码5的激活后驱节点线程
return true;
}
return false;
}
尝试释放锁,调用的是父类Sync的方法,如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //加锁次数叠减。这里的releases总是1
if (Thread.currentThread() != getExclusiveOwnerThread()) //如果释放锁的线程不是排他锁的持有线程,则抛出异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null); //如果加锁次数已经是0,则设置锁的持有现场为null
}
setState(c); //设置加锁次数
return free;
}
下面介绍非公平锁
final void lock() {
if (compareAndSetState(0, 1)) //先采用 CAS操作尝试获取锁,成功则把当前线程设置成排他(互斥)锁线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //否则,执行加锁操作。这里的加锁操作acquire(1),和公平锁的代码一模一样。唯一的区别,是加锁时候调用的tryAcquire,各自实现而已。
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); //这里的nonfairTryAcquire ,直接是调用父类Sync的方法。
}
//非公平锁尝试获取锁。由此可见,ReentrantLock的默认锁,是非公平锁。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread(); //获取当前线程
int c = getState(); //确认加锁次数。加锁次数维护在超类AQS的state里。这个state 是由volatile里(注意这个volatile内存言语的作用,是用于共享变量在多线程即时可见。
//也就是一个线程改变了state,另一个线程马上能够看见。这个内存言语,是实现并发的基础之一)
if (c == 0) { //加锁次数为0,证明锁还没有被获取
if (compareAndSetState(0, acquires)) { //CAS操作,加锁。这里的acquires在ReentrantLock里,总是1
setExclusiveOwnerThread(current); //设置当前线程持有排他锁
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //如果加锁次数大于1,且是当前线程持有锁,则加锁次数累加
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); //设置加锁次数
return true;
}
return false;
}
由上面的公平锁和非公平锁的实现可以看到,实现大同小异,都是调用超类AQS的 acquire(int arg) 方法(acquire(int arg)是一个模板方法模式代码)。
不同的是,公平锁总是第一个节点才能获取到锁,这里并不符合计算机的资源最大使用思想。而非公平锁,则是由jvm调度。因此ReentrantLock默认使用的是非公平锁。
公平锁和非公平锁,都有各自的tryAcquire方法
ReentrantLock实现的基础,是AQS的虚拟双向队列CLH,具体表现在代码里,则是Node节点。AQS的队列有两种,一种是资源队列(用于唤醒等操作),一种条件队列(用于条件达到Condition)
Node节点,在AQS里面,是由volatile这个关键字,volatile同时又是内存言语。volatile的修饰,可以使功节点对于不同的线程即时可见。这是关键字,是并发的基础之一。
当一个线程想获取锁,被阻塞的时候,表现在代码里面,就是一个死循环for(;;),直到当前线程所在的节点获取到锁。
这里是类似于监听事件的原理:利用Node节点的修饰符volatile的特性。当另一个节点a(线程)释放了锁的时候,另一个线程b马上可以检测到。如果是节点b是节点a的后驱节点,则节点b可以获取到锁,而b的后驱节点c
则需要等待b释放锁后,再通知后驱节点c。这样c节点的线程,就实际形成了阻塞。
---------------------------------------------------------------
个人水平有限,请各位大佬指点。
ReentrantLock 源代码之我见的更多相关文章
- 信号量,semaphore源代码之我见
信号量,Semaphore,一个限定访问线程数量的工具类,属于并发包java.util.concurrent 里面的类. Semaphore,内部提供了构造方法(包含默认的非公平信号量构造方法,已经可 ...
- 通过ReentrantLock源代码分析AbstractQueuedSynchronizer独占模式
1. 重入锁的概念与作用 reentrant 锁意味着什么呢?简单来说,它有一个与获取锁相关的计数器,如果已占有锁的某个线程再次获取锁,那么lock方法中将计数器就加1后就会立刻返回.当释 ...
- jdk代理和cglib代理源代码之我见
以前值是读过一遍jdk和cglib的代理,时间长了,都忘记入口在哪里了,值是记得其中的一些重点了,今天写一篇博客,当作是笔记.和以前一样,关键代码,我会用红色标记出来. 首先,先列出我的jdk代理对象 ...
- Spring Mvc 源代码之我见 二
上一篇简单介绍了spring mvc 的一些基本内容 和DispatcherServlet 的doc.这一篇将会继续写我对Spring Mvc 源代码的理解.直接上代码: /** * This imp ...
- Spring Mvc 源代码之我见 一
spring mvc 是一个web框架,包括controller.model.view 三大块.其中,核心在于model这个模块,用于处理请求的request. 和之前的博客一样,关键的代码,我会标注 ...
- Linux内核源代码分析方法
Linux内核源代码分析方法 一.内核源代码之我见 Linux内核代码的庞大令不少人"望而生畏",也正由于如此,使得人们对Linux的了解仅处于泛泛的层次.假设想透析Linux ...
- 【Java并发编程实战】—–“J.U.C”:ReentrantLock之二lock方法分析
前一篇博客简介了ReentrantLock的定义和与synchronized的差别,以下尾随LZ的笔记来扒扒ReentrantLock的lock方法.我们知道ReentrantLock有公平锁.非公平 ...
- Java并发包中Lock的实现原理
1. Lock 的简介及使用 Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制.本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\l ...
- Android控件之GridView
GridView是一项显示二维的viewgroup,可滚动的网格.一般用来显示多张图片. 以下模拟九宫图的实现,当鼠标点击图片时会进行相应的跳转链接. 目录结构 main.xml布局文件,存放Grid ...
随机推荐
- 【C#单元测试】 开篇
官方:https://docs.microsoft.com/zh-cn/visualstudio/test/install-third-party-unit-test-frameworks?view= ...
- Oracle之单行函数(字符串函数/数字函数/转换函数/日期函数/通用函数)
虚拟表DUAL介绍: dual是一张虚拟表,只有一行一列,用来构成select的语法规则. Oracle的查询中,必须使用"select 列- from 表"的完整语法,当查询单行 ...
- 谷歌集群数据 clusterdata-2011-2 Cluster workload traces
谷歌集群数据 clusterdata-2011-2 https://github.com/google/cluster-data/blob/master/ClusterData2011_2.md 链接 ...
- JAVA_Scanner 键盘输入
键盘输入语句 介绍:在编程中,需要接收用户输入的数据,就可以使用键盘输入语句来获取.Input.java , 需要一个 扫描器(对象), 就是 Scanner 步骤: 导入该类的所在包, java.u ...
- LeetCode-036-有效的数独
有效的数独 题目描述:请你判断一个 9x9 的数独是否有效.只需要 根据以下规则 ,验证已经填入的数字是否有效即可. 数字 1-9 在每一行只能出现一次. 数字 1-9 在每一列只能出现一次. 数字 ...
- java中的List接口(ArrayList、Vector、LinkedList)
一.List接口有三个常用的集合(ArrayList.Vector.LinkedList) ArrayList注意事项 ArrayList底层是用数组来实现数据存储的 底层是 transient Ob ...
- laravel8安装步骤
网址: https://learnku.com/docs/laravel/8.x/installation/9354 安装: # 安装laravel composer create-project - ...
- 在线O(1)求逆元
怎么还有厉害的在线O(1)求逆元,不过常数确实有点儿太大了 本文大部分搬运于这里 相信大家都做过 POJ2478 这道题吧,这道题的 Farey 序列 \(F_n\) 包含了分子分母不大于 \(n\) ...
- 2022年官网下安装NodeJS最全版与官网查阅方法
目录 安装部署NodeJS 1.百度搜索NodeJS. 2.选择下载,windows版,选择64位下载 3.找到本地安装包位置,双击打开 4.选择同意,点击下一步 5.选择安装目录,点击下一步 6.选 ...
- info sharp Are you trying to install as a root or sudo user? Try again with the --unsafe-perm flag
执行 npm install 编译出错,提示 ERR! sharp EACCES: permission denied, mkdir '/root/.npm' info sharp Are you t ...