目录
三、ReentrantLock 和Synchronized相比
4.1.1.3 NonfairSync()和FairSync()
4.1.2.5 shouldParkAfterFailedAcquire()
一、概述
ReentrantLock是一种基于AQS(抽象队列同步器)框架的应用实现,是Java提供的强大且灵活的可重入锁,支持公平和非公平特性。是JDK中一种线程并发访问的同步手段,它提供了与synchronized关键字相似的功能,但具有更多的灵活性和扩展性。
二、ReentrantLock的整体结构
ReentrantLock 实现了Lock通用接口,通过Sync继承了AQS特性,Syns下有两个实现,NonfairSyns和FairSync,分别实现的是非公平锁和公平锁功能,它的整体结构如下图所示:
三、ReentrantLock 和Synchronized相比
- 在功能实现和原理上
首先Synchronized 实现的是公平锁,是基于对象锁实现的并发编程同步关键字;而ReentrantLock是lock接口实现,基于AQS+CAS实现,支持公平锁和非公平锁,相比Synchronized,lock锁提供的功能更完善,lock可以使用tryLock指定等待锁的时间;lock锁还提供了lockInterruptibly允许线程在获取锁的期间被中断。
- 在使用上
Synchronized 是Java关键字,可以作用在方法上,也可以作用在代码块上,实现简单;对于并发编程掌握不好的编程人员来说比较友好。
ReentrantLock 的使用需要自己先初始化ReentrantLock,然后手动调用它的锁方法对同步代码加锁,使用完成后,需要在finally中调用释放锁方法释放。对于并发编程掌握不好的编程人员来说使用成本较高。而且可能极容易出错。
四、ReentrantLock 公平锁和非公平锁实现
4.1 ReentrantLock 源码解读
4.1.1 ReentrantLock 类源码解读
4.1.1.1 Lock接口
首先ReentrantLock实现了Lock接口,Lock接口是Java中对锁操作行为的统一规范,接口的定义如下:
public interface Lock {
// 获取锁
void lock();
// 获取锁--支持响应中断
void lockInterruptibly() throws InterruptedException;
//尝试加锁,返回是否成功状态
boolean tryLock();
// 尝试加锁,返回是否成功状态,支持指定加锁时间,超时响应中断
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 条件变量
Condition newCondition();
}
4.1.1.2 Sync抽象类
Sync继承了AbstractQueuedSynchronizer抽象接口,定义了抽象的lock方法,该方法需要子类自行实现,定义了一个nonfairTryAcquire非公平锁方法,定义了一个释放锁的tryRelease方法,它是ReentrantLock的核心类,具体源码和解释如下:
// 获取锁,需要子类去实现
abstract void lock();
// 非公平锁获取state资源
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前状态
int c = getState();
if (c == 0) { // state等于0,代表可加锁
// 使用cas锁,尝试将state修改为acquires,acquires等于1
if (compareAndSetState(0, acquires)) {
// 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程
setExclusiveOwnerThread(current);
// 返回true状态
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数
// state+1,累加重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个
setState(nextc);
// 返回true,表示成功
return true;
}
// 返回false,表示失败
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
// 获取state状态值,然后减去releases,此处releases为1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果持有锁线程不是当前线程,抛出异常。
throw new IllegalMonitorStateException();
// 返回的状态值,默认为false
boolean free = false;
if (c == 0) { // 如果state-1后等于0,表示释放成功
// 设置返回状态为true
free = true;
// 清空持有锁线程
setExclusiveOwnerThread(null);
}
// 如果state-1后不等于0,说明当前持有锁线程是重入锁状态,需要设置相应的释放次数
setState(c);
return free;
}
4.1.1.3 NonfairSync()和FairSync()
Lock接口定义的函数不多,接下来ReentrantLock要去实现这些函数,遵循着解耦可扩展设计,ReentrantLock内部定义了专门的组件Sync, Sync继承AbstractQueuedSynchronizer提供释放资源的实现,NonfairSync 和 FairSync是基于Sync扩展的子类,他们分别是ReentrantLock的非公平模式与公平模式,它们作为Lock接口功能的基本实现。
4.1.1.3.1 NonfairSync介绍
在 ReentrantLock中支持两种获取锁的策略,分别是非公平策略与公平策略,NonfairSync 就是非公平策略。
此时大家会有问,什么是非公平策略?
在说非公平策略前,先简单的说下AQS(AbstractQueuedSynchronizer)流程,AQS为加锁和解锁过程提供了统一的模板函数,加锁与解锁的模板流程是,获取锁失败的线程,会进入CLH队列阻塞,其他线程解锁会唤醒CLH队列线程,如下图所示(简化流程):
接下来我们来解读下源码,看下NonfairSync是如何实现的
源码:
/**
* 非公平锁实现
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 获取锁
final void lock() {
if (compareAndSetState(0, 1)) // 使用cas设置state状态为1,如果成功,代表加锁成功
// 获取锁成功,设置当前持有锁的线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁失败,执行AQS获取锁的模板方法流程
acquire(1);
}
// 获取锁,使用的Sync提供的nonfairTryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
从源码我们可以知道,NonfairSync继承Sync,然后实现了lock()函数,lock()函数也非常简单,使用CAS设置状态值state为1,如果成功代表获取锁成功,否则执行AQS的acquire()函数( 获取锁模板 );另外NonfairSync还实现了AQS留给子类实现的tryAcquire()函数( 获取资源 ),函数直接使用Sync提供的nonfairTryAcquire()函数来实现tryAcquire(),最后子类实现的tryAcquire()函数在AQS的 acquire函数中被使用。
是不是有点绕?画个图给大家一起缕一缕:
首先 AQS 的acquire()函数是获取锁的流程模板,模板流程会先执行 tryAcquire()函数获取资源,tryAcquire()函数要子类实现,NonfairSync作为子类,实现了tryAcquire()函数,具体实现是调用了 Sync的 nonfairTryAcquire()函数。
我们接着看下nonfairTryAcquire() 的源码是如何实现的:
// 非公平锁获取state资源
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前状态
int c = getState();
if (c == 0) { // state等于0,代表可加锁
// 使用cas锁,尝试将state修改为acquires,acquires等于1
if (compareAndSetState(0, acquires)) {
// 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程
setExclusiveOwnerThread(current);
// 返回true状态
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数
// state+1,累加重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个
setState(nextc);
// 返回true,表示成功
return true;
}
// 返回false,表示失败
return false;
}
对上述代码逻辑我们做个简单的概括,当前线程查看资源是否可获取:
- 可获取,尝试使用CAS设置state为 1,CAS成功代表获取资源成功,否则获取资源失败
- 不可获取,判断当线程是不是持有锁的线程,如果是,state重入计数,获取资源成功,否则获取资源失败
用下图说明一下流程:
4.1.1.3.2 FairSync介绍
有非公平策略,就有公平策略,FairSync 就是公平策略。
所谓公平策略就是,严格按照 CLH 队列顺序获取锁,线程释放锁时,会唤醒 CLH 队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH队列的线程参与竞争,为了保证公平,一定会让CLH队列线程竞争成功,如果非CLH队列线程一直占用时间片,那就一直失败( 构建成节点插入到CLH队尾,由AQS模板流程执行 ),直到时间片轮到CLH队列线程为止,所以公平策略的性能会更差。
接下来我们来看下FairSync 公平锁的源码和解析
源码:
// 公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 获取锁
final void lock() {
// 执行AQS获取锁的模板方法流程
acquire(1);
}
// 获取锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前state状态
int c = getState();
if (c == 0) { // state等于0,代表可加锁
// hasQueuedPredecessors 判断当前线程是不是CLH队列中唤醒的线程
// 使用cas锁,尝试将state修改为acquires,acquires等于1
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数
else if (current == getExclusiveOwnerThread()) {
// state+1,累加重入次数
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个
setState(nextc);
return true;
}
return false;
}
}
其实从上面的源码中我们不难发现 FairSync 的流程与 NonfairSync 基本一致,唯一的区别就是在 CAS执行前,多了一步调用hasQueuedPredecessors()函数,这一步就是判断当前线程是不是 CLH 队列被唤醒的线程,如果是就执行CAS操作,否则获取资源失败,具体流程图如下所示:
4.1.2 ReentrantLock涉及的AQS方法源码
4.1.2.1 acquire()
acquire是一个业务方法,里面并没有实际的业务处理,都是在调用其他方法,首先执行tryAcquire()函数,这个函数其实最终调用的是NonfairSync和FairSync里面的tryAcquire() 函数;在执行加锁不成功后,会将当前线程封装成Node节点加入到CLH队列中,具体源码和解析如下:
// 核心acquire arg = 1
public final void acquire(int arg) {
//1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法。 没有拿到锁资源,
// 需要执行&&后面的方法
//2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,
// 并且插入到AQS的队列的末尾,并且作为tail
//3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源
// 如果没在前面,尝试将线程挂起,阻塞起来!
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.1.2.2 addWaite()
addWaite()函数,主要是将当前获取锁失败的线程,封装成功Node对象节点,加入到CLH队列中,具体源码和解析如下:
// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
private Node addWaiter(Node mode) {
// 将当前线程封装为Node对象,mode为null,代表互斥锁
Node node = new Node(Thread.currentThread(), mode);
// pred是tail节点
Node pred = tail;
if (pred != null) { // 如果pred不为null,有线程正在排队
// 将当前节点的prev,指定tail尾节点
node.prev = pred;
// 以CAS的方式,将当前节点变为tail节点
if (compareAndSetTail(pred, node)) {
// 之前的tail的next指向当前节点
pred.next = node;
return node;
}
}
// 添加的流程为, 自己prev指向、tail指向自己、前节点next指向我
// 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列
enq(node);
return node;
}
4.1.2.3 acquireQueued()
// 查看当前排队的Node是否是head的next,
// 如果是,尝试获取锁资源,
// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 拿到上一个节点
final Node p = node.predecessor();
//p == head 说明当前节点是head的next
// tryAcquire 竞争锁资源,成功:true,失败:false
if (p == head && tryAcquire(arg)) {
// 进来说明拿到锁资源成功
// 将当前节点置位head,thread和prev属性置位null
setHead(node);
p.next = null; // help GC
// 设置获取锁资源成功
failed = false;
// 不管线程中断。
return interrupted;
}
// 如果不是或者获取锁资源失败,尝试将线程挂起
// shouldParkAfterFailedAcquire 当前节点的上一个节点的状态正常!
// parkAndCheckInterrupt 挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4.1.2.4 enq()
private Node enq(final Node node) {
for (;;) {
// 拿到tail
Node t = tail;
if (t == null) { // 如果tail为null,说明当前没有Node在队列中
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 以CAS的方式,将当前节点变为tail节点
if (compareAndSetTail(t, node)) {
// 之前的tail的next指向当前节点
t.next = node;
return t;
}
}
}
}
4.1.2.5 shouldParkAfterFailedAcquire()
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//拿到上一个节点的状态
int ws = pred.waitStatus;
// 如果上一个节点为 -1
if (ws == Node.SIGNAL)
// 返回true,挂起线程
return true;
if (ws > 0) { // 如果上一个节点是取消状态
// 循环往前找,找到一个状态小于等于0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将小于等于0的节点状态该为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
4.2 公平锁、非公平锁流程
经过上面的源码解读,我们基本上对ReentrantLock的公平锁、非公平锁有了一个而大概的了解,为了使我们理解更深刻,我们这里总结一下他们的加锁流程。
公平锁调用流程:
非公平锁调用流程:
公平锁、非公平锁整体加锁流程:
五、ReentrantLock 释放锁实现
5.1 释放锁源码
5.1.1 release()
public final boolean release(int arg) {
// 核心的释放锁资源方法
if (tryRelease(arg)) {
Node h = head;
// 释放锁资源释放干净了。 (state == 0)
if (h != null && h.waitStatus != 0)
// 唤醒线程
unparkSuccessor(h);
return true;
}
// 释放锁成功,但是state != 0
return false;
}
5.1.2 unparkSuccessor()
// 唤醒节点
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
// 如果头节点状态小于0,换为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿到当前节点的next
Node s = node.next;
// 如果s == null ,或者s的状态为1
if (s == null || s.waitStatus > 0) {
// next节点不需要唤醒,需要唤醒next的next
s = null;
// 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 经过循环的获取,如果拿到状态正常的节点,并且不为null
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
为什么唤醒线程时,为啥从尾部往前找,而不是从前往后找?
因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前Node,最后才是能上一个节点的next指针,指向当前Node。
如果从前往后,通过next去找,可能会丢失某个节点,导致这个节点不会被唤醒~
如果从后往前找,肯定可以找到全部的节点。
5.2 释放锁流程
下面用一张图说明一下释放锁的流程:
今天ReentrantLock的源码实现和原理介绍的相关内容就分享到这里,如果帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!