目录

  • ReentrantLock 简介
  • ReentrantLock 使用示例
  • ReentrantLock 与 synchronized 的区别
  • ReentrantLock 实现原理
  • ReentrantLock 源码解析

ReentrantLock 简介

ReentrantLock 是 JDK 提供的一个可重入的独占锁,

  • 独占锁:同一时间只有一个线程可以持有锁
  • 可重入:持有锁的线程可重复加锁,意味着第一次成功加锁时,需要记录锁的持有者为当前线程,后续再次加锁时,可以识别当前线程。

ReentrantLock 提供了公平锁以及非公平锁两种模式,要解释这两种模式不异同,得先了解一下 ReentrantLock 的加锁流程,ReentrantLock 基于 AQS 同步器实现加解锁,基本的实现流程为:

线程 A、B、C 同时执行加锁,加锁是通过CAS操作完成,CAS 是原子操作,可以保证同一时间只有一个线程加锁成功,假设线程 A 加锁成功,则线程 B、C 进入 AQS 等待队列并被挂起,假设 B 在前,C 在后,当线程 A 释放锁时,会唤醒排在等待队列队首的线程 B,该线程会尝试通过 CAS 进行获取锁。如果线程 B 尝试加锁的同时,有线程 D 也同时进行加锁,如果线程 D 与 线程 B 竞争加锁,则为非公平锁,线程 D 加入等待队列排在线程 C 之后,则为公平锁。

  • 非公平锁:加锁时会与等待队列中的头节点进行竞争。
  • 公平锁:加锁时首先判断等待队列中是否有线程在排队,如果没有则参与竞争锁,如果有则排队等待。

所谓公平就是,大家一起到,就竞争上岗,如果已经有人在排队了,那就先来后到。

使用示例

使用伪代码表示

class X {    private final ReentrantLock lock = new ReentrantLock();    public void m() {        lock.lock();  // block until condition holds        try {            // ... method body        } finally {            lock.unlock();        }    }}

默认实现的是非公平锁,如果要使用公平锁,只需要创建 ReentrantLock 对象时传递入参 true 即可,使用方法与非公平锁一样。

private final ReentrantLock lock = new ReentrantLock(true);

condition 使用示例:

class X {    private final ReentrantLock lock = new ReentrantLock();    private final Condition condition = lock.newCondition();    public void poll() {        lock.lock();  // block until condition holds        try {            while(条件判断表达式) {                condition.wait();            }        } finally {            lock.unlock();        }    }        public void push() {        condition.signal();    }}

ReentrantLock 与 synchronized 的区别

ReentrantLock 提供了 synchronized 类似的功能和内存语义。

相同点

  • ReentrantLock 与 synchronized 都是独占锁,可以让程序正确同步。
  • ReentrantLock 与 synchronized 都可重入锁,可以在循环中使用 synchronized 进行加锁并不用担心解锁问题,但 ReentrantLock 则必须要进行与加锁相同次数的解锁操作,不然可能导致没有真正解锁成功。

不同点

