【Java并发编程】15、ReentrantLock实现原理深入探究
原文已经写得非常详细了,直接把大神的文章转发过来了 https://www.cnblogs.com/xrq730/p/4979021.html
前言
这篇文章被归到Java基础分类中,其实真的一点都不基础。网上写ReentrantLock的使用、ReentrantLock和synchronized的区别的文章很多,研究ReentrantLock并且能讲清楚ReentrantLock的原理的文章很少,本文就来研究一下ReentrantLock的实现原理。研究ReentrantLock的实现原理需要比较好的Java基础以及阅读代码的能力,有些朋友看不懂没关系,可以以后看,相信你一定会有所收获。
最后说一句,ReentrantLock是基于AQS实现的,这在下面会讲到,AQS的基础又是CAS,如果不是很熟悉CAS的朋友,可以看一下这篇文章Unsafe与CAS。
AbstractQueuedSynchronizer
ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。先用两张表格介绍一下AQS。第一个讲的是Node,由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点,Node里面有:
| 属 性 | 定 义 |
| Node SHARED = new Node() | 表示Node处于共享模式 |
| Node EXCLUSIVE = null | 表示Node处于独占模式 |
| int CANCELLED = 1 | 因为超时或者中断,Node被设置为取消状态,被取消的Node不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态,处于这种状态的Node会被踢出队列,被GC回收 |
| int SIGNAL = -1 | 表示这个Node的继任Node被阻塞了,到时需要通知它 |
| int CONDITION = -2 | 表示这个Node在条件队列中,因为等待某个条件而被阻塞 |
| int PROPAGATE = -3 | 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播 |
| int waitStatus | 0,新Node会处于这种状态 |
| Node prev | 队列中某个Node的前驱Node |
| Node next | 队列中某个Node的后继Node |
| Thread thread | 这个Node持有的线程,表示等待锁的线程 |
| Node nextWaiter | 表示下一个等待condition的Node |
看完了Node,下面再看一下AQS中有哪些变量和方法:
| 属性/方法 | 含 义 |
| Thread exclusiveOwnerThread | 这个是AQS父类AbstractOwnableSynchronizer的属性,表示独占模式同步器的当前拥有者 |
| Node | 上面已经介绍过了,FIFO队列的基本单位 |
| Node head | FIFO队列中的头Node |
| Node tail | FIFO队列中的尾Node |
| int state | 同步状态,0表示未锁 |
| int getState() | 获取同步状态 |
| setState(int newState) | 设置同步状态 |
| boolean compareAndSetState(int expect, int update) | 利用CAS进行State的设置 |
| long spinForTimeoutThreshold = 1000L | 线程自旋等待的时间 |
| Node enq(final Node node) | 插入一个Node到FIFO队列中 |
| Node addWaiter(Node mode) | 为当前线程和指定模式创建并扩充一个等待队列 |
| void setHead(Node node) | 设置队列的头Node |
| void unparkSuccessor(Node node) | 如果存在的话,唤起Node持有的线程 |
| void doReleaseShared() | 共享模式下做释放锁的动作 |
| void cancelAcquire(Node node) | 取消正在进行的Node获取锁的尝试 |
| boolean shouldParkAfterFailedAcquire(Node pred, Node node) | 在尝试获取锁失败后是否应该禁用当前线程并等待 |
| void selfInterrupt() | 中断当前线程本身 |
| boolean parkAndCheckInterrupt() | 禁用当前线程进入等待状态并中断线程本身 |
| boolean acquireQueued(final Node node, int arg) | 队列中的线程获取锁 |
| tryAcquire(int arg) | 尝试获得锁(由AQS的子类实现它) |
| tryRelease(int arg) | 尝试释放锁(由AQS的子类实现它) |
| isHeldExclusively() | 是否独自持有锁 |
| acquire(int arg) | 获取锁 |
| release(int arg) | 释放锁 |
| compareAndSetHead(Node update) | 利用CAS设置头Node |
| compareAndSetTail(Node expect, Node update) | 利用CAS设置尾Node |
| compareAndSetWaitStatus(Node node, int expect, int update) | 利用CAS设置某个Node中的等待状态 |
上面列出了AQS中最主要的一些方法和属性。整个AQS是典型的模板模式的应用,设计得十分精巧,对于FIFO队列的各种操作在AQS中已经实现了,AQS的子类一般只需要重写tryAcquire(int arg)和tryRelease(int arg)两个方法即可。
ReentrantLock的实现
ReentrantLock中有一个抽象类Sync:

