Java并发(5)- ReentrantLock与AQS
引言
在synchronized
未优化之前,我们在编码中使用最多的同步工具类应该是ReentrantLock
类,ReentrantLock
拥有优化后synchronized
关键字的性能,又提供了更多的灵活性。相比synchronized
,他在功能上更加强大,具有等待可中断,公平锁以及绑定多个条件等synchronized
不具备的功能,是我们开发过程中必须要重点掌握的一个关键并发类。
ReentrantLock
在JDK并发包中举足轻重,不仅是因为他本身的使用频度,同时他也为大量JDK并发包中的并发类提供底层支持,包括CopyOnWriteArrayLit
、CyclicBarrier
和LinkedBlockingDeque
等等。既然ReentrantLock
如此重要,那么了解他的底层实现原理对我们在不同场景下灵活使用ReentrantLock
以及查找各种并发问题就很关键。这篇文章就带领大家一步步剖析ReentrantLock
底层的实现逻辑,了解实现逻辑之后又应该怎么更好的使用ReentrantLock
。
ReentrantLock与AbstractQueuedSynchronizer的关系
在使用ReentrantLock
类时,第一步就是对他进行实例化,也就是使用new ReentrantLock()
,来看看他的实例化的源码:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
在代码中可以看到,ReentrantLock
提供了2个实例化方法,未带参数的实例化方法默认用NonfairSync()
初始化了sync
字段,带参数的实例化方法通过参数区用NonfairSync()
或FairSync()
初始化sync
字段。
通过名字看出也就是我们常用的非公平锁与公平锁的实现,公平锁需要通过排队FIFO的方式来获取锁,非公平锁也就是说可以插队,默认情况下ReentrantLock
会使用非公平锁的实现。那么是sync
字段的实现逻辑是什么呢?看下sync
的代码:
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {......}
static final class NonfairSync extends Sync {......}
static final class FairSync extends Sync {......}
到这里就发现了AbstractQueuedSynchronizer
类,公平锁和非公平锁其实都是在AbstractQueuedSynchronizer
的基础上实现的,也就是AQS。AQS提供了ReentrantLock
实现的基础。
ReentrantLock的lock()方法
分析了ReentrantLock
的实例化之后,来看看他是怎么实现锁这个功能的:
//ReentrantLock的lock方法
public void lock() {
sync.lock();
}
//调用了Sync中的lock抽象方法
abstract static class Sync extends AbstractQueuedSynchronizer {
......
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
......
}
调用了sync
的lock()
方法,Sync
类的lock()
方法是一个抽象方法,NonfairSync()
和FairSync()
分别对lock()
方法进行了实现。
//非公平锁的lock实现
static final class NonfairSync extends Sync {
......
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1)) //插队操作,首先尝试CAS获取锁,0为锁空闲
setExclusiveOwnerThread(Thread.currentThread()); //获取锁成功后设置当前线程为占有锁线程
else
acquire(1);
}
......
}
//公平锁的lock实现
static final class FairSync extends Sync {
......
final void lock() {
acquire(1);
}
......
}
注意看他们的区别,NonfairSync()
会先进行一个CAS操作,将一个state状态从0设置到1,这个也就是上面所说的非公平锁的“插队”操作,前面讲过CAS操作默认是原子性的,这样就保证了设置的线程安全性。这是非公平锁和公平锁的第一点区别。
那么这个state状态是做什么用的呢?从0设置到1又代表了什么呢?再来看看跟state有关的源码:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
/**
* The synchronization state.
*/
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
首先state变量是一个volatile
修饰的int
类型变量,这样就保证了这个变量在多线程环境下的可见性。从变量的注释“The synchronization state”可以看出state代表了一个同步状态。再回到上面的lock()
方法,在设置成功之后,调用了setExclusiveOwnerThread
方法将当前线程设置给了一个私有的变量,这个变量代表了当前获取锁的线程,放到了AQS的父类AbstractOwnableSynchronizer
类中实现。
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
......
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
如果设置state成功,lock()
方法执行完毕,代表获取了锁。可以看出state状态就是用来管理是否获取到锁的一个同步状态,0代表锁空闲,1代表获取到了锁。那么如果设置state状态不成功呢?接下来会调用acquire(1)
方法,公平锁则直接调用acquire(1)
方法,不会用CAS操作进行插队。acquire(1)
方法是实现在AQS中的一个方法,看下他的源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法很重要也很简单理解,有几步操作,首先调用tryAcquire
尝试获取锁,如果成功,则执行完毕,如果获取失败,则调用addWaiter
方法添加当前线程到等待队列,同时添加后执行acquireQueued
方法挂起线程。如果挂起等待中需要中断则执行selfInterrupt
将线程中断。下面来具体看看这个流程执行的细节,首先看看tryAcquire
方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//NonfairSync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //锁空闲
if (compareAndSetState(0, acquires)) { //再次cas操作获取锁
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;
}
//FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //判断队列中是否已经存在等待线程,如果存在则获取锁失败,需要排队
compareAndSetState(0, acquires)) { //不存在等待线程,再次cas操作获取锁
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中实现,判断队列中是否已经存在等待线程
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
AQS没有提供具体的实现,ReentrantLock
中公平锁和非公平锁分别有自己的实现。非公平锁在锁空闲的状态下再次CAS操作尝试获取锁,保证线程安全。如果当前锁非空闲,也就是state状态不为0,则判断是否是重入锁,也就是同一个线程多次获取锁,是重入锁则将state状态+1,这也是
ReentrantLock`支持锁重入的逻辑。
公平锁和非公平锁在这上面有第二点区别,公平锁在锁空闲时首先会调用hasQueuedPredecessors
方法判断锁等待队列中是否存在等待线程,如果存在,则不会去尝试获取锁,而是走接下来的排队流程。至此非公平锁和公平锁的区别大家应该清楚了。如果面试时问道公平锁和非公平锁的区别,相信大家可以很容易答出来了。
通过tryAcquire
获取锁失败之后,会调用acquireQueued(addWaiter)
,先来看看addWaiter
方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //用EXCLUSIVE模式初始化一个Node节点,代表是一个独占锁节点
// 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)) { //cas设置尾节点为当前节点,将当前线程加入到队列末尾,避免多线程设置导致数据丢失
pred.next = node;
return node;
}
}
enq(node); //如果队列中无等待线程,或者设置尾节点不成功,则循环设置尾节点
return 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)) { //重复addWaiter中的设置尾节点,也是cas的经典操作--自旋,避免使用Synchronized关键字导致的线程挂起
t.next = node;
return t;
}
}
}
}
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node(); //共享模式
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null; //独占模式
......
}
addWaiter
方法首先初始化了一个EXCLUSIVE模式的Node节点。Node节点大家应该很熟悉,我写的集合系列文章里面介绍了很多链式结构都是通过这种方式来实现的。AQS中的Node也不例外,他的队列结构也是通过实现一个Node内部类来实现的,这里实现的是一个双向队列。Node节点分两种模式,一种SHARED共享锁模式,一种EXCLUSIVE独占锁模式,ReentrantLock
使用的是EXCLUSIVE独占锁模式,所用用EXCLUSIVE来初始化。共享锁模式后面的文章我们再详细讲解。
初始化Node节点之后就是将节点加入到队列之中,这里有一点要注意的是多线程环境下,如果CAS设置尾节点不成功,需要自旋进行CAS操作来设置尾节点,这样即保证了线程安全,又保证了设置成功,这是一种乐观的锁模式,当然你可以通过synchronized关键字锁住这个方法,但这样效率就会下降,是一种悲观锁模式。
设置节点的过程我通过下面几张图来描述下,让大家有更形象的理解:
将当前线程加入等待队列之后,需要调用acquireQueued
挂起当前线程:
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)) { //如果前置节点是头节点,说明当前节点是第一个挂起的线程节点,再次cas尝试获取锁
setHead(node); //获取锁成功设置当前节点为头节点,当前节点占有锁
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && //非头节点或者获取锁失败,检查节点状态,查看是否需要挂起线程
parkAndCheckInterrupt()) //挂起线程,当前线程阻塞在这里!
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到这个方法是一个自旋的过程,首先获取当前节点的前置节点,如果前置节点为头结点则再次尝试获取锁,失败则挂起阻塞,阻塞被取消后自旋这一过程。是否可以阻塞通过shouldParkAfterFailedAcquire
方法来判断,阻塞通过parkAndCheckInterrupt
方法来执行。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
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) { //代表前置节点已经退出(超时或中断等情况)
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); //前置节点退出,循环设置到最近的一个未退出节点
pred.next = node;
} else { //非可挂起状态或退出状态则尝试设置为Node.SIGNAL状态
/*
* 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);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//挂起当前线程
return Thread.interrupted();
}
只有当节点处于SIGNAL状态时才可以挂起线程,Node的waitStatus有4个状态分别是:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
注释写的很清楚,这里就不详细解释着四种状态了。到这里整个Lock的过程我们就全部说完了,公平锁和非公平锁的区别从Lock的过程中我们也很容易发现,非公平锁一样要进行排队,只不过在排队之前会CAS尝试直接获取锁。说完了获取锁,下面来看下释放锁的过程。
ReentrantLock的unLock()方法
unLock()
方法比较好理解,因为他不需要考虑多线程的问题,如果unLock()
的不是之前lock
的线程,直接退出就可以了。看看unLock()
的源码:
public class ReentrantLock implements Lock, java.io.Serializable {
......
public void unlock() {
sync.release(1);
}
......
}
public abstract class AbstractQueuedSynchronizer {
......
public final boolean release(int arg) {
if (tryRelease(arg)) { //尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //释放锁成功后启动后继线程
return true;
}
return false;
}
......
}
abstract static class Sync extends AbstractQueuedSynchronizer {
......
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //释放锁必须要是获取锁的线程,否则退出,保证了这个方法只能单线程访问
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //独占锁为0后代表锁释放,否则为重入锁,不释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
......
}
abstract static class Sync extends AbstractQueuedSynchronizer {
......
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)
compareAndSetWaitStatus(node, ws, 0);
/*
* 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)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //挂起当前线程
}
......
}
同lock()
方法一样,会调用AQS的release
方法,首先调用tryRelease
尝试释放,首先必须要是当前获取锁的线程,之后判断是否为重入锁,非重入锁则释放当前线程的锁。锁释放之后调用unparkSuccessor
方法启动后继线程。
总结
ReentrantLock
的获取锁和释放锁到这里就讲完了,总的来说还是比较清晰的一个流程,通过AQS的state状态来控制锁获取和释放状态,AQS内部用一个双向链表来维护挂起的线程。在AQS和ReentrantLock之间通过状态和行为来分离,AQS用管理各种状态,并内部通过链表管理线程队列,ReentrantLock则对外提供锁获取和释放的功能,具体实现则在AQS中。下面我通过两张流程图总结了公平锁和非公平锁的流程。
非公平锁:
公平锁:
Java并发(5)- ReentrantLock与AQS的更多相关文章
- 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport
在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...
- Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock(转)
本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步 ...
- Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock
本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步 ...
- 【Java并发编程实战】----- AQS(二):获取锁、释放锁
上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...
- Java并发编程-看懂AQS的前世今生
在具备了volatile.CAS和模板方法设计模式的知识之后,我们可以来深入学习下AbstractQueuedSynchronizer(AQS),本文主要想从AQS的产生背景.设计和结构.源代码实现及 ...
- Java并发编程:用AQS写一把可重入锁
Java并发编程:自己动手写一把可重入锁详述了如何用synchronized同步的方式来实现一把可重入锁,今天我们来效仿ReentrantLock类用AQS来改写一下这把锁.要想使用AQS为我们服务, ...
- JAVA并发编程: CAS和AQS
版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/u010862794/article/details/72892300 说起JAVA并发编程,就不得不聊 ...
- Java并发指南9:AQS共享模式与并发工具类的实现
一行一行源码分析清楚 AbstractQueuedSynchronizer (三) 转自:https://javadoop.com/post/AbstractQueuedSynchronizer-3 ...
- 【Java并发编程实战】----- AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...
- 【Java并发编程实战】—– AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了 ...
随机推荐
- LeetCode:5. Longest Palindromic Substring(Medium)
原题链接:https://leetcode.com/problems/longest-palindromic-substring/description/ 1. 题目要求:找出字符串中的最大回文子串 ...
- 区分Oracle的数据库,实例,服务名,SID
文章摘自:http://www.zhetao.com/content240 感谢分享O(∩_∩)O~ 在实际的开发应用中,关于Oracle数据库,经常听见有人说建立一个数据库,建立一个Instance ...
- Linux 下 PHP 扩展Soap 编译安装
1.进入 PHP 的软件包 pdo 扩展目录中(注:不是 PHP 安装目录) [root@tester /]# /home/tdweb/php-5.4.34/ext/soap 执行 phpize 命令 ...
- (2)分布式下的爬虫Scrapy应该如何做-关于对Scrapy的反思和核心对象的介绍
本篇主要介绍对于一个爬虫框架的思考和,核心部件的介绍,以及常规的思考方法: 一,猜想 我们说的爬虫,一般至少要包含几个基本要素: 1.请求发送对象(sender,对于request的封装,防止被封) ...
- ubuntu自带的ibus输入法问题解决方法
ubuntu自带的ibus有点问题,输入字的时候不知道是个什么模式. 在网上搜到一个解决方法. 终端下执行: ibus-daemon -drx 然后切换到拼音输入法,就正常了. 写下作为记录.
- Linux下创建pycharm的快捷方式
第一步:创建桌面快捷方式文件Pycharm.desktop,并打开 sudo gedit /usr/share/applications/Pycharm.desktop 第二步:在打开的文件Pycha ...
- 远程连接云主机MySql数据库
笔者最近在学习MySql数据库,试着远程连接阿里云主机数据库.在连接过程中遇到不少麻烦,这里总结一下过程中遇到的问题. 基本前提 先在本地电脑和远程主机上安装MySql数据库,保证数据库服务启动. 云 ...
- eclipse 创建Makefile工程生成多个执行文件
1.创建Makefile工程 2.创建inc src Debug 目录 用于存放头文件源文件 3.编写Makefile 需要在有源文件的目标天剑Makefile文件,如下给出一个生成两个target的 ...
- Bitcoin-NG
Bitcoin-NG,一个新的可扩展的区块链协议 Bitcoin-NG仅受限于网络的传输延时,它的带宽仅受限于个人节点的处理能力.通过将比特币的区块链操作分解为两部分来实现这个性能改善:首领选择(le ...
- 提高python执行效率的方法
python上手很容易,但是在使用过程中,怎么才能使效率变高呢? 下面说一下提高python执行效率的方法,这里只是说一点,python在引入模块过程中提高效率的方法. 例如: 1.我们要使用os模块 ...