  • synchronized 是JDK提供的语法,加锁解锁的过程是隐式的,用户不用手动操作,操作简单,但不够灵活。
  • ReentrantLock 需要手动加锁解锁,且解锁次数必须与加锁次数一样,才能保证正确释放锁,操作较为复杂,但是因为是手动操作,所以可以应付复杂的并发场景。
  • ReentrantLock 可以实现公平锁
  • ReentrantLock 可以响应中断,使用 lockInterruptibly 方法进行加锁,可以在加锁过程中响应中断,synchronized 不能响应中断
  • ReentrantLock 可以实现快速失败,使用 tryLock 方法进行加锁,如果不能加锁成功,会立即返回 false,而 synchronized 是阻塞式。
  • ReentrantLock 可以结合 Condition 实现条件机制。

可以看到,ReentrantLock 与 synchronized 都是实现线程同步加锁,但 ReentrantLock 比起 synchronized 要灵活很多。

实现原理

ReentrantLock 使用组合的方式,通过继承 AQS 同步器实现线程同步。通过控制 AQS 同步器的同步状态 state 达到加锁解锁的效果,该状态默认为 0,代表锁未被占用,加锁则是通过 cas 操作将其设置为 1,cas 是原子性操作,可以保证同一时间只有一个线程可以加锁成功,同一个线程可以重复加锁,每次加锁同步状态自增 1,释放锁的过程就是将同步状态自减,减到 0 时才算完全释放,这也解释了为什么释放锁的次数必须与加锁次数一样的问题,因为只有次数一样才能将同步状态减至 0,这样其它线程才能进行加锁。

源码分析

Lock 接口

ReentrantLock 实现了 Lock 接口,这是 JDK 提供的所有 JVM 锁的基类。

public interface Lock {    // 阻塞式加锁    void lock();    // 阻塞式加锁,但可以响应中断,加锁过程中线程中断,抛出 InterruptedException 异常    void lockInterruptibly() throws InterruptedException;    // 快速失败加锁,只尝试一次,    boolean tryLock();    // 阻塞式加锁,可以响应中断并且实现超时失败    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    // 释放锁    void unlock();    // 实现条件    Condition newCondition();}

通过代码可以看到,ReentrantLock 的内部实现都是通过 Sync 这个类实现,可以认为遵守组合设计原则,Sync 是 ReentrantLock 的内部类。这里的方法调用,并没有区分是公平锁还是非公平锁,而是无差别地调用,所以区别一定在 Sync 这个类的实现中。

public class ReentrantLock implements Lock, java.io.Serializable {    private final Sync sync;    public void lock() {        sync.lock();    }    public void lockInterruptibly() throws InterruptedException {        sync.acquireInterruptibly(1);    }    public boolean tryLock() {        return sync.nonfairTryAcquire(1);    }    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {        return sync.tryAcquireNanos(1, unit.toNanos(timeout));    }    public void unlock() {        sync.release(1);    }    public Condition newCondition() {        return sync.newCondition();    }}

Sync 类继承了 AQS 同步器,通过同步器实现线程同步,因为是独占锁,所以最重要的就是实现 tryAcquire 与 tryRelease 两个方法,Sync 类是一个 abstract 类,它拥有两个实现类 FairSync 与 NonfairSync,通过名字应该就可分辨他们就是公平锁与非公平锁。

abstract static class Sync extends AbstractQueuedSynchronizer {    abstract void lock();    // 非公平加锁    final boolean nonfairTryAcquire(int acquires) {        final Thread current = Thread.currentThread();        int c = getState();        if (c == 0) { // 如果没有线程执行锁            if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁                setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程                return true;            }        } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁            int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1            if (nextc < 0) // overflow                throw new Error("Maximum lock count exceeded");            setState(nextc);            return true;        }        return false;    }    // 释放锁    protected final boolean tryRelease(int releases) {        int c = getState() - releases; // 将同步状态进行自减,acquires 的传值为 1        if (Thread.currentThread() != getExclusiveOwnerThread())            throw new IllegalMonitorStateException();        boolean free = false;        if (c == 0) { // 当同步状态减成 0 时,代表完全释放锁,将锁的拥有者置空            free = true;            setExclusiveOwnerThread(null);        }        setState(c);        return free;    }    // ...省略其它...}

所以公平锁与非公平锁的玄机就在 ReentrantLock 的构造方法中,默认的无参构造方法创建非公平锁,如果传参 true,则创建公平锁。而这两个锁都是 Sync 的子类,使用了不同的实现策略,可以认为使用了策略模式。

public class ReentrantLock implements Lock, java.io.Serializable {    // 默认创建非公平锁    public ReentrantLock() {        sync = new NonfairSync();    }    // 如果为 true,则创建公平锁    public ReentrantLock(boolean fair) {        sync = fair ? new FairSync() : new NonfairSync();    }}

接下来分别看一下 FairSync 与 NonfairSync 是如何实现公平锁与非公平锁的,首先分析非公平锁

static final class NonfairSync extends Sync {    // 阻塞式加锁    final void lock() {        // 首先尝试竞争加锁,如果成功则设置当前线程为锁的拥有者        if (compareAndSetState(0, 1))             setExclusiveOwnerThread(Thread.currentThread());        else            acquire(1); // 使用 AQS 排队    }    // 尝试加锁    protected final boolean tryAcquire(int acquires) {        return nonfairTryAcquire(acquires);    }}

阻塞式加锁调用 ReentrantLock 的 lock() 方法,该方法调用 sync.lock() 执行加锁,非公平锁也就是调用 NonfairSync 类的 lock 方法,该方法首先尝试竞争加锁,此时有三种情况:

