性能文章>干货,深入剖析ReentrantLock源码,推荐收藏>

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

1年前
200637

ReentrantLock和Synchronized都是Java开发中最常用的锁,与Synchronized这种JVM内置锁不同的是,ReentrantLock提供了更丰富的语义。可以创建公平锁或非公平锁、响应中断、超时等待、按条件唤醒等。在某些场景下,使用ReentrantLock更适合,功能更强大。
前两篇文章,我们分析了AQS的加锁流程、以及源码实现。当时我们就说了,AQS使用了模板设计模式,父类中定义加锁流程,子类去实现具体的加锁逻辑。所以大部分加锁代码已经在父类AQS中实现了,导致ReentrantLock的源码非常简单,一块学习一下。
先看一下ReentrantLock怎么使用?

1. ReentrantLock的使用


/**
 * @author 一灯架构
 * @apiNote 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方法。
调用链路就是下面这样:
image1.png
根据调用链路,实际的加锁逻辑在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方法:

image2.png

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 公平锁源码

先看一下公平锁的加锁流程:
image1.png
最终的加锁方法是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。

我是「一灯架构」,如果本文对你有帮助,欢迎各位小伙伴点赞、评论和关注,感谢各位老铁,我们下期见

点赞收藏
一灯架构

只分享有趣的技术干货

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

万字完整深入解析JVM面试必备,原来这就是和年薪百万的差距

万字完整深入解析JVM面试必备,原来这就是和年薪百万的差距

搭稳Netty开发的地基,用漫画帮你分清同步异步阻塞非阻塞

搭稳Netty开发的地基,用漫画帮你分清同步异步阻塞非阻塞

为什么 JVM 叫做基于栈的 RISC 虚拟机

为什么 JVM 叫做基于栈的 RISC 虚拟机

如何随心所欲调试HotSpot VM源代码?(改造为CMakeLists项目)

如何随心所欲调试HotSpot VM源代码?(改造为CMakeLists项目)

朋友们,就在今天,JDK 21,它终于带着重磅新特性正式发布了!!!

朋友们,就在今天,JDK 21,它终于带着重磅新特性正式发布了!!!

13 轻量级锁的重入 以及 线程1获取轻量级锁并释放线程2获取锁 的调试

13 轻量级锁的重入 以及 线程1获取轻量级锁并释放线程2获取锁 的调试

7
3