private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}

ReentrantLock根据传入构造方法的布尔型参数实例化出Sync的实现类FairSync和NonfairSync,分别表示公平的Sync和非公平的Sync。由于ReentrantLock我们用的比较多的是非公平锁,所以看下非公平锁是如何实现的。假设线程1调用了ReentrantLock的lock()方法,那么线程1将会独占锁,整个调用链十分简单:

第一个获取锁的线程就做了两件事情:
1、设置AbstractQueuedSynchronizer的state为1
2、设置AbstractOwnableSynchronizer的thread为当前线程
这两步做完之后就表示线程1独占了锁。然后线程2也要尝试获取同一个锁,在线程1没有释放锁的情况下必然是行不通的,所以线程2就要阻塞。那么,线程2如何被阻塞?看下线程2的方法调用链,这就比较复杂了:

调用链看到确实非常长,没关系,结合代码分析一下,其实ReentrantLock没有那么复杂,我们一点点来扒代码:
1 final void lock() {
2 if (compareAndSetState(0, 1))
3 setExclusiveOwnerThread(Thread.currentThread());
4 else
5 acquire(1);
6 }
首先线程2尝试利用CAS去判断state是不是0,是0就设置为1,当然这一步操作肯定是失败的,因为线程1已经将state设置成了1,所以第2行必定是false,因此线程2走第5行的acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
从字面上就很好理解这个if的意思,先走第一个判断条件尝试获取一次锁,如果获取的结果为false即失败,走第二个判断条件添加FIFO等待队列。所以先看一下tryAcquire方法做了什么,这个方法最终调用到的是Sync的nonfairTryAcquire方法:

1 final boolean nonfairTryAcquire(int acquires) {
2 final Thread current = Thread.currentThread();
3 int c = getState();
4 if (c == 0) {
5 if (compareAndSetState(0, acquires)) {
6 setExclusiveOwnerThread(current);
7 return true;
8 }
9 }
10 else if (current == getExclusiveOwnerThread()) {
11 int nextc = c + acquires;
12 if (nextc < 0) // overflow
13 throw new Error("Maximum lock count exceeded");
14 setState(nextc);
15 return true;
16 }
17 return false;
18 }

由于state是volatile的,所以state对线程2具有可见性,线程2拿到最新的state,再次判断一下能否持有锁(可能线程1同步代码执行得比较快,这会儿已经释放了锁),不可以就返回false。
注意一下第10~第16行,这段代码的作用是让某个线程可以多次调用同一个ReentrantLock,每调用一次给state+1,由于某个线程已经持有了锁,所以这里不会有竞争,因此不需要利用CAS设置state(相当于一个偏向锁)。从这段代码可以看到,nextc每次加1,当nextc<0的时候抛出error,那么同一个锁最多能重入Integer.MAX_VALUE次,也就是2147483647。
然后就走到if的第二个判断里面了,先走AQS的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,模式为独占模式(因为传入的mode是一个NULL),再判断一下队列上有没有节点,没有就创建一个队列,因此走enq方法:

1 private Node enq(final Node node) {
2 for (;;) {
3 Node t = tail;
4 if (t == null) { // Must initialize
5 Node h = new Node(); // Dummy header
6 h.next = node;
7 node.prev = h;
8 if (compareAndSetHead(h)) {
9 tail = node;
10 return h;
11 }
12 }
13 else {
14 node.prev = t;
15 if (compareAndSetTail(t, node)) {
16 t.next = node;
17 return t;
18 }
19 }
20 }
21 }

这个方法其实画一张图应该比较好理解,形成一个队列之后应该是这样的:

每一步都用图表示出来了,由于线程2所在的Node是第一个要等待的Node,因此FIFO队列上肯定没有内容,tail为null,走的就是第4行~第10行的代码逻辑。这里用了CAS设置头Node,当然有可能线程2设置头Node的时候CPU切换了,线程3已经把头Node设置好了形成了上图所示的一个队列,这时线程2再循环一次获取tail,由于tail是volatile的,所以对线程2可见,线程2看见tail不为null,就走到了13行的else里面去往尾Node后面添加自身。整个过程下来,形成了一个双向队列。最后走AQS的acquireQueued(node, 1):

