在上面一篇分析ThreadExecutedPool的文章中我们看到线程池实现源码中大量使用了ReentrantLock锁,那么ReentrantLock锁的优势是什么?它又是怎么实现的呢?

ReentrantLock又名可重入锁,为什么称之为可重入锁呢?简单来说因为它允许一个线程多次取获得该锁,不过多次获取该锁之后,也需要执行同样次数的释放锁操作,否则该锁将被当前线程一直持有,导致其它线程无法获取。需要注意的是,释放锁的操作需要我们用代码来控制,它并不会自动取释放锁。在ReentrantLock中实现了两种锁fairSync和NonfairSync,即公平锁和非公平锁,今天我们就来聊聊ReentrantLock中nonfairSync锁的实现。

废话不多说,下面开始分析代码!

1、1 Lock()

首先看一下lock()方法,这个方法非常重要,它也是我们获取锁的入口:

    final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

ReentrantLock锁的初始状态为0,compareAndSetState方法将尝试获取锁并将当前锁的状态设置为1。如果成功获取了锁会调用setExclusiveOwnerThread()方法设置当前线程拥有该锁的独占访问权。

如果调用compareAndSetState()获取锁失败,则返回false并执行acquire(1)。

    public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

我们看到acquire(1)的代码中有一条if语句,当tryAcquire(1)返回false以及acquireQueued(addWaiter(Node.EXCLUSIVE), arg)返回true时,才会去执行selfInterrupt();方法。下面我们来看看tryAcquire(1)和acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这两个家伙干了什么。

先看一下tryAcquire()方法。

    protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

tryAcquire方法会去调用nonfairTryAcquire()方法。

    final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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;
}
return false;
}

1、首先调用getState()方法获取当前锁状态,如果锁状态为0。表示当前锁没有被其他线程占用,这里会再次尝试去获取锁。如果成功的拿到了锁,将设置锁的拥有者为当前线程,同时返回true。如果此时返回true的话,表示当前线程成功获取到了锁,lock()方法调用成功。

2、如果当前锁状态不为0,判断当前线程是否为锁的拥有者,如果是的话,尝试将当前锁的状态值加acquires。如果当前neextc的值小于0,抛出异常。若不小于0,将当前锁的值设置为nextc。为什么说ReentrantLock为可重入锁,就体现在这里了,如果当前线程为锁的拥有者,该线程再次调用lock方法时,当前锁的状态值会加1。当然我们释放该锁的时候,也要调用相应的unlock()方法,以使得锁的state值为0,可被其他线程请求。

3、如果当前锁的值不为0且拥有锁的线程也不为当前线程则返回false。也就是tryAcquire()再次获取锁并没有成功。

值得注意的是,既然再第一次调用compareAndSetState()的时候,已经获取失败了为什么还要再调用tryAcquire()方法再获取一次呢?我们可以理解为这是一种保险机制,如果此时无法获取锁,我们将会将当前线程加入到阻塞队列中挂起等待后面被唤醒重新争夺锁。

回顾一下上面的if判断条件

if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

如果tryAcquire()的返回值为false,那么接下来会执行acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法。这个方法看起来比较复杂,它在acquireQueued()方法又调用了addWaiter()方法,我们先来看看addWaiter()方法:

    private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

首先我们创建了一个包含了当前线程的Node节点,并将tail(tail节点即是尾节点)赋值给pred节点。如果我们第一次进来,那么tail节点肯定为空,将会去执行enq(node)方法。如果tail不为空,那么接下来的三句代码干了什么呢?

先回忆一下,如果我们希望在一个双向链表的尾部新增一个节点,应该如何操作,大致应该有如下三步:

  • node.prev = pred; node节点的前驱指向尾节点
  • pred.next = node; 将尾节点的后继设置为当前节点
  • tail = node; 将node节点设置为尾节点

    我们再看一下详细代码:
  1. node.prev=pred; 当前pred节点代表的是尾节点,也就是说设置node节点的前驱为当前尾节点。
  2. if (compareAndSetTail(pred, node)),我们看下compareAndSetTail(pred, node)方法。
    private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
compareAndSetTail的原理其实就是CAS算法,将期望值和内存地址为tailOffset上的值进行比较,如果两者相同,则更新tailOffset上的值为最新值update。其实也就是如果tailOffset上的值和pred(老的尾节点)的值相同,则将尾节点更新为新的node节点。
  1. 将原尾节点的后继设置为当前节点。

    其实上面三步实现的功能和在双向链表尾部新增一个节点的功能大致相同,只是顺序略有调整。

