从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁

源码,入手,详解,reentrantlock,一个,synchronized,强大,重入 · 浏览次数 : 53

小编点评

## ReentrantLock 详解 ReentrantLock 是 Java 中并发编程中非常重要的一个同步工具,它是一种 **独占式可重入锁**,这意味着它可以被多个线程在并发情况下获取,但始终只会被一个线程获取锁。 **ReentrantLock 的内部构造** ReentrantLock 的内部包含三个类: * `Sync`:用于公平锁。 * `FairSync`:用于可公平锁。 * `NonfairSync`:用于非公平锁。 **公平锁和非公平锁** * **公平锁**:按照申请锁的时间进行分配锁的顺序性,保证申请时间上的顺序性。但公平锁获取锁的时间会多一步调用 `hasQueuedPredecessors` 来判断当前线程对应的节点在等待队列中是否有前驱节点。 * **非公平锁**:锁的获取和释放按照申请锁的时间进行分配的,线程可以获取锁,但不能保证申请时间上的顺序性。 **ReentrantLock 的获取和释放** * 获取锁:`nonfairTryAcquire` 方法,它先获取锁的状态,若为 0 说明没有任何线程获取,此时直接获取即可。 * 释放锁:`tryRelease` 方法,它同步状态减 1,直至同步状态为 0,锁才被完全释放。 **总结** ReentrantLock 是一个非常实用的同步工具,它可以用于各种并发编程场景。它拥有独占式、可重入、可中断和公平/非公平等多种锁定模式,可以根据需要选择不同的模式。 **其他问题** * ReentrantLock 的使用我们通过一个小 demo 来感受一下基于非公平锁模式下的使用场景。 * ReentrantLock 是 Java 中并发编程中的一个非常重要的工具,它可以帮助我们编写更加可靠的代码。

正文

写在开头

随手一翻,发现对于Java中并发多线程的学习已经发布了十几篇博客了,多线程 是Java基础中的重中之重!因此,可能还需要十几篇博客才能大致的讲完这部分的知识点,初学者对于这部分内容一定要多花心思,不可马虎!今天我们继续来学习一个重要知识点:ReentrantLock

ReentrantLock :是一种独占式的可重入锁,位于java.util.concurrent.locks中,是Lock接口的默认实现类,底部的同步特性基于AQS实现,和synchronized关键字类似,但更灵活、功能更强大、也是目前实战中使用频率非常高的同步类。

几种不同锁的定义

在学习ReentrantLock之前,我们先来复习一下如下的几类锁的定义,这个其实很早的博文中就已经详细的整理过了,这里为了更好理解ReentrantLock锁,还是简单罗列一下。

独占锁与共享锁

  1. 独占锁:同一时间,一把锁只能被一个线程获取;
  2. 共享锁:同意时间,一把锁可以被多个线程获取。

公平锁与非公平锁

  1. 公平锁:按照申请锁的时间先后,进行锁的再分配工作,这种锁往往性能稍差,因为要保证申请时间上的顺序性;
  2. 非公平锁: 锁被释放后,后续线程获得锁的可能性随机,或者按照设置的优先级进行抢占式获取锁。

可重入锁

所谓可重入锁就是一个线程在获取到了一个对象锁后,线程内部再次获取该锁,依旧可以获得,即便持有的锁还没释放,仍然可以获得,不可重入锁这种情况下会发生死锁!

可重入锁在使用时需要注意的是:由于锁会被获取 n 次,那么只有锁在被释放同样的 n 次之后,该锁才算是完全释放成功。

可中断锁与不可中断锁

  1. 可中断锁:在获取锁的过程中可以中断获取,不需要非得等到获取锁后再去执行其他逻辑;
  2. 不可中断锁:一旦线程申请了锁,就必须等待获取锁后方能执行其他的逻辑处理。

ReentrantLock是一种同时拥有独占式、可重入、可中断、公平/非公平特性的同步器!


ReentrantLock

根据上面总结出的特点,我们从底层源码出发来验证一下结论的准确性,首先我们通过一个关系图谱来大致梳理一下ReentrantLock的内部构造。

image

ReentrantLock实现了Lock和Serializable接口:

public class ReentrantLock implements Lock, java.io.Serializable {}

其内部拥有三个内部类,分别为Sync、FairSync、NonfariSync,其中FairSync、NonfariSync继承父类Sync。Sync又继承了AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。

问题1:ReentrantLock内部公平锁与非公平锁如何实现?

在内部通过构造器来实现公平锁与非公平锁的设置,默认为非公平锁,同样可以通过传参设置为公平锁。底层实现其实是通过FairSync、NonfariSync这个两个内部类,源码如下:

//无参构造,默认为非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

问题2:独占锁如何实现?

在源码中无论是Sync这个内部类或是其子类,都会调用setExclusiveOwnerThread(current)这个方法,这个方法是AQS的父类AOS(AbstractOwnableSynchronizer)中的方法,用以标记锁的持有者为独占模式。

image

问题3:ReentrantLock如何获取和释放锁?

由于ReentrantLock是默认非公平锁,所以我们就以非公平模式为例去看一下它底层如何实现锁的获取与释放的。

1️⃣ 锁的获取

核心方法为Sync内部类的nonfairTryAcquire方法,如下为其源码,先获取当前锁的状态,若为0说明没有被任何线程获取,此时直接获取即可;另外一种state不为0时,则需要判断占有线程是否为当前线程,若是则可以获取,并将state值加一返回,否则获取失败。

