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 源代码之我见的更多相关文章

  1. 信号量,semaphore源代码之我见

    信号量,Semaphore,一个限定访问线程数量的工具类,属于并发包java.util.concurrent 里面的类. Semaphore,内部提供了构造方法(包含默认的非公平信号量构造方法,已经可 ...

  2. 通过ReentrantLock源代码分析AbstractQueuedSynchronizer独占模式

    1. 重入锁的概念与作用       reentrant 锁意味着什么呢?简单来说,它有一个与获取锁相关的计数器,如果已占有锁的某个线程再次获取锁,那么lock方法中将计数器就加1后就会立刻返回.当释 ...

  3. jdk代理和cglib代理源代码之我见

    以前值是读过一遍jdk和cglib的代理,时间长了,都忘记入口在哪里了,值是记得其中的一些重点了,今天写一篇博客,当作是笔记.和以前一样,关键代码,我会用红色标记出来. 首先,先列出我的jdk代理对象 ...

  4. Spring Mvc 源代码之我见 二

    上一篇简单介绍了spring mvc 的一些基本内容 和DispatcherServlet 的doc.这一篇将会继续写我对Spring Mvc 源代码的理解.直接上代码: /** * This imp ...

  5. Spring Mvc 源代码之我见 一

    spring mvc 是一个web框架,包括controller.model.view 三大块.其中,核心在于model这个模块,用于处理请求的request. 和之前的博客一样,关键的代码,我会标注 ...

  6. Linux内核源代码分析方法

    Linux内核源代码分析方法   一.内核源代码之我见 Linux内核代码的庞大令不少人"望而生畏",也正由于如此,使得人们对Linux的了解仅处于泛泛的层次.假设想透析Linux ...

  7. 【Java并发编程实战】—–“J.U.C”:ReentrantLock之二lock方法分析

    前一篇博客简介了ReentrantLock的定义和与synchronized的差别,以下尾随LZ的笔记来扒扒ReentrantLock的lock方法.我们知道ReentrantLock有公平锁.非公平 ...

  8. Java并发包中Lock的实现原理

    1. Lock 的简介及使用 Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制.本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\l ...

  9. Android控件之GridView

    GridView是一项显示二维的viewgroup,可滚动的网格.一般用来显示多张图片. 以下模拟九宫图的实现,当鼠标点击图片时会进行相应的跳转链接. 目录结构 main.xml布局文件,存放Grid ...

随机推荐

  1. ☕Java 面向对象进阶内容

    目录 == 和 equals 方法 封装 多态 抽象类和抽象方法 抽象方法 抽象类 抽象类的使用要点 接口 接口使用 内部类 String 字符串常量拼接时的优化 String Pool String ...

  2. Zabbix——可视化的资源监控解决方案

    Zabbix监控的内容 1.硬件监控 温度 磁盘.主板等故障 待机时间 2.系统监控 CPU 内存 负载 磁盘 内核参数 网卡流量 TCP连接数 进程数 IO 端口采集 3.程序 应用:Nginx.m ...

  3. js 数组/对象/日期的浅克隆

    //封装 function clone (obj) { // Handle the 3 simple types, and null or undefined if (null == obj || & ...

  4. tensorflow_keras_预训练模型_Applications接口的使用

    在很多复杂的计算机视觉问题上,我们需要使用层次相对较深的卷积神经网络才能得到好结果,但是自己从头去构建卷积神经网络是一个耗时耗力的事情,而且还不一定能训练好.大家通常用到最多的技巧是,使用" ...

  5. 安装CentOS 7

    1.安装引导 首先要设置计算机的启动顺序为CD-ROM或USB 启动,保存设置后将CD放入光驱或插入USB,重新启动计算机. 计算机重启后显示有几个选项的引导菜单.如果在 60 秒内没有按任何键,则运 ...

  6. Spring入门一:IOC、DI、AOP基本思想

    Spring框架是一个集众多涉及模式于一身的开源的.轻量级的项目管理框架,致力于javaee轻量级解决方案.相对于原来学过的框架而言,spring框架和之前学习的struts2.mybatis框架有了 ...

  7. Docker安装与基本命令使用

    1. 卸载旧版本 Docker在CentOS上的安装 官方文档:https://docs.docker.com/engine/install/centos/ sudo yum remove docke ...

  8. SQL从零到迅速精通【查询利器】

    1.[列选取]从fruits表中获取f_name和f_price两列,T-SQL语句如下. SELECT f_name,f_price FROM fruits; 2.[去重]查询fruits表中s_i ...

  9. tp5怎么防sql注入 xss跨站脚本攻击

    在 application/config.php 中有个配置选项 框架默认没有设置任何过滤规则,你可以是配置文件中设置全局的过滤规则 则会调用这些函数 自动过滤 // 默认全局过滤方法 用逗号分隔多个 ...

  10. 什么是phpize及其用法

    应用场景在使用php的过程中,我们常常需要去添加一些PHP扩展库.但是重新对php进行编译是比较蛮烦的,所以这时候我们可以使用phpize对php进行添加扩展.并且phpize编译的扩展库可以随时启用 ...