这篇 ReentrantLock 看不懂,加我我给你发红包(一)

简介: 在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗?

回答一个问题

在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗?

我听过一句话非常的经典,也是我认为是每个人都应该了解的一句话:你以为的并不是你以为的。明白什么意思么?不明白的话,加我微信我告诉你。

初识 ReentrantLock

ReentrantLock 位于 java.util.concurrent.locks 包下,它实现了 Lock 接口和 Serializable 接口。

微信图片_20220412185516.jpg

ReentrantLock 是一把可重入锁互斥锁,它具有与 synchronized 关键字相同的含有隐式监视器锁(monitor)的基本行为和语义,但是它比 synchronized 具有更多的方法和功能。

ReentrantLock 基本方法

构造方法

ReentrantLock 类中带有两个构造函数,一个是默认的构造函数,不带任何参数;一个是带有 fair 参数的构造函数

public ReentrantLock() {
  sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

第二个构造函数也是判断 ReentrantLock 是否是公平锁的条件,如果 fair 为 true,则会创建一个公平锁的实现,也就是 new FairSync(),如果 fair 为 false,则会创建一个 非公平锁的实现,也就是 new NonfairSync(),默认的情况下创建的是非公平锁

// 创建的是公平锁
private ReentrantLock lock = new ReentrantLock(true);
// 创建的是非公平锁
private ReentrantLock lock = new ReentrantLock(false);
// 默认创建非公平锁
private ReentrantLock lock = new ReentrantLock();

FairSync 和 NonfairSync 都是 ReentrantLock 的内部类,继承于 Sync 类,下面来看一下它们的继承结构,便于梳理。

微信图片_20220412185522.jpg

abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class FairSync extends Sync {...}
static final class NonfairSync extends Sync {...}

在多线程尝试加锁时,如果是公平锁,那么锁获取的机会是相同的否则,如果是非公平锁,那么 ReentrantLock 则不会保证每个锁的访问顺序

下面是一个公平锁的实现

public class MyFairLock extends Thread{
    private ReentrantLock lock = new ReentrantLock(true);
    public void fairLock(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()  + "正在持有锁");
        }finally {
            System.out.println(Thread.currentThread().getName()  + "释放了锁");
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        MyFairLock myFairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "启动");
            myFairLock.fairLock();
        };
        Thread[] thread = new Thread[10];
        for(int i = 0;i < 10;i++){
            thread[i] = new Thread(runnable);
        }
        for(int i = 0;i < 10;i++){
            thread[i].start();
        }
    }
}

不信?不信你输出试试啊!懒得输出?就知道你懒得输出,所以直接告诉你结论吧,结论就是自己试

试完了吗?试完了我是不会让你休息的,过来再试一下非公平锁的测试和结论,知道怎么试吗?上面不是讲过要给 ReentrantLock 传递一个参数的吗?你想,传 true 的时候是公平锁,那么反过来不就是非公平锁了?其他代码还用改吗?不需要了啊。

明白了吧,再来测试一下非公平锁的流程,看看是不是你想要的结果。

公平锁的加锁(lock)流程详解

通常情况下,使用多线程访问公平锁的效率会非常低(通常情况下会慢很多),但是 ReentrantLock 会保证每个线程都会公平的持有锁,线程饥饿的次数比较小。锁的公平性并不能保证线程调度的公平性。

此时如果你想了解更多的话,那么我就从源码的角度跟你聊聊如何 ReentrantLock 是如何实现这两种锁的。

微信图片_20220412185529.jpg

如上图所示,公平锁的加锁流程要比非公平锁的加锁流程简单,下面要聊一下具体的流程了,请小伙伴们备好板凳。

下面先看一张流程图,这张图是 acquire 方法的三条主要流程

微信图片_20220412185538.jpg

首先是第一条路线,tryAcquire 方法,顾名思义尝试获取,也就是说可以成功获取锁,也可以获取锁失败。

使用 ctrl+左键 点进去是调用 AQS 的方法,但是 ReentrantLock 实现了 AQS 接口,所以调用的是 ReentrantLock 的 tryAcquire 方法;

微信图片_20220412185541.jpg

首先会取得当前线程,然后去读取当前锁的同步状态,还记得锁的四种状态吗?分别是 无锁、偏向锁、轻量级锁和重量级锁,如果你不是很明白的话,请参考博主这篇文章(不懂什么是锁?看看这篇你就明白了),如果判断同步状态是 0 的话,就证明是无锁的,参考下面这幅图( 1bit 表示的是是否偏向锁 )

微信图片_20220412185544.jpg