  • 此时锁没有人持有,竞争成功,直接设置当前线程为锁的拥有者并返回
  • 此时锁没有人持有,竞争失败,走 AQS 加锁流程
  • 此时锁被其它线程拥有,走 AQS 加锁流程
  • 此时锁被自己拥有,竞争失败,走 AQS 加锁流程

AQS 加锁流程就是调用 tryAcquire 方法尝试加锁,如果成功则返回加锁成功,如果失败则进入等待队列并挂起,等待锁的持有者释放锁时唤醒等待队列中的线程,并再次尝试加锁,如此反复,直到加锁成功。

public final void acquire(int arg) {    if (!tryAcquire(arg) &&        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        selfInterrupt();}

AQS 加锁流程在 AQS 中已经提供了完整实现模板,不需要去了解底层就可以使用,需要做的就是自行实现 tryAcquire 方法,NonfairSync 的 tryAcquire 方法这里再贴一次实现代码。

final boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) { // 如果没有线程执行锁        if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁            setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程            return true;        }    } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁        int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1        if (nextc < 0) // overflow            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    return false;}

接下来看一下 FairSync 的实现

static final class FairSync extends Sync {    final void lock() {        acquire(1);    }    protected final boolean tryAcquire(int acquires) {        final Thread current = Thread.currentThread();        int c = getState();        if (c == 0) { // 如果锁没有人持有            // 首先判断队列是否为空,如果为空则竞争锁,如果不为空则返回尝试失败,线程会被加入等待队列            if (!hasQueuedPredecessors() &&                compareAndSetState(0, acquires)) {                setExclusiveOwnerThread(current);                return true;            }        } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程持有,与非公平锁同样处理            int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1            if (nextc < 0)                throw new Error("Maximum lock count exceeded");            setState(nextc);            return true;        }        return false;    }}

释放锁的过程,公平锁与非公平锁是一样的,前面的代码中已经解释过了,这里就不再多说了。

synchronized 和 reentrantlock 区别是什么_JUC源码系列之ReentrantLock源码解析相关推荐

  1. [darknet源码系列-2] darknet源码中的cfg解析

    [darknet源码系列-2] darknet源码中的cfg解析 FesianXu 20201118 at UESTC 前言 笔者在[1]一文中简单介绍了在darknet中常见的数据结构,本文继续上文 ...

  2. 【阅读源码系列】ConcurrentHashMap源码分析(JDK1.7和1.8)

    个人学习源码的思路: 使用ctrl+单机进入源码,并阅读源码的官方文档–>大致的了解一下此类的特点和功能 使用ALIT+7查看类中所有方法–>大致的看一下此类的属性和方法 找到重要方法并阅 ...

  3. Spring源码系列:BeanDefinition源码解析

    Bean的定义主要由BeanDefinition来描述的.作为Spring中用于包装Bean的数据结构,今天就来看看它的面纱下的真容吧. 首先就是BeanDefinition的类定义: public ...

  4. 【源码系列】Eureka源码分析

    对于服务注册中心.服务提供者.服务消费者这个三个主要元素来说,服务提供者和服务消费者(即Eureka客户端)在整个运行机制中是大部分通信行为的主动发起者(服务注册.续约.下线等),而注册中心主要是处理 ...

  5. [darknet源码系列-3] 在darknet中,如何根据解析出来的配置进行网络层构建

    [darknet源码系列-3] 在darknet中,如何根据解析出来的配置进行网络层构建 FesianXu 20201120 at UESTC 前言 笔者在[1,2]中已经对darknet如何进行配置 ...

  6. ReentrantLock 公平锁和非公平锁加锁和解锁源码分析(简述)

    - title: ReentrantLock 公平锁和非公平锁加锁和解锁源码分析(简述) - date: 2021/8/16 文章目录 一.ReentrantLock 1. 构造函数 二.Reentr ...

  7. 死磕 java同步系列之ReentrantReadWriteLock源码解析

    问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...

  8. SpringMVC源码系列:HandlerMapping

    SpringMVC源码系列:HandlerMapping SpringMVC源码系列:AbstractHandlerMapping HandlerMapping接口是用来查找Handler的.在Spr ...

  9. java多线程系列(四)---ReentrantLock的使用

    Lock的使用 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我的理 ...

最新文章

  1. Vue SPA 打包优化实践
  2. python用def编写calsum函数_Python函数
  3. SecureCRT录制的安卓电视切换台脚本
  4. 近视手术─医学界的一个阴谋? !
  5. C++ 学习之旅(1)——编译器Compiler
  6. java单例模式的实现方法_JAVA单例模式的几种实现方法
  7. 全民加速节:全站加速在互联网媒体应用上的最佳实践
  8. spring-in-action-mvc-jdbc搭建工程
  9. (61)UART外设驱动接收驱动(六)(第13天)
  10. 学习vue3系列computed
  11. pecamaker+corosync高可用集群的搭建
  12. python连接sql引用的第三方库_python连接sqlserver数据库操作
  13. Eclipse或MyEclipse—在Eclipse或MyEclipse中的操作(1)
  14. ping可以访问百度ip但不能访问百度域名|couldn't resolve host api.weixin.qq.com
  15. 典型信息化案例点评(2)
  16. 小米手机修改imei教程_小米手机imei码和s/n码以及测试调试界面唤起代码
  17. 推荐几个资源搜索网站
  18. 一篇文章教会你用Python抓取抖音App热点数据
  19. 披着“云”衣裳的狗——搜狗输入法“云”版本尝鲜记
  20. mac如何看html5视频播放器,适用于Mac的HTML5视频播放器

热门文章

  1. Redis 服务安装
  2. 一名3年工作经验的程序员应该具备的技能
  3. Redis 3.0.1 安装和配置
  4. ocp linux 基础要点
  5. Redis(1):简介
  6. Android界面菜单(4)—快捷菜单
  7. Silverlight 5 新特性
  8. C# 视频监控系列(11):H264播放器——封装API[HikPlayM4.dll]
  9. 回归分析预测_使用回归分析预测心脏病。
  10. 计算机科学与技术科研论文,计算机科学与技术学院2007年度科研论文一览表