分析ReentrantLock的实现原理
http://www.jianshu.com/p/fe027772e156
什么是AQS
AQS即是AbstractQueuedSynchronizer,一个用来构建锁和同步工具的框架,包括常用的ReentrantLock、CountDownLatch、Semaphore等。
AQS没有锁之类的概念,它有个state变量,是个int类型,在不同场合有着不同含义。本文研究的是锁,为了好理解,姑且先把state当成锁。
AQS围绕state提供两种基本操作“获取”和“释放”,有条双向队列存放阻塞的等待线程,并提供一系列判断和处理方法,简单说几点:
- state是独占的,还是共享的;
- state被获取后,其他线程需要等待;
- state被释放后,唤醒等待线程;
- 线程等不及时,如何退出等待。
至于线程是否可以获得state,如何释放state,就不是AQS关心的了,要由子类具体实现。
直接分析AQS的代码会比较难明白,所以结合子类ReentrantLock来分析。AQS的功能可以分为独占和共享,ReentrantLock实现了独占功能,是本文分析的目标。
ReentrantLock对比synchronized
Lock lock = new ReentranLock();
lock.lock();
try{
//do something
}finally{
lock.unlock();
}
ReentrantLock实现了Lock接口,加锁和解锁都需要显式写出,注意一定要在适当时候unlock。
和synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
公平锁和非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
- 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
- 非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。
ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。
尝试获取锁
final void lock() { acquire(1);}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先来看公平锁的实现,lock方法很简单的一句话调用AQS的acquire方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
噢,AQS的tryAcquire不能直接调用,因为是否获取锁成功是由子类决定的,直接看ReentrantLock的tryAcquire的实现。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
获取锁成功分为两种情况,第一个if判断AQS的state是否等于0,表示锁没有人占有。接着,hasQueuedPredecessors判断队列是否有排在前面的线程在等待锁,没有的话调用compareAndSetState使用cas的方式修改state,传入的acquires写死是1。最后线程获取锁成功,setExclusiveOwnerThread将线程记录为独占锁的线程。
第二个if判断当前线程是否为独占锁的线程,因为ReentrantLock是可重入的,线程可以不停地lock来增加state的值,对应地需要unlock来解锁,直到state为零。
如果最后获取锁失败,下一步需要将线程加入到等待队列。
线程进入等待队列
AQS内部有一条双向的队列存放等待线程,节点是Node对象。每个Node维护了线程、前后Node的指针和等待状态等参数。
线程在加入队列之前,需要包装进Node,调用方法是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决定,ReentrantLock自然是使用独占模式Node.EXCLUSIVE。
创建好Node后,如果队列不为空,使用cas的方式将Node加入到队列尾。注意,这里只执行了一次修改操作,并且可能因为并发的原因失败。因此修改失败的情况和队列为空的情况,需要进入enq。
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是个死循环,保证Node一定能插入队列。注意到,当队列为空时,会先为头节点创建一个空的Node,因为头节点代表获取了锁的线程,现在还没有,所以先空着。
阻塞等待线程
线程加入队列后,下一步是调用acquireQueued阻塞线程。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//1
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//2
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
标记1是线程唤醒后尝试获取锁的过程。如果前一个节点正好是head,表示自己排在第一位,可以马上调用tryAcquire尝试。如果获取成功就简单了,直接修改自己为head。这步是实现公平锁的核心,保证释放锁时,由下个排队线程获取锁。(看到线程解锁时,再看回这里啦)
标记2是线程获取锁失败的处理。这个时候,线程可能等着下一次获取,也可能不想要了,Node变量waitState描述了线程的等待状态,一共四种情况:
static final int CANCELLED = 1; //取消
static final int SIGNAL = -1; //下个节点需要被唤醒
static final int CONDITION = -2; //线程在等待条件触发
static final int PROPAGATE = -3; //(共享锁)状态需要向后传播
shouldParkAfterFailedAcquire传入当前节点和前节点,根据前节点的状态,判断线程是否需要阻塞。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- 前节点状态是SIGNAL时,当前线程需要阻塞;
- 前节点状态是CANCELLED时,通过循环将当前节点之前所有取消状态的节点移出队列;
- 前节点状态是其他状态时,需要设置前节点为SIGNAL。
如果线程需要阻塞,由parkAndCheckInterrupt方法进行操作。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
parkAndCheckInterrupt使用了LockSupport,和cas一样,最终使用UNSAFE调用Native方法实现线程阻塞(以后有机会就分析下LockSupport的原理,park和unpark方法作用类似于wait和notify)。最后返回线程唤醒后的中断状态,关于中断,后文会分析。
到这里总结一下获取锁的过程:线程去竞争一个锁,可能成功也可能失败。成功就直接持有资源,不需要进入队列;失败的话进入队列阻塞,等待唤醒后再尝试竞争锁。
释放锁
通过上面详细的获取锁过程分析,释放锁过程大概可以猜到:头节点是获取锁的线程,先移出队列,再通知后面的节点获取锁。
public void unlock() {
sync.release(1);
}
ReentrantLock的unlock方法很简单地调用了AQS的release:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
和lock的tryAcquire一样,unlock的tryRelease同样由ReentrantLock实现:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
因为锁是可以重入的,所以每次lock会让state加1,对应地每次unlock要让state减1,直到为0时将独占线程变量设置为空,返回标记是否彻底释放锁。
最后,调用unparkSuccessor将头节点的下个节点唤醒:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
寻找下个待唤醒的线程是从队列尾向前查询的,找到线程后调用LockSupport的unpark方法唤醒线程。被唤醒的线程重新执行acquireQueued里的循环,就是上文关于acquireQueued标记1部分,线程重新尝试获取锁。
中断锁
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
在acquire里还有最后一句代码调用了selfInterrupt,功能很简单,对当前线程产生一个中断请求。
为什么要这样操作呢?因为LockSupport.park阻塞线程后,有两种可能被唤醒。
第一种情况,前节点是头节点,释放锁后,会调用LockSupport.unpark唤醒当前线程。整个过程没有涉及到中断,最终acquireQueued返回false时,不需要调用selfInterrupt。
第二种情况,LockSupport.park支持响应中断请求,能够被其他线程通过interrupt()唤醒。但这种唤醒并没有用,因为线程前面可能还有等待线程,在acquireQueued的循环里,线程会再次被阻塞。parkAndCheckInterrupt返回的是Thread.interrupted(),不仅返回中断状态,还会清除中断状态,保证阻塞线程忽略中断。最终acquireQueued返回true时,真正的中断状态已经被清除,需要调用selfInterrupt维持中断状态。
因此普通的lock方法并不能被其他线程中断,ReentrantLock是可以支持中断,需要使用lockInterruptibly。
两者的逻辑基本一样,不同之处是parkAndCheckInterrupt返回true时,lockInterruptibly直接throw new InterruptedException()。
非公平锁
分析完公平锁的实现,还剩下非公平锁,主要区别是获取锁的过程不同。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
在NonfairSync的lock方法里,第一步直接尝试将state修改为1,很明显,这是抢先获取锁的过程。如果修改state失败,则和公平锁一样,调用acquire。
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;
}
nonfairTryAcquire和tryAcquire乍一看几乎一样,差异只是缺少调用hasQueuedPredecessors。这点体验出公平锁和非公平锁的不同,公平锁会关注队列里排队的情况,老老实实按照FIFO的次序;非公平锁只要有机会就抢占,才不管排队的事。
总结
从ReentrantLock的实现完整分析了AQS的独占功能,总的来讲并不复杂。别忘了AQS还有共享功能,下一篇是--分析CountDownLatch的实现原理。
作者:展翅而飞
链接:http://www.jianshu.com/p/fe027772e156
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
分析ReentrantLock的实现原理的更多相关文章
- java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析
java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...
- 深入ReentrantLock的实现原理和源码分析
ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Sync ...
- 面经手册 · 第17篇《码农会锁,ReentrantLock之AQS原理分析和实践使用》
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 如果你相信你做什么都能成,你会自信的多! 千万不要总自我否定,尤其是职场的打工人.如 ...
- jQuery 2.0.3 源码分析Sizzle引擎解析原理
jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理 声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 先来回答博友的提问: 如何解析 div > p + ...
- 编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理
很久没有写过 .NET Core 相关的文章了,目前关店在家休息所以有些时间写一篇新的
- Java并发包4--可重入锁ReentrantLock的实现原理
前言 ReentrantLock是JUC提供的可重入锁的实现,用法上几乎等同于Synchronized,但是ReentrantLock在功能的丰富性上要比Synchronized要强大. 一.Reen ...
- 【Java面试】请说一下ReentrantLock的实现原理?
一个工作了3年的粉丝私信我,在面试的时候遇到了这样一个问题. "请说一下ReentrantLock的实现原理",他当时根据自己的理解零零散散的说了一些. 但是似乎没有说到关键点上, ...
- AQS实现原理分析——ReentrantLock
在Java并发包java.util.concurrent中可以看到,不少源码是基于AbstractQueuedSynchronizer(以下简写AQS)这个抽象类,因为它是Java并发包的基础工具类, ...
- 透过 ReentrantLock 分析 AQS 的实现原理
对于 Java 开发者来说,都会碰到多线程访问公共资源的情况,这时候,往往都是通过加锁来保证访问资源结果的正确性.在 java 中通常采用下面两种方式来解决加锁得问题: synchronized 关键 ...
随机推荐
- 前端开发 - HTML
1.index2.head标签相关内容3.常用标签一4.常用标签二 table5.常用标签二 form6.标签分类 1.index <!--声明文档的类型 标记该文档为HTML5的文件--> ...
- HDFS集群安装
DFS集群安装: 1.准备工作 (1)虚拟机(电脑8G 磁盘500GB) (2)3台linux系统(1台namenode 2台datanode) 2.安装HDFS(软件) (1)关闭防火墙 firew ...
- var 声明变量的变量提升问题(let/const)
在ES6之前,JavaScript没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域.变量提升即将变量声明提升到它所在作用域的最开始的部分.既全局变量. <scrip ...
- 重新编写equals()方法,hashCode()方法,以及toString(),提供自定义的相等标准,以及自描述方法
下面给出一个实例,重新编写equals()方法,提供自定义的相等标准 public class PersonTest { public static void main(String[] args) ...
- C语言中const和数组
C语言中const的用法 const:在定义变量时,如果使用关键字const,那就表示限制这个变量值不允许被改变. (1) 修饰变量 const离谁越近,谁的值就不能改变. int const ...
- android 弹出带按钮的对话框
package com.example.helloworld; import android.os.Bundle;import android.app.Activity;import android. ...
- Linux系统——rpm命令
(1)查看操作系统名字.版本cat /etc/redhat-release 查看系统内核kernel的版本号uname -r查看操作系统位数uname -m (2)rpm命令rpm 参数 软件名-q ...
- Log4net 自定义字段 写入Oracle 使用ODP.NET Managed驱动
一.环境说明: 开发工具:vs2010 ,数据库:oracle 11g ,版本:log4net的目前最新版本1.2.13.0 : Oracle.ManagedDataAccess.dll ...
- MySql—模糊查询
实例: SQL模糊查询,使用like比较关键字,加上SQL里的通配符,请参考以下: 1.LIKE 'Mc%' 将搜索以字母 Mc 开头的所有字符串(如 McBadden). 2.LIKE '%in ...
- PKU 1655 Balancing Act(树+树的重心)
#include<cstdio> #include<cstring> #include<algorithm> #define maxn 20005 using na ...