1 final boolean acquireQueued(final Node node, int arg) {
2 try {
3 boolean interrupted = false;
4 for (;;) {
5 final Node p = node.predecessor();
6 if (p == head && tryAcquire(arg)) {
7 setHead(node);
8 p.next = null; // help GC
9 return interrupted;
10 }
11 if (shouldParkAfterFailedAcquire(p, node) &&
12 parkAndCheckInterrupt())
13 interrupted = true;
14 }
15 } catch (RuntimeException ex) {
16 cancelAcquire(node);
17 throw ex;
18 }
19 }

此时再做判断,由于线程2是双向队列的真正的第一个Node(前面还有一个h),所以第5行~第10行再次判断一下线程2能不能获取锁(可能这段时间内线程1已经执行完了把锁释放了,state从1变为了0),如果还是不行,先调用AQS的shouldParkAfterFailedAcquire(p, node)方法:

1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
2 int s = pred.waitStatus;
3 if (s < 0)
4 /*
5 * This node has already set status asking a release
6 * to signal it, so it can safely park
7 */
8 return true;
9 if (s > 0) {
10 /*
11 * Predecessor was cancelled. Skip over predecessors and
12 * indicate retry.
13 */
14 do {
15 node.prev = pred = pred.prev;
16 } while (pred.waitStatus > 0);
17 pred.next = node;
18 }
19 else
20 /*
21 * Indicate that we need a signal, but don't park yet. Caller
22 * will need to retry to make sure it cannot acquire before
23 * parking.
24 */
25 compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
26 return false;
27 }

吐槽一下先,这段代码的代码格式真糟糕(看来JDK的开发大牛们也有写得不好的地方),这个waitStatus是h的waitStatus,很明显是0,所以此时把h的waitStatus设置为Noed.SIGNAL即-1并返回false。既然返回了false,上面的acquireQueued的11行if自然不成立,再走一次for循环,还是先尝试获取锁,不成功,继续走shouldParkAfterFailedAcquire,此时waitStatus为-1,小于0,走第三行的判断,返回true。然后走acquireQueued的11行if的第二个判断条件parkAndCheckInterrupt:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
最后一步,调用LockSupport的park方法阻塞住了当前的线程。至此,使用ReentrantLock让线程1独占锁、线程2进入FIFO队列并阻塞的完整流程已经整理出来了。
lock()的操作明了之后,就要探究一下unlock()的时候代码又做了什么了,接着看下一部分。
unlock()的时候做了什么
就不画流程图了,直接看一下代码流程,比较简单,调用ReentrantLock的unlock方法:
public void unlock() {
sync.release(1);
}
走AQS的release:

1 public final boolean release(int arg) {
2 if (tryRelease(arg)) {
3 Node h = head;
4 if (h != null && h.waitStatus != 0)
5 unparkSuccessor(h);
6 return true;
7 }
8 return false;
9 }

先调用Sync的tryRelease尝试释放锁:

1 protected final boolean tryRelease(int releases) {
2 int c = getState() - releases;
3 if (Thread.currentThread() != getExclusiveOwnerThread())
4 throw new IllegalMonitorStateException();
5 boolean free = false;
6 if (c == 0) {
7 free = true;
8 setExclusiveOwnerThread(null);
9 }
10 setState(c);
11 return free;
12 }

首先,只有当c==0的时候才会让free=true,这和上面一个线程多次调用lock方法累加state是对应的,调用了多少次的lock()方法自然必须调用同样次数的unlock()方法才行,这样才把一个锁给全部解开。
当一条线程对同一个ReentrantLock全部解锁之后,AQS的state自然就是0了,AbstractOwnableSynchronizer的exclusiveOwnerThread将被设置为null,这样就表示没有线程占有锁,方法返回true。代码继续往下走,上面的release方法的第四行,h不为null成立,h的waitStatus为-1,不等于0也成立,所以走第5行的unparkSuccessor方法:

1 private void unparkSuccessor(Node node) {
2 /*
3 * Try to clear status in anticipation of signalling. It is
4 * OK if this fails or if status is changed by waiting thread.
5 */
6 compareAndSetWaitStatus(node, Node.SIGNAL, 0);
7
8 /*
9 * Thread to unpark is held in successor, which is normally
10 * just the next node. But if cancelled or apparently null,
11 * traverse backwards from tail to find the actual
12 * non-cancelled successor.
13 */
14 Node s = node.next;
15 if (s == null || s.waitStatus > 0) {
16 s = null;
17 for (Node t = tail; t != null && t != node; t = t.prev)
18 if (t.waitStatus <= 0)
19 s = t;
20 }
21 if (s != null)
22 LockSupport.unpark(s.thread);
23 }

