详解ReentrantLock---可重入锁(小白易懂)
详解ReentrantLock(小白易懂)
初识ReentrantLock
ReentrantLock是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(具有更多的方法)。
ReentrantLock底层基于AbstractQueuedSynchronizer实现
AbstractQueuedSynchronizer抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,
用大白话来说,AbstractQueuedSynchronizer为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。
ReentrantLock结构组成
学任何知识的第一件事,就是看清它的全貌,梳理出整体结构与主流程,之后逐个击破,所以带读者们先看下ReentrantLock整体结构组成,
对它的实现有个大致的了解。
上图可以看出来,ReentrantLock整体结构还是非常简单,给读者们分析一波,为什么ReentrantLock结构是这样设计的,
首先ReentrantLock实现了Lock接口,Lock接口是Java中对锁操作行为的统一规范,遵守规则规范是守法公民的基本素养,合情合理,Lock接口的定义如下:
public interface Lock {
/**
* 获取锁
*/
void lock( );
/**
* 获取锁-响应中断
*/
void lockInterruptibly() throws InterruptedException;
/**
* 返回获取锁是否成功状态
*/
boolean tryLock():
/**
* 返回获取锁是否成功状态-响应中断
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 创建条件变量
*/
Condition newCondition():
}
Lock接口定义的函数不多,接下来ReentrantLock要去实现这些函数,遵循着解耦可扩展设计,ReentrantLock内部定义了专门的组件Sync,
Sync继承AbstractQueuedSynchronizer提供释放资源的实现,NonfairSync和FairSync是基于Sync扩展的子类,
即ReentrantLock的非公平模式与公平模式,它们作为Lock接口功能的基本实现。
大白话来说,企业的老板,为了响应政府的政策,需要对企业内部做调整,但是政府每年政策都不一样,每次都要自己去亲力亲为,索性长痛不如短痛,
专门成立一个政策应对部门,以后这些事情都交予这个部门去做,老板只需要指挥它们就好了。
清楚了ReentrantLock结构组成之后,下面我只需对Sync、NonfairSync、FairSync逐个击破,ReentrantLock自然水到渠成。
小贴士:在ReentrantLock中,它对AbstractQueuedSynchronizer的state状态值定义为线程获取该锁的重入次数,
state状态值为0表示当前没有被任何线程持有,state状态值为1表示被其他线程持有,因为支持可重入,如果是持有锁的线程,再次获取同一把锁,
直接成功,并且state状态值+1,线程释放锁state状态值-1,同理重入多次锁的线程,需要释放相应的次数。
Sync
Sync可以说是ReentrantLock的亲儿子,它寄托了全村的希望,完美的继承了AbstractQueuedSynchronizer,是ReentrantLock的核心,
后面的NonfairSync与FairSync都是基于Sync扩展出来的子类。以下是Sync类定义的核心部分:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialversionUID = -5179523762034025860L;
//获取锁-子类实现
abstract void lock();
//非公平-获取资源
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)) {
//cas成功,设置当前持有锁的线程
setExclusiveOwnerThread(current);
//返回成功
return true;
}
}
else if (current== getExclusiveOwnerThread()){
//如果state!=0,但是当前线程是持有锁线程,直接重入
//state状态+1
int nextc = c + acquires;
if (nextc < 0)// overflow
throw new Error("Maximum lock count exceeded");
//设置state状态,此处不需要cas,因为持有锁的线程只有一个
setState(nextc);
//返回成功
return true;
}
//返回失败
return false;
}
/**
* 释放资源
*/
protected final boolean tryRelease(int releases) {
//state状s-releases,releases传入的是1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
//如果当前线程不是持有锁线程,抛出异常
throw new IllegalMonitorstateException();
//设置返回状态,默认为失败
boolean free = false;
if (c == 0) {
//state-1后,如果c==0代表释放资源成功
//返回状态设置为true
free = true;
//清空持有锁线程
setExclusiveOwnerThread(null);
//如果state-1后,state还是>0,代表当前线程有锁重入操作,需更做相应的释放次数,设置state值
setState(c):
return free;
}
}
我发现Sync有点偏心,首先Sync实现释放资源的细节(AQS留给子类实现的tryRelease),然后声明了获取锁的抽象函数(lock),子类根据业务实现,
目前看来还是很公平,但是Sync还定义了一个nonfairTryAcquire函数,这个函数是专门给NonfairSync使用的,FairSync却没有这种待遇,所以说Sync偏心。
Sync逻辑都比较简单,实现了AQS类的释放资源(tryRelease),然后抽象了一个获取锁的函数让子类自行实现(lock),
再加一个偏心的函数nonfairTryAcquire,但是再怎么简单,图还是要有的,这是我读者们的福利。
下面放一张tryRelease流程图,在后续的NonfairSync、FairSync都会有全面的流程。
NonfairSync
现在我们把视线转移到NonfairSync,在ReentrantLock中支持两种获取锁的策略,分别是非公平策略与公平策略,NonfairSync就是非公平策略。
此时会有疑问,什么是非公平策略?
在说非公平策略前,先简单的说下AQS(AbstractQueuedSynchronizer)流程,AQS为加锁和解锁过程提供了统一的模板函数,加锁与解锁的模板流程是,
获取锁失败的线程,会进入CLH队列阻塞,其他线程解锁会唤醒CLH队列线程,如下图所示(简化流程)
上图中,线程释放锁时,会唤醒CLH
队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH
队列的线程参与竞争,所以非公平就体现在这里,非CLH
队列线程与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
//cas设置state为1失败,代表获取资源失败,执行AQS获取锁模板流程,否获取资源成功
acquire(1);
}
//获取资源-使用的是Sync提供的nonfairTryAcquire函数
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//AQS获取锁模板函数,这是AQS类中的函数
public final void acquire(int arg) {
//我们只需要关注tryAcquire函数,后面的函数是AQS获取资源失败,线程节点进入CLH队列的细节流程,本文不关注
if(!tryAcquire(arg) && acquireQueued(addwaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
NonfairSync
继承Sync
实现了lock
函数,lock
函数也非常简单,CAS
设置状态值state
为1代表获取锁成功,否则执行AQS
的acquire
函数(获取锁模板),另外NonfairSync
还实现了AQS
留给子类实现的tryAcquire
函数(获取资源),这个被Sync
宠幸的幸运儿,直接使用Sync
提供的nonfairTryAcquire
函数来实现tryAcquire
,最后子类实现的tryAcquire
函数在AQS的acquire
函数中被使用。
首先AQS
的acquire
函数是获取锁的流程模板,模板流程会先执行tryAcquire
函数获取资源,tryAcquire
函数要子类实现,NonfairSync
作为子类,实现了tryAcquire
函数,具体实现是调用了Sync
的nonfairTryAcquire
函数。
接下来,我们再看看Sync
专门给NonfairSync
准备的nonfairTryAcquire
函数逻辑
/**
* 非公平-获取资源
*/
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)) {
//cas成功,设置当前持有锁的线程
setExclusiveOwnerThread(current);
//返回成功
return true;
}
}
//如果state!=0,但是当前线程是持有锁线程,直接重入
else if (current == getExclusiveOwnerThread()) {
//state状态+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置state状态,此处不需要cas,因为持有锁的线程只有一个
setstate(nextc);
//返回成功
return true;
}
//返回失败
return false;
}
对上述代码逻辑做个简单的概括,当前线程查看资源是否可获取:
可获取,尝试使用CAS设置state为1,CAS成功代表获取资源成功,否则获取资源失败
不可获取,判断当线程是不是持有锁的线程,如果是,state重入计数,获取资源成功,否则获取资源失败
以下是nonfairTryAcquire流程图
FairSync
有非公平策略,就有公平策略,FairSync
就是ReentrantLock
的公平策略。
所谓公平策略就是,严格按照CLH
队列顺序获取锁,线程释放锁时,会唤醒CLH
队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH
队列的线程参与竞争,为了保证公平,一定会让CLH
队列线程竞争成功,如果非CLH
队列线程一直占用时间片,那就一直失败(构建成节点插入到CLH
队尾,由AQS
模板流程执行),直到时间片轮到CLH
队列线程为止,所以公平策略的性能会更差。
了解了什么是公平策略,我们再来看看FairSync
类定义
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//获取锁
final void lock() {
//cas设置state为1失败,代表获取资源失败,执行AQs获取锁模板流程,否获取资源成功
acquire(1);
}
//获取资源
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state状态
int c = getstate();
if (c == 0){ // state==0 代表资源可获取
//1.hasqueuedPredecessors判断当前线程是不是CLH队列被唤醒的线程,如果是执行下一个步歌
//2.cas设置state为acquires,acquires传入的是1
if (!hasQueuedPredecessors() && compareAndSetstate(o, acquires)) {
//cas成功,设置当前持有锁的线程
setExclusiveOwnerThread(current);
//返回成功
return true;
}
}
else if (current== getExclusiveOwnerThread()){
//如果state!=0,但是当前线程是持有锁线程,直接重入
//state状态+1
int nextc = c + acquires;
if (nextc < 0)// overflow
throw new Error("Maximum lock count exceeded");
//设置state状态,此处不需要cas,因为持有锁的线程只有一个
setState(nextc);
//返回成功
return true;
}
//返回失败
return false;
}
}
//AQS获取锁模板函数,这是AQS类中的函数
public final void acquire(int arg) {
//我们只需要关注tryAcquire函数,后面的函数是AQS获取资源失败,线程节点进入CLH队列的细节流程,本文不关注
if(!tryAcquire(arg) && acquireQueued(addwaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
其实我们不难发现FairSync
流程与NonfairSync
基本一致,唯一的区别就是在CAS
执行前,多了一步hasQueuedPredecessors
函数,这一步就是判断当前线程是不是CLH
队列被唤醒的线程,如果是就执行CAS
,否则获取资源失败,下面是流程图
Lock的实现
最后看看ReentrantLock
中是如何实现Lock
的,先看构造器部分
//同步器
private final Sync sync;
//默认使用非公平策略
public ReentrantLock() {
sync = new NonfairSync();
}
//true-公平策略 false非公平策略
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock
默认是使用非公平策略,如果想指定模式,可以通过入参fair
来选择,这里就不做过多概述,接下来看看ReentrantLock
对Lock
的实现
public class ReentrantLock implements Lock, java,io,Serializable {
private static final long serialVersionUID = 7373984872572414699L;
//同步器
private final Sync sync;
//默认使用非公平策略
public ReentrantLock() {
sync = new Nonfairsync():
}
//true-公平策略 false非公平策路
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
//获取锁-阻塞
public void lock() {
//基于sync实现
sync.lock();
}
//获取锁-阻塞,支持响应线程中断
public void lockInterruptibly() throws InterruptedException {
//甚于sync实现
sync.acquireInterruptibly(1);
}
//获取资源,返回是否成功状态-非阻塞
public boolean tryLock() {
//基于sync实现
return sync.nonfairTryAcquire(1);
}
//获取锁-阻塞,支持超时
public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {
//基于sync实现
return sync.tryAcquireNanos(1. unit.toNanos(timeout));
}
//释放锁
public void unlock() {
//基于sync实现
sync.release(1);
}
//创建条件变量
public Condition newCondition() {
//基于sync实现
return sync.newCondition();
}
}
ReentrantLock
对Lock
的实现都是基于Sync
来做的。Sync
承包了所有事情,为何它如此厉害,因为Sync
上有AbstractQueuedSynchronizer
老大哥罩着,下有NonfairSync
与FairSync
两小弟可差遣,所以成为ReentrantLock
的利器也合情合理。
感谢各位大哥的阅读