文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

干货,深入剖析ReentrantLock源码,推荐收藏

2024-12-01 12:38

关注

前两篇文章,我们分析了AQS的加锁流程、以及源码实现。当时我们就说了,AQS使用了模板设计模式,父类中定义加锁流程,子类去实现具体的加锁逻辑。所以大部分加锁代码已经在父类AQS中实现了,导致ReentrantLock的源码非常简单,一块学习一下。

先看一下ReentrantLock怎么使用?

1. ReentrantLock的使用


public class ReentrantLockDemo {

public static void main(String[] args){
// 1. 创建ReentrantLock对象
ReentrantLock lock = new ReentrantLock();
// 2. 加锁
lock.lock();
try {
// 3. 这里执行具体的业务逻辑
} finally {
// 4. 释放锁
lock.unlock();
}
}
}

可以看到ReentrantLock的使用非常简单,调用lock加锁,unlock释放锁,需要配置try/finally使用,保证在代码执行出错的时候也能释放锁。

ReentrantLock也可以配合Condition条件使用,具体可以翻一下前几篇文章中BlockingQueue的源码解析,那里面有ReentrantLock的实际使用。

再看一下ReentrantLock的类结构

2. ReentrantLock类结构

// 实现Lock接口
public class ReentrantLock implements Lock {

// 只有一个Sync同步变量
private final Sync sync;

// Sync继承自AQS,主要逻辑都在这里面
abstract static class Sync extends AbstractQueuedSynchronizer {
}

// Sync的两个子类,分别实现了公平锁和非公平锁
static final class FairSync extends Sync {
}
static final class NonfairSync extends Sync {
}

}

可以看出ReentrantLock的类结构非常简单,实现了Lock接口。

类里面有两个静态内部类,分别实现公平锁和非公平锁。

看一下Lock接口中,定义了哪些方法?

public interface Lock {

// 加锁
void lock();

// 加可中断的锁
void lockInterruptibly() throws InterruptedException;

// 尝试加锁
boolean tryLock();

// 一段时间内,尝试加锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

// 释放锁
void unlock();

// 新建条件状态
Condition newCondition();
}

就是一些使用锁的常用方法。

在上篇文章中浏览AQS源码的时候,了解到AQS定义了一些有关具体加锁、释放锁的抽象方法,留给子类去实现,再看一下有哪些抽象方法:

// 加独占锁
protected boolean tryAcquire(int arg){
throw new UnsupportedOperationException();
}
// 释放独占锁
protected boolean tryRelease(int arg){
throw new UnsupportedOperationException();
}

// 加共享锁
protected int tryAcquireShared(int arg){
throw new UnsupportedOperationException();
}
// 释放共享锁
protected boolean tryReleaseShared(int arg){
throw new UnsupportedOperationException();
}

// 判断是否是当前线程正在持有锁
protected boolean isHeldExclusively(){
throw new UnsupportedOperationException();
}

由于ReentrantLock使用的是独占锁,所以只需要实现独占锁相关的方法就可以了。

3.ReentrantLock源码解析

3.1 ReentrantLock构造方法

// 默认的构造方法,使用非公平锁
public ReentrantLock(){
sync = new NonfairSync();
}

// 传true,可以指定使用公平锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
}

在创建ReentrantLock对象的时候,可以指定使用公平锁还是非公平锁,默认使用非公平锁,显然非公平锁的性能更好。

先思考一个面试常考问题,公平锁和非公平锁是怎么实现的?

3.2 非公平锁源码

先看一下加锁源码:

从父类ReentrantLock的加锁方法入口:

public class ReentrantLock implements Lock {
// 加锁入口方法
public void lock(){
// 调用Sync中加锁方法
sync.lock();
}
}

在子类NonfairSync的加锁方法:

// 非公平锁
static final class NonfairSync extends Sync {

// 加锁
final void lock(){
// 1. 先尝试加锁(使用CAS设置state=1
if (compareAndSetState(0, 1))
// 2. 加锁成功,就把当前线程设置为持有锁线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 3. 没加锁成功,再调用父类AQS中实际的加锁逻辑
acquire(1);
}
}

加锁逻辑也很简单,先尝试使用CAS加锁(也就是把state从0设置成1),加锁成功,就把当前线程设置为持有锁线程。

设计者很聪明,在锁竞争不激烈的情况下,很大概率可以加锁成功,也就不用走else中复杂的加锁逻辑了。

如果没有加锁成功,还是需要走else中调用父类AQS的acquire方法,而acquire又需要调用子类的tryAcquire方法。

调用链路就是下面这样:

根据调用链路,实际的加锁逻辑在Sync.nonfairTryAcquire方法里面。

abstract static class Sync extends AbstractQueuedSynchronizer {
// 非公平锁的最终加锁方法
final boolean nonfairTryAcquire(int acquires){
final Thread current = Thread.currentThread();
// 1. 获取同步状态
int c = getState();
// 2. state=0表示无锁,先尝试加锁(使用CAS设置state=1
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 3. 加锁成功,就把当前线程设置为持有锁线程
setExclusiveOwnerThread(current);
return true;
}
// 4. 如果当前线程已经持有锁,执行可重入的逻辑
} else if (current == getExclusiveOwnerThread()) {
// 5. 加锁次数+acquires
int nextc = c + acquires;
// 6. 超过tnt类型最大值,溢出了
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

再看一下释放锁的调用流程,公平锁和非公平锁流程是一样的,最终都是执行Sync.tryRelease方法:

abstract static class Sync extends AbstractQueuedSynchronizer {
// 释放锁
protected final boolean tryRelease(int releases){
// 1. 同步状态减去释放锁次数
int c = getState() - releases;
// 2. 校验当前线程不持有锁,就报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 3. 判断同步状态是否等于0,无锁后,就删除持有锁的线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}

再看一下公平锁的源码

3.3 公平锁源码

先看一下公平锁的加锁流程:

最终的加锁方法是FairSync.tryAcquire,看一下具体逻辑:

static final class FairSync extends Sync {

// 实现父类的加锁逻辑
protected final boolean tryAcquire(int acquires){
final Thread current = Thread.currentThread();
// 1. 获取同步状态
int c = getState();
// 2. state=0表示无锁,先尝试加锁(使用CAS设置state=1
if (c == 0) {
// 3. 判断当前线程是不是头节点的下一个节点(讲究先来后到)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// 4. 如果当前线程已经持有锁,执行可重入的逻辑
} else if (current == getExclusiveOwnerThread()) {
// 5. 加锁次数+acquires
int nextc = c + acquires;
// 6. 超过tnt类型最大值,溢出了
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

// 判断当前线程是不是头节点的下一个节点(讲究先来后到)
public final boolean hasQueuedPredecessors(){
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
}

公平锁的释放锁逻辑跟非公平锁一样,上面已经讲过。

4. 总结

看完了ReentrantLock的所有源码,是不是觉得ReentrantLock很简单。

由于加锁流程的编排工作已经在父类AQS中实现,子类只需要实现具体的加锁逻辑即可。

加锁逻辑也很简单,也就是修改同步状态state的值和持有锁的线程exclusiveOwnerThread。

来源:一灯架构内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