ReentrantLock 实现原理
使用 synchronize
来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。
而 ReentrantLock
就是一个普通的类,它是基于 AQS(AbstractQueuedSynchronizer)
来实现的。
是一个重入锁:一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况。
AQS
是Java
并发包里实现锁、同步的一个重要的基础框架。
锁类型
ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型:
//默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多(后面会分析具体原因)。
获取锁
通常的使用方式如下:
private ReentrantLock lock = new ReentrantLock();
public void run() {
lock.lock();
try {
//do bussiness
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
公平锁获取锁
首先看下获取锁的过程:
public void lock() {
sync.lock();
}
可以看到是使用 sync
的方法,而这个方法是一个抽象方法,具体是由其子类(FairSync
)来实现的,以下是公平锁的实现:
final void lock() {
acquire(1);
}
//AbstractQueuedSynchronizer 中的 acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
第一步是尝试获取锁(tryAcquire(arg)
),这个也是由其子类实现:
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;
}
}
首先会判断 AQS
中的 state
是否等于 0,0 表示目前没有其他线程获得锁,当前线程就可以尝试获取锁。
注意:尝试之前会利用 hasQueuedPredecessors()
方法来判断 AQS 的队列中中是否有其他线程,如果有则不会尝试获取锁(这是公平锁特有的情况)。
如果队列中没有线程就利用 CAS 来将 AQS 中的 state 修改为1,也就是获取锁,获取成功则将当前线程置为获得锁的独占线程(setExclusiveOwnerThread(current)
)。
如果 state
大于 0 时,说明锁已经被获取了,则需要判断获取锁的线程是否为当前线程(ReentrantLock
支持重入),是则需要将 state + 1
,并将值更新。
写入队列
如果 tryAcquire(arg)
获取锁失败,则需要用 addWaiter(Node.EXCLUSIVE)
将当前线程写入队列中。
写入之前需要将当前线程包装为一个 Node
对象(addWaiter(Node.EXCLUSIVE)
)。
AQS 中的队列是由 Node 节点组成的双向链表实现的。
包装代码:
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
利用 CAS
写入队尾,如果出现并发写入失败就需要调用 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;
}
}
}
}
这个处理逻辑就相当于自旋
加上 CAS
保证一定能写入队列。
挂起等待线程
写入队列之后需要将当前线程挂起(利用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
):
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
首先会根据 node.predecessor()
获取到上一个节点是否为头节点,如果是则尝试获取一次锁,获取成功就万事大吉了。
如果不是头节点,或者获取锁失败,则会根据上一个节点的 waitStatus
状态来处理(shouldParkAfterFailedAcquire(p, node)
)。
waitStatus
用于记录当前节点的状态,如节点取消、节点等待等。
shouldParkAfterFailedAcquire(p, node)
返回当前线程是否需要挂起,如果需要则调用 parkAndCheckInterrupt()
:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
他是利用 LockSupport
的 part
方法来挂起当前线程的,直到被唤醒。
非公平锁获取锁
公平锁与非公平锁的差异主要在获取锁:
公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。
而非公平锁则没有这些规则,是抢占模式,每来一个人不会去管队列如何,直接尝试获取锁。
非公平锁:
final void lock() {
//直接尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
公平锁:
final void lock() {
acquire(1);
}
还要一个重要的区别是在尝试获取锁时tryAcquire(arg)
,非公平锁是不需要判断队列中是否还有其他线程,也是直接尝试获取锁:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//没有 !hasQueuedPredecessors() 判断
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;
}
释放锁
公平锁和非公平锁的释放流程都是一样的:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒被挂起的线程
unparkSuccessor(h);
return true;
}
return false;
}
//尝试释放锁
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;
}
首先会判断当前线程是否为获得锁的线程,由于是重入锁所以需要将 state
减到 0 才认为完全释放锁。
释放之后需要调用 unparkSuccessor(h)
来唤醒被挂起的线程。
总结
由于公平锁需要关心队列的情况,得按照队列里的先后顺序来获取锁(会造成大量的线程上下文切换),而非公平锁则没有这个限制。
所以也就能解释非公平锁的效率会被公平锁更高。
号外
最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。
ReentrantLock 实现原理的更多相关文章
- ReentrantLock实现原理深入探究
前言 这篇文章被归到Java基础分类中,其实真的一点都不基础.网上写ReentrantLock的使用.ReentrantLock和synchronized的区别的文章很多,研究ReentrantLoc ...
- (转)ReentrantLock实现原理及源码分析
背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...
- 【Java并发编程】15、ReentrantLock实现原理深入探究
原文已经写得非常详细了,直接把大神的文章转发过来了 https://www.cnblogs.com/xrq730/p/4979021.html 前言 这篇文章被归到Java基础分类中,其实真的一点都 ...
- ReentrantLock实现原理
以下是本篇文章的大纲 1 synchronized和lock 1.1 synchronized的局限性 1.2 Lock简介 2 AQS 3 lock()与unlock()实现原理 3.1 基础知识 ...
- ReentrantLock实现原理及源码分析
ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Sync ...
- 解析ReentrantLock实现原理
在Java中通常实现锁有两种方式,一种是synchronized关键字,另一种是Lock(Lock的实现主要有ReentrantLock.ReadLock和WriteLock).synchronize ...
- ReentrantLock的原理解析
重入锁(ReentrantLock)是一种可重入无阻塞的同步机制.性能同synchronized接近(老版本jdk中性能很差). 下面重点看下常用的lock()和unlock()方法的实现原理. lo ...
- 什么是 AQS?简单说一下 ReentrantLock 的原理?
AQS 简介 java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchr ...
- 聊聊ReentrantLock实现原理
ReentrantLock 是常用的锁,相对于Synchronized ,lock锁更人性化,阅读性更强 从LOCK切入 考虑下面的场景如果有A,B线程,同时去执行lock.lock(Lock loc ...
随机推荐
- 并发编程 futuretask
package com.mrbird.api.demoThread.future; import java.util.concurrent.Callable; import java.util.con ...
- ConcurrentHashmap详解以及在JDK1.8的更新
因为hashmap本身是非线程安全的,如果多线程对hashmap进行put操作的话,就会导致死循环等现象.ConcurrentHashMap主要就是为了应对hashmap在并发环境下不安全而诞生的,C ...
- 关于<Servlet>定义
1,百度百科定义: Servlet,称为小服务程序或服务连接器,用Java编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容 ...
- 感动到哭的SBT下载
在centos上搭建spark开发环境.使用IntelliJ IDEA做scala开发,需要配置SBT.一直卡在从maven上下载jar包的过程中,还几次都下失败了.试过vpn也没有用. 还好偶然看到 ...
- SpringBoot主程序注解@SpringBootApplication简单分析
一.@SpringBootApplication说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用: @SpringBootA ...
- vue笔记-条件渲染
条件渲染 1:指令v-if单独使用和结合v-else //单独使用 <h1 v-if="ok">Yes</h1> //组合使用 <h1 v-if=&q ...
- Win Server 2003 10条小技巧
微软推出Windows Server 2003已经有一段时间了,但是,由于它是一个面向企业用户的服务器操作系统,所以,没有引起更多个人用户的注意.实际上,简单地改变一下系统的设置,您也可以将Windo ...
- [POJ2259]Team Queue (队列,模拟)
2559是栈,2259是队列,真的是巧啊 题意 模拟队列 思路 水题 代码 因为太水,不想打,发博客只是为了与2559照应,于是附上lyd的std #include <queue> #in ...
- Windows环境下,本地Oracle创建dblink连接远程mysql
前言 我的情况是,本地安装了oracle(安装完成后带有SQL Developer,不需要再安装instantclient),创建dblink去连接远程的mysql.有些朋友可能是 本地使用PL\SQ ...
- Gcd&Exgcd算法学习小记
Preface 对于许多数论问题,都需要涉及到Gcd,求解Gcd,常常使用欧几里得算法,以前也只是背下来,没有真正了解并证明过. 对于许多求解问题,可以列出贝祖方程:ax+by=Gcd(a,b),用E ...