接下来看一下enq(node)方法

    private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

enq代码中有一个死循环for(;

ReentrantLock源码分析与理解的更多相关文章

  1. JUC AQS ReentrantLock源码分析

    警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳. Java的内置锁一直都是备受争议的,在JDK1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6 ...

  2. ReentrantLock 源码分析以及 AQS (一)

    前言 JDK1.5 之后发布了JUC(java.util.concurrent),用于解决多线程并发问题.AQS 是一个特别重要的同步框架,很多同步类都借助于 AQS 实现了对线程同步状态的管理. A ...

  3. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  4. java多线程---ReentrantLock源码分析

    ReentrantLock源码分析 基础知识复习 synchronized和lock的区别 synchronized是非公平锁,无法保证线程按照申请锁的顺序获得锁,而Lock锁提供了可选参数,可以配置 ...

  5. ReentrantLock源码分析--jdk1.8

    JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...

  6. JUC之ReentrantLock源码分析

    ReentrantLock:实现了Lock接口,是一个可重入锁,并且支持线程公平竞争和非公平竞争两种模式,默认情况下是非公平模式.ReentrantLock算是synchronized的补充和替代方案 ...

  7. Java并发编程之ReentrantLock源码分析

    ReentrantLock介绍 从JDK1.5之前,我们都是使用synchronized关键字来对代码块加锁,在JDK1.5引入了ReentrantLock锁.synchronized关键字性能比Re ...

  8. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  9. ReentrantLock 源码分析从入门到入土

    回答一个问题 在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕 ...

随机推荐

  1. Windows编程坐标系统概念

    Windows编程中关于设置映象模式的四个函数 SetWindowOrgSetViewPortOrgSetMapModeSetWindowExtExSetViewPortExtEx 一.Windows ...

  2. Extjs grid 组件

    表格面板类Ext.grid.Panel 重要的配置参数 columns : Array 列模式(Ext.grid.column.Columnxtype: gridcolumn) 重要的配置参数 tex ...

  3. oracle系列笔记(2)---多表查询

    多表查询     这篇文章主要讲四点: (1)oracle多表查询    (2)SQL99标准的连接查询   (3)子查询     (4)分级查询 oracle多表查询有两种方式,一种是oracle所 ...

  4. web及H5 的链接测试

    1:先下载一个Xenu工具 2:安装完成之后,进入页面(将弹出框关闭) 3:进行设置(一般不用修改设置) 4:修改完成之后点击工具栏中的file按钮,并输入想要测试的URL地址 5:点击OK测试完成之 ...

  5. 华为HG8245 电信 光猫破解获取超级密码

    这款光猫是 猫+无线路由器一体的 默认没有打开路由功能.  光猫背后的用户名和密码是有限制的没人什么用处,如果要打开路由功能就得要有 超级用户名和密码 不然就算收的到无线连的起也上不了网 .这时就需要 ...

  6. 每天一个Linux命令(06)--rmdir命令

    终于忙完了公司的事,可以安静的充充电了. 今天学习一下Linux中命令:rmdir 命令,rmdir是常用的命令,该命令的功能是删除空目录,一个目录被删除之前必须是空的.(注意,rm -r dir 命 ...

  7. redhat linux enterprise 5 输入ifconfig无效的解决方法

    redhat linux enterprise 5 输入ifconfig无效的解决方法   在安装完成linux后,进入终端,输入命令行ifconfig,会提示bash: ifconfig: comm ...

  8. 谈JavaScript的继承

    最近在忙前端的工作,因为之前做.net和php的开发比较多,前端开发喜欢把库拿来就用,几次事实证明,不懂原理,连改代码也改不好,所以还是下定决心研究下JavaScript的几个技术难点. 0x1.Ja ...

  9. gulp自动化压缩合并、加版本号解决方案

    虽然网上有很多的 gulp 构建文章,但是很多都已经随着 gulp 插件的更新无法运行了.因此,我写了这个比较简单的构建方案. 如果还不熟悉 gulp 的插件,可以阅读上一篇文章:精通gulp常用插件 ...

  10. 实现自动构建编译javaweb项目并发布到N台服务器

    前言 当你使用nginx实现了负载均衡,当你有了超过3台以上的应用服务器时,一个特别头疼的问题就来了,发布项目好麻烦. 你每次都要在本地编译打包一遍,然后手动复制到每一台服务器上面去,如果只有一两台服 ...