引言

synchronized未优化之前,我们在编码中使用最多的同步工具类应该是ReentrantLock类,ReentrantLock拥有优化后synchronized关键字的性能,又提供了更多的灵活性。相比synchronized,他在功能上更加强大,具有等待可中断,公平锁以及绑定多个条件等synchronized不具备的功能,是我们开发过程中必须要重点掌握的一个关键并发类。

ReentrantLock在JDK并发包中举足轻重,不仅是因为他本身的使用频度,同时他也为大量JDK并发包中的并发类提供底层支持,包括CopyOnWriteArrayLitCyclicBarrierLinkedBlockingDeque等等。既然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();
......
}

调用了synclock()方法,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的更多相关文章

  1. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  2. Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock(转)

    本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步 ...

  3. Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock

    本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步 ...

  4. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  5. Java并发编程-看懂AQS的前世今生

    在具备了volatile.CAS和模板方法设计模式的知识之后,我们可以来深入学习下AbstractQueuedSynchronizer(AQS),本文主要想从AQS的产生背景.设计和结构.源代码实现及 ...

  6. Java并发编程:用AQS写一把可重入锁

    Java并发编程:自己动手写一把可重入锁详述了如何用synchronized同步的方式来实现一把可重入锁,今天我们来效仿ReentrantLock类用AQS来改写一下这把锁.要想使用AQS为我们服务, ...

  7. JAVA并发编程: CAS和AQS

       版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/u010862794/article/details/72892300 说起JAVA并发编程,就不得不聊 ...

  8. Java并发指南9:AQS共享模式与并发工具类的实现

    一行一行源码分析清楚 AbstractQueuedSynchronizer (三) 转自:https://javadoop.com/post/AbstractQueuedSynchronizer-3 ...

  9. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  10. 【Java并发编程实战】—– AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了 ...

随机推荐

  1. 11 TCP实现QQ聊天

    1.客户端参考代码 #coding=utf-8 from socket import * # 创建socket tcpClientSocket = socket(AF_INET, SOCK_STREA ...

  2. Laravel框架

    curl操作: ===================================初始化$ch=curl_init() 设置请求的urlcurl_setopt($ch,CURLOPT_URL,&q ...

  3. Linux 下 PHP 扩展 PDO 编译安装

    1.进入 PHP 的软件包 pdo 扩展目录中(注:不是 PHP 安装目录) [root@tester /]# /home/tdweb/php-5.4.34/ext/pdo_mysql 执行 phpi ...

  4. mysql连接jdbc查询代码

    package com.answer.test; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.S ...

  5. 多个Target的使用

    背景介绍 开发过程中,我们会在内网搭建一个测试服务器,开发.测试都是在内网进行的.这样产生脏数据不会影响外网的服务器.外网服务器只有最后发布时才会进行一些必要的测试. 还有就是要对同一份代码生成不同的 ...

  6. MQTT 开源代理mosquitto的网络层封装相当sucks

    最近学习MQTT协议,选择了当前比较流行的MQTT Broker “mosquitto”,但是在阅读代码过程中发现其网络底层库封装的相当差劲. 对于MQTT协议的变长头长度的读取上,基本上采取每次一个 ...

  7. webpack使用时可能出现的问题

    1.在配置完webpack.config.js准备进行热加载开发时,修改React内容浏览器不会自动局部刷新,而且会console出一些提示: The following modules couldn ...

  8. 【Dataset】Goodbooks-10k: 图书推荐数据

    当前推荐领域一些公开的据集都是关于电影和音乐的(比如Netflix.Movielens等),没有关于图书推荐的数据.本文将要介绍的就是一份用于图书推荐的数据集,该数据来源于goodreads网站,包含 ...

  9. 修复 Ubuntu 中“Unable to lock the administration directory (/var/lib/dpkg/)”

    在 Ubuntu 或者它的衍生版如 Linux Mint(我已经作为日常工作使用的系统)中使用 apt-get 命令或者其相对更新的APT 管理工具时,你可能会在命令行中看到一个 unable to ...

  10. TLS协议分析

    TLS协议分析 本文目标: 学习鉴赏TLS协议的设计,透彻理解原理和重点细节 跟进一下密码学应用领域的历史和进展 整理现代加密通信协议设计的一般思路 本文有门槛,读者需要对现代密码学有清晰而系统的理解 ...