目录
一、概述
二、ReentrantLock的整体结构
三、ReentrantLock 和Synchronized相比
四、ReentrantLock 公平锁和非公平锁实现
4.1 ReentrantLock 源码解读
4.1.1 ReentrantLock 类源码解读
4.1.1.1 Lock接口
4.1.1.2 Sync抽象类
4.1.1.3 NonfairSync()和FairSync()
4.1.1.3.1 NonfairSync介绍
4.1.1.3.2 FairSync介绍
4.1.2 ReentrantLock涉及的AQS方法源码
4.1.2.1 acquire()
4.1.2.2 addWaite()
4.1.2.3 acquireQueued()
4.1.2.4 enq()
4.1.2.5 shouldParkAfterFailedAcquire()
4.2 公平锁、非公平锁流程
五、ReentrantLock 释放锁实现
5.1 释放锁源码
5.1.1 release()
5.1.2 unparkSuccessor()
5.2 释放锁流程
一、概述
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的源码实现和原理介绍的相关内容就分享到这里,如果帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!