s即h的下一个Node,这个Node里面的线程就是线程2,由于这个Node不等于null,所以走21行,线程2被unPark了,得以运行。有一个很重要的问题是:锁被解了怎样保证整个FIFO队列减少一个Node呢?这是一个很巧妙的设计,又回到了AQS的acquireQueued方法了:

1 final boolean acquireQueued(final Node node, int arg) {
2 try {
3 boolean interrupted = false;
4 for (;;) {
5 final Node p = node.predecessor();
6 if (p == head && tryAcquire(arg)) {
7 setHead(node);
8 p.next = null; // help GC
9 return interrupted;
10 }
11 if (shouldParkAfterFailedAcquire(p, node) &&
12 parkAndCheckInterrupt())
13 interrupted = true;
14 }
15 } catch (RuntimeException ex) {
16 cancelAcquire(node);
17 throw ex;
18 }
19 }

被阻塞的线程2是被阻塞在第12行,注意这里并没有return语句,也就是说,阻塞完成线程2依然会进行for循环。然后,阻塞完成了,线程2所在的Node的前驱Node是p,线程2尝试tryAcquire,成功,然后线程2就成为了head节点了,把p的next设置为null,这样原头Node里面的所有对象都不指向任何块内存空间,h属于栈内存的内容,方法结束被自动回收,这样随着方法的调用完毕,原头Node也没有任何的引用指向它了,这样它就被GC自动回收了。此时,遇到一个return语句,acquireQueued方法结束,后面的Node也是一样的原理。
这里有一个细节,看一下setHead方法:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
setHead方法里面的前驱Node是Null,也没有线程,那么为什么不用一个在等待的线程作为Head Node呢?
因为一个线程随时有可能因为中断而取消,而取消的话,Node自然就要被GC了,那GC前必然要把头Node的后继Node变为一个新的头而且要应对多种情况,这样就很麻烦。用一个没有thread的Node作为头,相当于起了一个引导作用,因为head没有线程,自然也不会被取消。
再看一下上面unparkSuccessor的14行~20行,就是为了防止head的下一个node被取消的情况,这样,就从尾到头遍历,找出离head最近的一个node,对这个node进行unPark操作。
ReentrantLock其他方法的实现
如果能理解ReentrantLock的实现方式,那么你会发现ReentrantLock中其余一些方法的实现还是很简单的,从JDK API关于ReentrantLock方法的介绍这部分,举几个例子:
1、int getHoldCount()
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
获取ReentrantLock的lock()方法被调用了几次,就是state的当前值
2、Thread getOwner()
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
获取当前占有锁的线程,就是AbstractOwnableSynchronizer中exclusiveOwnerThread的值
3、Collection<Thread> getQueuedThreads()

public final Collection<Thread> getQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
return list;
}

从尾到头遍历一下,添加进ArrayList中
4、int getQueuedLength()

public final int getQueueLength() {
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}

