ReentrantLock 基本信息介绍
ReentrantLock是Java中的一个互斥锁,它允许线程重入锁定的代码段,而不会产生死锁。它实现了Lock 接口,内部继承了AQS(AbstractQueuedSynchronizer) 并提供了比synchronized更多的灵活性和功能,例如可中断锁、超时锁、公平锁和多条件变量。ReentrantLock的主要作用是实现线程间的同步和互斥访问共享资源,保证数据的一致性和完整性。
在锁竞争的比较激烈的情况,优先推荐使用ReentrantLock,具体原因以后解析synchronized的时候再来详细说明。
ReentranLock源码
AQS
是什么是AQS?
AQS(AbstractQueuedSynchronizer)是 Java 中用于实现同步器的抽象基类。AQS 提供了一些队列操作方法,可以方便地实现同步器的内部队列。在并发编程中,AQS 通常用于实现锁和同步器等。
AQS 的 关键属性
因为ReentranLock使用了大量的AQS的方法,这里要先对AQS的关键属性进行介绍以方便后续的理解,简单来说就是 state 、一个单向链表 和 一个双向链表。
state:
private volatile int state;
主要的作用是用来做一些锁的标志位,如:ReentranLock 加锁的时候会使用CAS的方式将这个属性由0变为1,重入的情况下会对这个属性不断+1,因为它是int的类型所以它的重入次数是有限制的。
双向链表:
private transient volatile Node head;
private transient volatile Node tail;
用来储存挂起的线程,在节点中会维护五个状态。
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
0: 默认状态
1: 当前节点取消了
-1:后续节点挂起了
-2:当前节点ConditionObject类(单项列表)中
-3:当前节点是共享锁状态
单向链表:
public class ConditionObject implements Condition, java.io.Serializable
维护在Condition中主要的作用使用用来储存一些被Wait()的线程等待被Notify唤醒。
生成对象
根据传入的参数决定生成公平锁/非公平锁,值得注意的是 FairSync
和 NonfairSync
这两个内部类都继承了 AQS。
public ReentrantLock() {
// 默认走非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
// 会根据传入参数决定走公平的锁/非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
加锁
lock 方法
非公平锁:
//使用 CAS 方法直接尝试将 state 由0改成1;如果成功则抢锁成功
if (compareAndSetState(0, 1))
//抢锁成功后将持有线程设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//抢锁失败调用AQS acquire 方法
acquire(1);
公平锁:
final void lock() {
//直接调用AQS acquire 方法
acquire(1);
}
非公平锁会尝试将用CAS 将 State 由 0 改为 1 ,如果修改成功则抢锁成功,将持有线程设置为当前线程,否则进入 acquire 方法。
公平锁会直接进入 acquire 方法。
acquire 方法
该方法主要的作用调用抢锁和加入双向列表的方法。
public final void acquire(int arg) {
// 这是AQS的方法,会先调用 tryAcquire, tryAcquire 由继承类自行实现,acquireQueued 为挂入双向列表的方法稍后会介绍。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//线程中断方法,对于逻辑的理解不是很重要 Thread.currentThread().interrupt()
selfInterrupt();
}
这里 acquire 是 AQS 的方法,会先调用 tryAcquire,tryAcquire
由继承类自行实现,如果在 tryAcquire 中如果抢锁不成功就会进入到 acquireQueued 方法中。
tryAcquire
tryAcquire是尝试抢锁的方法,由继承类自行实现,公平锁和非公平锁实现方式是不一致的,tryLock是直接调用该方法。
非公平锁:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state状态
int c = getState();
// 如果 state 为 0 证明当前没有线程持有锁进入下列逻辑
if (c == 0) {
// 尝试将 state 由 0 改为 acquires(ReentranLock为1),如果修改成功进入下列的逻辑
if (compareAndSetState(0, acquires)) {
//将持有线程改为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果持有线程为当前线程进入重入逻辑
else if (current == getExclusiveOwnerThread()) {
//将state改为+acquires(ReentranLock为1)
int nextc = c + acquires;
//如果小于0,参数出现问题报错(一般不会出现)
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//将现在的次数设置到state中
setState(nextc);
return true;
}
return false;
}
公平锁:
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state状态
int c = getState();
// 如果 state 为 0 证明当前没有线程持有锁进入下列逻辑
if (c == 0) {
//hasQueuedPredecessors 涉及AQS的双向列表,用来判断线程是否有prev指向其他结点,如果指向其他节点则会返回false,如果指向head节点或者链表没被初始化则会返回false。
//简单来说这个步骤是为了检查该线程是否是在队列中的第一位。
if (!hasQueuedPredecessors() &&
// 尝试将 state 由 0 改为 acquires(ReentranLock为1),如果修改成功进入下列的逻辑
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;
}
这里是公平锁与非公平锁差距比较大的地方,具体的差距已经标注在上述的备注中;用一句话简单来表达就是非公平锁在进入方法的时候会直接进行抢锁;而公平锁会先进行判断自己是否处于双向队列的第一位,如果是在进行抢锁。
addWaiter & acquireQueued 方法
没错这又是AQS的方法,这里先插入讲一下双向列表和它的插入逻辑有助于帮助了解下面的逻辑。
以下讲解下大概几个要点:
1、双向链表 head 是一个 虚节点,它的存在是为了更好的标识后续节点的状态。( -1:后续节点挂起了)
2、插入双向链表的过程如上图所示:为了解释方便我们把当前节点的前节点成为前节点;先将当前节点 PREV 指针指向前节点,再用 CAS 的方式将尾结点设置为当前节点,最后前节点会先把 NEXT 的指针指向当前节点。
addWaiter 方法:
//将当前节点插入双线链表
private Node addWaiter(Node mode) {
//封装Node
Node node = new Node(Thread.currentThread(), mode);
// 前节点为tail节点
Node pred = tail;
//前节点不为空,证明链表已经初始化,进入逻辑。
if (pred != null) {
//1、将当前节点前指针指向原尾结点
node.prev = pred;
//2、将当前节点用 CAS 的方式设置为尾结点
if (compareAndSetTail(pred, node)) {
//3、将前节点指向当前节点
pred.next = node;
return node;
}
}
//设置不成进入enq方法
enq(node);
return node;
}
private Node enq(final Node node) {
//死循环到加入为止
for (;;) {
Node t = tail;
//尾节点为空,证明链表未初始化,进入初始化
if (t == null) {
//创建虚拟头结点,thread为null
if (compareAndSetHead(new Node()))
tail = head;
} else {
//和addWaiter中的插入双向链表逻辑一致
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取Node 的前节点
final Node p = node.predecessor();
//前节点如果为 head 证明当前节点已经排在队列中的第一位,则直接抢锁。
if (p == head && tryAcquire(arg)) {
//将当前节点设置头结点,thread == null, node.prev == null.
setHead(node);
//原头结点帮助快速GC,详情参照JVM GC原理(后续也许我会更新)。
p.next = null;
failed = false;
return interrupted;
}
//负责轮询到前节点为 -1 的节点。
if (shouldParkAfterFailedAcquire(p, node) &&
//unsafe.park() 方法挂起线程,顺便检查一下线程是否被中断。
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//检查前节点状态是否为 -1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前节点的状态
int ws = pred.waitStatus;
//如果前节点状态为 -1, 后续节点挂起了, 则返回 True 然后后续代码执行挂起。
if (ws == Node.SIGNAL)
return true;
//若大于0,按照AQS的实现一般情况下为1,则表明线程已经取消了,进入逻辑。
if (ws > 0) {
//指向前节点的前节点,把前节点删除掉,直到前节点不为1为止。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//将前节点的状态更新为 -1(后续节点挂起)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
其他锁的方式
tryLock() 直接尝试调用了一次nonfairTryAcquire() 方法。
tryLock(time) 主要是使用了 doAcquireNanos 方法没什么太大的区别,多了一个时间控制挂起的时间;具体以后有时间的会更新到这篇文章中。
lockInterruptibly() 差别更小了,就是在检测到中断标识位会抛出InterruptedException()异常。
解锁
释放锁的逻辑公平锁和非公平锁是一致的。
unlock方法:
public void unlock() {
sync.release(1);
}
release方法:
又是AQS方法哦
public final boolean release(int arg) {
//尝试释放tryRelease方法由继承类具体实现
if (tryRelease(arg)) {
//获取头结点
Node h = head;
//如果头结点不为空,且状态不为0(0状态是默认状态,表示它后续没有挂载结点)。
if (h != null && h.waitStatus != 0)
//唤醒NEXT节点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//如果当前节点可以激活后续节点,则使用 CAS 修改节点状态为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//如果节点为null 或者 状态为 1, 则需要从双向队列队尾开始向前循环,找到的第一个为被取消的节点。
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);
}
在这里会先尝试释放结点,一般来说是减掉state的状态,如果成功释放则继续释放下一个结点。
为什么双向队列要从前往后循环呢?
这和上面提到的双向队列的加入方式有关;双向队列是先将当前节点 PREV 指针指向前节点,再用 CAS 的方式将尾结点设置为当前节点,最后前节点会先把 NEXT 的指针指向当前节点; 这个过程中并发的情况发生可能会导致双向队列已经被设置到了队尾,但是前节点 的NEXT指针还没有指向当前节点导致并发安全问题发生,所以要从后往前遍历。
tryRelease方法:
protected final boolean tryRelease(int releases) {
//将当前状态 -1
int c = getState() - releases;
// 判断是否是当前线程,如果不是当前线程则报错,一般不会出现。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果state == 0, 则标识就可以彻底被释放了
if (c == 0) {
free = true;
//将持有线程设置为null
setExclusiveOwnerThread(null);
}
//修改状态
setState(c);
return free;
}
因为 ReentranLock 是可重入锁,重入多次就会不断地在 state 上 +1, 相反释放锁则会 -1;当 state 被减到 0 的时候则表示这个可以被彻底释放了。
作者的话
本来今晚是想发布两篇的(还有一篇是读写锁的),但是没想到这一篇我写了快四个小时,现在已经快12点了,没办法完成另外一篇了,后续会持续更新关于我学习并发编程源码的过程。如果什么错误可以提出,有什么疑问也可以直接提问,可以直接评论或者发送到我的邮箱(965479041@qq.com)我看到就会回复。