如果是无锁(也就是没有加锁),说明是第一次上锁,首先会先判断一下队列中是否有比当前线程等待时间更长的线程(hasQueuedPredecessors);然后通过 CAS 方法原子性的更新锁的状态,CAS 方法更新的要求涉及三个变量,currentValue(当前线程的值),expectedValue(期望更新的值),updateValue(更新的值),它们的更新如下

if(currentValue == expectedValue){
  currentValue = updateValue
}

CAS 通过 C 底层机制保证原子性,这个你不需要考虑它。如果既没有排队的线程而且使用 CAS 方法成功的把 0 -> 1 (偏向锁),那么当前线程就会获得偏向锁,记录获取锁的线程为当前线程。

然后我们看 else if 逻辑,如果读取的同步状态是1,说明已经线程获取到了锁,那么就先判断当前线程是不是获取锁的线程,如果是的话,记录一下获取锁的次数 + 1,也就是说,只有同步状态为 0 的时候是无锁状态。如果当前线程不是获取锁的线程,直接返回 false。

acquire 方法会先查看同步状态是否获取成功,如果成功则方法结束返回,也就是 !tryAcquire == false ,若失败则先调用 addWaiter 方法再调用 acquireQueued 方法

然后看一下第二条路线 addWaiter

微信图片_20220412185549.jpg

这里首先把当前线程和 Node 的节点类型进行封装,Node 节点的类型有两种,EXCLUSIVESHARED ,前者为独占模式,后者为共享模式,具体的区别我们会在 AQS 源码讨论,这里读者只需要知道即可。

首先会进行 tail 节点的判断,有没有尾节点,其实没有头节点也就相当于没有尾节点,如果有尾节点,就会原子性的将当前节点插入同步队列中,再执行 enq 入队操作,入队操作相当于原子性的把节点插入队列中。

如果当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程。

相关文章
|
前端开发 JavaScript 物联网
JavaScript使用Modbus协议实现RTU设备连云
在阿里云物联网平台下发物模型属性设置数据,HaaS600Kit 接收并解析云端数据后控制 Modbus 继电器设备进行开关动作。
JavaScript使用Modbus协议实现RTU设备连云
uni-app 155朋友圈评论功能(二)
uni-app 155朋友圈评论功能(二)
214 0
|
7月前
|
人工智能 自然语言处理 运维
让AI读懂K线图!ChatTS-14B:字节开源的时间序列理解和推理大模型,自然语言提问秒解趋势密码!
ChatTS-14B是字节跳动开源的时间序列专用大模型,基于Qwen2.5-14B微调优化,通过合成数据对齐技术显著提升分析能力,支持自然语言交互完成预测推理等复杂任务。
1375 1
让AI读懂K线图!ChatTS-14B:字节开源的时间序列理解和推理大模型,自然语言提问秒解趋势密码!
在 Python 中,对列表进行排序有两种常用的方法
在 Python 中,对列表进行排序有两种常用的方法
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的卷积神经网络(CNN)入门与实践
【8月更文挑战第62天】本文以浅显易懂的方式介绍了深度学习领域中的核心技术之一——卷积神经网络(CNN)。文章通过生动的比喻和直观的图示,逐步揭示了CNN的工作原理和应用场景。同时,结合具体的代码示例,引导读者从零开始构建一个简单的CNN模型,实现对图像数据的分类任务。无论你是深度学习的初学者还是希望巩固理解的开发者,这篇文章都将为你打开一扇通往深度学习世界的大门。
|
监控 程序员 开发工具
如何规范Git提交-参考阿里云开发者社区
这篇文章分享了如何规范Git提交,介绍了commit message的格式规范,并通过webhook监控机制来确保代码提交的规范性,从而提高研发效率和代码维护质量。
|
XML 开发框架 JSON
【Azure 应用程序见解】 Application Insights 对App Service的支持问题
【Azure 应用程序见解】 Application Insights 对App Service的支持问题
153 0
|
存储 弹性计算 安全
对象存储OSS产品常见问题之ZIP包解压缩失败异常如何解决
对象存储OSS是基于互联网的数据存储服务模式,让用户可以安全、可靠地存储大量非结构化数据,如图片、音频、视频、文档等任意类型文件,并通过简单的基于HTTP/HTTPS协议的RESTful API接口进行访问和管理。本帖梳理了用户在实际使用中可能遇到的各种常见问题,涵盖了基础操作、性能优化、安全设置、费用管理、数据备份与恢复、跨区域同步、API接口调用等多个方面。
595 0
|
关系型数据库 Serverless 异构计算
Serverless 应用引擎产品使用之在阿里云函数计算中使用包含GPU的实例并且镜像超过10GB了如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
247 0
|
JSON 前端开发 JavaScript
ajax和axios、fetch的区别
ajax和axios、fetch的区别
197 0