从尾到头遍历一下,累加n。当然这个方法和上面那个方法可能是不准确的,因为遍历的时候可能别的线程又往队列尾部添加了Node。
其余方法也都差不多,可以自己去看一下。
遗留问题
ReentrantLock的流程基本已经理清楚了,现在还有一个遗留问题:我们知道ReentrantLock是可以指定公平锁或是非公平锁,那么到底是怎么样的代码差别导致公平锁和非公平锁的产生的呢?
说实话,这个问题,我自己到现在还没有完全想通。之后会持续跟进这个问题,一旦想明白了,会第一时间更新此文或者是新发一篇文章来专门讲述公平ReentrantLock和非公平ReentrantLock在代码上的差别。
【Java并发编程】15、ReentrantLock实现原理深入探究的更多相关文章
- java并发编程——通过ReentrantLock,Condition实现银行存取款
java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器.该框架允许更灵活地使用锁和条件,但以更难用的语法为代价. Lock 接口 ...
- Java并发编程笔记之ConcurrentHashMap原理探究
在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap. HashTable是一个线程安全的类 ...
- Java并发编程基础-ReentrantLock的机制
同步锁: 我们知道,锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源,在Lock接口出现之前,Java应用程序只能依靠synchronized关键字来实现同步锁 ...
- java并发编程基础-ReentrantLock及LinkedBlockingQueue源码分析
ReentrantLock是一个较为常用的锁对象.在上次分析的uil开源项目中也多次被用到,下面谈谈其概念和基本使用. 概念 一个可重入的互斥锁定 Lock,它具有与使用 synchronized 相 ...
- 【java并发编程】ReentrantLock 可重入读写锁
目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 欢迎关注我的博客,更多精品知识合集 一.ReentrantLock可重入锁 可 ...
- Java并发编程笔记之LinkedBlockingQueue源码探究
JDK 中基于链表的阻塞队列 LinkedBlockingQueue 原理剖析,LinkedBlockingQueue 内部是如何使用两个独占锁 ReentrantLock 以及对应的条件变量保证多线 ...
- Java并发编程笔记之ConcurrentLinkedQueue源码探究
JDK 中基于链表的非阻塞无界队列 ConcurrentLinkedQueue 原理剖析,ConcurrentLinkedQueue 内部是如何使用 CAS 非阻塞算法来保证多线程下入队出队操作的线程 ...
- 那些年读过的书《Java并发编程实战》十、再探究Java内存模型
1.什么是内存模型,为什么需要它? (1)内存模型的发展背景 近几年计算性能通过重排序实现了很大的提升,而且处理器也越来越朝着多核处理器发展以实现硬件的并行性.随着处理器的不断强大,编译器也在不断的改 ...
- Java并发编程笔记之ThreadLocal内存泄漏探究
使用 ThreadLocal 不当可能会导致内存泄露,是什么原因导致的内存泄漏呢? 我们首先看一个例子,代码如下: /** * Created by cong on 2018/7/14. */ pub ...
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
随机推荐
- Django用openLDAP做认证
前言 之前有需求要做一个django+ldap用户管理的简单接口,研究了好几个模块,最后终于能实现django用ldap做用户认证了.也是自己的水平有限吧,做了好长时间,现在就和大家分享一下这个过程吧 ...
- CMD指令及其意义
1. appwiz.cpl:程序和功能 2. calc:启动计算器 5. chkdsk.exe:Chkdsk磁盘检查(管理员身份运行命令提示符) 6. cleanmgr: 打开磁盘清理工具 9. cm ...
- css3的动画效果
全新的css3加入的动画效果: [ animation-name ]:检索或设置对象所应用的动画名称 [ animation-duration ]: 检索或设置对象动画的持续时间 [ animatio ...
- 背水一战 Windows 10 (72) - 控件(控件基类): UIElement - UIElement 的位置, UIElement 的布局, UIElement 的其他特性
[源码下载] 背水一战 Windows 10 (72) - 控件(控件基类): UIElement - UIElement 的位置, UIElement 的布局, UIElement 的其他特性 作者 ...
- Java匹马行天下——开篇
个人感言: 匹马行天下是我高中时候看过一部叫<九鼎记>的小说中的其中一个大章节标题,在整个这一章中,讲的是是主人公滕青山历经艰险,又心如磐石,一心修行,最后巅峰归来的故事.现在回想,依旧心 ...
- javaScript 节流与防抖
首先 我们要知道 节流与防抖可以干什么. 优化网络请求性能——节流 优化页面请求性能——防抖 举两个简单的小例子: 节流: 例如 有些购物页面,会有一些让你抢购的活动,到点的时候,需要你快速的点某个按 ...
- ArrayList源码解读笔记
简介: ArrayList是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储任意类型的数据,ArrayList是线程不安全的,非常适合用于对元素进行查找,效率非常高. 线 ...
- 【2019北京集训六】路径(path) 二分+DP
此题niubi! 题目大意:给你一颗n个点的点带权无根树,现在请您进行以下两步操作: 1,选择一个$[0,T]$之间的整数$C$,并令所有的点权$wi$变为$(wi+C)%MOD$ 2,选择若干条点不 ...
- Xamarin.Android 使用 Encoding.GetEncoding("GB2312") 报错解决方案
问题描述:使用byte转换成汉字编码格式,debug模式下可以正常运行,但是release模式下就会出现死机问题. 排除过程:最开始不知道是编码格式转换错误,于是把相关代码块注释掉,然后进行relea ...
- .Net开源myrtille远程连接服务(支持SSH)
今天在博客园首页,无意中看到一篇文章(https://github.com/Microsoft/dotnet/blob/master/dotnet-developer-projects.md),于是对 ...