【注意】:公平模式下获取锁的时会多一步调用hasQueuedPredecessors 的逻辑判断,用以判断当前线程对应的节点在等待队列中是否有前驱节点,毕竟公平锁的竞争严格按照获取锁的时间进行分配的。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果该锁未被任何线程占有,该锁能被当前线程获取
	if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
	//2.若被占有,检查占有线程是否是当前线程
    else if (current == getExclusiveOwnerThread()) {
		// 3. 再次获取,计数加一
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

2️⃣ 锁的释放

对应的以非公平锁中释放为例,通过源码我们可以看到,每调用一次则同步状态减1,直至同步状态为0,锁才被完全的释放完,否则返回false。

protected final boolean tryRelease(int releases) {
	//1. 同步状态减1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
		//2. 只有当同步状态为0时,锁成功被释放,返回true
        free = true;
        setExclusiveOwnerThread(null);
    }
	// 3. 锁未被完全释放,返回false
    setState(c);
    return free;
}

3️⃣ 小总结

经过上面源码的学习,我们已经能够确认一点就是:ReentrantLock是一种同时拥有独占式、可重入、可中断、公平/非公平特性的同步器!我们接下来就继续再来学习一下它的使用。

问题4:ReentrantLock的使用

我们通过一个小demo,来感受一下基于非公平锁模式下的ReentrantLock的使用哈

public class Test {
    //初始化一个静态lock对象
    private static final ReentrantLock lock = new ReentrantLock();
    //初始化计算量值
    private static int count;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("result:"+count);
    }
}

上面这个程序预期输出结果为:2000,thread1和thread2分别做了加1000次的操作,由于ReentrantLock是独占式可重入锁,故最终可以成功打印出预期结果!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

image

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

image

与从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁相似的内容:

从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁

写在开头 随手一翻,发现对于Java中并发多线程的学习已经发布了十几篇博客了,多线程 是Java基础中的重中之重!因此,可能还需要十几篇博客才能大致的讲完这部分的知识点,初学者对于这部分内容一定要多花心思,不可马虎!今天我们继续来学习一个重要知识点:ReentrantLock ReentrantLo

源码级深度理解 Java SPI

本文从源码入手分析,深入探讨 Java SPI 的特性、原理,以及在一些比较经典领域的应用。

redis 源码分析:Jedis 哨兵模式连接原理

1. 可以从单元测试开始入手 查看类JedisSentinelPool private static final String MASTER_NAME = "mymaster"; protected static final HostAndPort sentinel1 = HostAndPorts.

containerd 源码分析:启动注册流程

0. 前言 containerd 是一个行业标准的容器运行时,其强调简单性、健壮性和可移植性。本文将从 containerd 的代码结构入手,查看 containerd 的启动注册流程。 1. 启动注册流程 1.1 containerd 首先以调试模式运行 containerd: // contai

深入浅出 OkHttp 源码解析及应用实践

OkHttp 在 Java 和 android 世界中被广泛使用,深入学习源代码有助于掌握软件特性和提到编程水平。本文首先从源代码入手简要分析了一个请求发起过程中的核心代码,接着通过流程图和架构图概括地介绍了OkHttp的整体结构,重点分析了拦截器的责任链模式设计,最后列举了OkHttp拦截器在项目中的实际应用。

Kafka多维度调优

优化金字塔 应用程序层面 框架层面(Broker层面) JVM层面 操作系统层面 应用程序层面:应当优化业务代码合理使用kafka,合理规划主题,合理规划分区,合理设计数据结构; 框架层面:在不改动源码的情况下,从kafka参数配置入手,结合业务体量和运行数据进行调优 JVM层面:在出现明显缓慢和可

MongoDB从入门到实战之.NET Core使用MongoDB开发ToDoList系统(2)-Swagger框架集成

Swagger是什么? Swagger是一个规范且完整API文档管理框架,可以用于生成、描述和调用可视化的RESTful风格的 Web 服务。Swagger 的目标是对 REST API 定义一个标准且和语言无关的接口,可以让人和计算机拥有无须访问源码、文档或网络流量监测就可以发现和理解服务的能力。

从源码彻底理解 Prometheus/VictoriaMetrics 中的 relabel_configs/metric_relabel_configs 配置

背景 最近接手维护了公司的指标监控系统,之后踩到坑就没站起来过。。 本次问题的起因是我们配置了一些指标的删除策略没有生效: - action: drop_metrics regex: "^envoy_.*|^url\_\_\_\_.*|istio_request_bytes_sum" 与这两个容易引

从源码层面深度剖析Spring循环依赖

作者:郭艳红 以下举例皆针对单例模式讨论 图解参考 https://www.processon.com/view/link/60e3b0ae0e3e74200e2478ce 1、Spring 如何创建Bean? 对于单例Bean来说,在Spring容器整个生命周期内,有且只有一个对象。 Sprin

从源码角度剖析 golang 如何fork一个进程

# 从源码角度剖析 golang 如何fork一个进程 创建一个新进程分为两个步骤,一个是fork系统调用,一个是execve 系统调用,fork调用会复用父进程的堆栈,而execve直接覆盖当前进程的堆栈,并且将下一条执行指令指向新的可执行文件。 在分析源码之前,我们先来看看golang fork