深入理解Java中的Lock和AQS

news2024/11/8 13:43:18

文章目录

  • 前言
  • 正文
    • 一、Lock接口的定义
    • 二、ReentrantLock 的实现
    • 三、AbstractQueuedSynchronizer的实现
      • 3.1 AQS 中的加锁底层
      • 3.2 ReentrantLock中的 Sync 同步器
      • 3.3 NonfairSync 的实现
      • 3.4 FairSync 的实现
      • 3.5 公平锁和非公平锁的总结
        • 3.5.1 公平锁
        • 3.5.2 非公平锁
      • 3.6 释放锁

前言

提起Java中的锁,一般我们最快的反应是 synchronized 。但是在Java1.5之后,Doug Lea 大姥设计并实现的 JUC 中,提供了更加丰富的API操作。其中 Lock 接口及其相关实现尤为经典。今天我们来一起学习这 JUC 中的优秀设计思想。

synchronized 有锁不同,Lock接口及其实现是由Java代码实现的。其底层代码实现,基于抽象队列同步器(AbstractQueuedSynchronizer)以及 volatile 关键字、CAS机制。

以下内容分析,代码参考于Java11。

正文

一、Lock接口的定义

因为Lock本身是一个接口,所以我们在用的时候,基本都是找它的实现。而我们经常用到的是ReentrantLock 类。Lock中定义的方法如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xw6PwVpd-1687686276186)(img\20230625100927.jpg)]

对应的官方文档如下:Lock接口官方文档

从文档中也可以得知,经典的Lock用法是:

public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    // 上锁
    lock.lock();
    try {
        // TODO 你的需要加锁的代码
    } finally {
        // 解锁
        lock.unlock();
    }
}

需要额外注意的是, Lock在使用时,需要手动加锁和释放锁,其中释放锁需要放在 try-finally 代码块中的 finally 块内,保证其能及时释放。

二、ReentrantLock 的实现

ReentrantLockLock 接口的一个实现类 。也是我们经常用的一个锁,下面我们从源码的角度来看看它是如何实现一个锁的功能的。

可以先来看看类之间的关系:
在这里插入图片描述

随后我们看看ReentrantLock类的构造器是怎样的。

它一共提供了2个构造器:

public ReentrantLock() {
    sync = new NonfairSync();
}

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

关键的加锁和释放锁的方法也很短小:

// 加锁
public void lock() {
    sync.acquire(1);
}

// 释放锁
public void unlock() {
    sync.release(1);
}

观察构造器以及加锁、释放锁的方法我们可以知道,默认情况下我们使用的锁都是基于 NonfairSync(不公平同步器)的。

当你传入参数,一个布尔值,可以进行选择,使用公平还是不公平的同步器。

而在加锁、释放锁时调用的方法,是在抽象队列同步器(AbstractQueuedSynchronizer)中实现的。也就是常常被人提到的得 AQS

三、AbstractQueuedSynchronizer的实现

3.1 AQS 中的加锁底层

通过第二小节的分析,我们能够知道,加锁调用了acquire方法。在AQS中的定义如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        // 创建新节点,并将新节点放在队列尾部
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 补偿机制,自我中断
        selfInterrupt();
}

在这段代码中,我们看到会调用一次 tryAcquire 方法(尝试获取锁):

  • 如果该方法返回true会直接结束;

  • 如果该方法返回false,会调用acquireQueued,给队列中增加节点;增加节点返回true时,会中断当前线程。

  • acquireQueued 内部是一个“死循环”,一直尝试调用tryAcquire方法,而它只有在线程等待时中断或者出现其他异常时,会返回true。

    • final boolean acquireQueued(final Node node, int arg) {
              boolean interrupted = false;
              try {
                  for (;;) {
                      // 拿到node的前一个节点
                      final Node p = node.predecessor();
                      // 若前一个节点是head,说明自己现在是第一个,可以尝试获取锁
                      if (p == head && tryAcquire(arg)) {
                          // 将本次节点设置为head
                          setHead(node);
                          // help GC
                          p.next = null;
                          // 获取到锁,返回false
                          return interrupted;
                      }
                      // 阻塞判断:应该阻塞时会阻塞,不该阻塞时会再给一次抢锁机会
                      // 返回true表示需要阻塞
                      if (shouldParkAfterFailedAcquire(p, node))
                          // 阻塞线程,interrupted设置为true
                          interrupted |= parkAndCheckInterrupt();
                  }
              } catch (Throwable t) {
                  // 取消获取锁
                  cancelAcquire(node);
                  // 中断线程
                  if (interrupted)
                      selfInterrupt();
                  throw t;
              }
          }
      

因此我们可以分析出来,tryAcquire 如果返回true就说明获取锁成功。

AQS 在设计这里的时候,使用了模板方法设计模式,将 tryAcquire定义了,但是实现时只抛出了 UnsupportedOperationException

所以我们接下来要去看看AQS的子类(以及孙子类),也就是 SyncNonfairSyncFairSync

3.2 ReentrantLock中的 Sync 同步器

ReentrantLock 中定义了SyncNonfairSyncFairSync

3.3 NonfairSync 的实现

我们本小节主要关注“不公平同步器”,也是很常用的一种。

NonfairSync中代码很少,如下:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

而它里边的 nonfairTryAcquire方法在 Sync中做了实现。

因此加锁方法的核心实现是:

// 这里的入参acquires是1
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取AQS中的 volatile 修饰的 state值,默认为0,表示没有获取锁
    int c = getState();
    if (c == 0) {
        // 进行CAS改值,改值成功返回true,表示获取锁成功
        if (compareAndSetState(0, acquires)) {
            // 设置当前线程独占锁
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // state值不是0,表示已经有线程持有锁
    // 判断当前持有锁的是不是当前线程,如果是,则计算新的state值,获取锁成功
    else if (current == getExclusiveOwnerThread()) {
        // 计算新的状态值
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 修改AQS中的state
        setState(nextc);
        return true;
    }
    // 其他情况
    return false;
}

基本流程如下:
在这里插入图片描述

3.4 FairSync 的实现

接下来我们看看公平锁时怎么做的,它和非公平锁又有什么区别呢。

FairSync 中实现了 tryAcquire 方法,如下:

@ReservedStackAccess
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;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

查看代码可以发现,它和非公平锁很像,只是在**“没有线程占用锁时,获取锁增加了额外的条件”**,这个条件是hasQueuedPredecessors。它的含义是:查询是否有任何线程等待获取的时间超过当前线程。

也就是说,如果存在等待时间超过当前线程的线程,本轮流程中,当前线程就不会获取锁,会创建节点加入到队列中,看着挺公平的(不是绝对公平的)。以等待时间做为了判定条件。

有了这个等待时间的判定条件,我们就可以预料到一些问题:

假如队列里有等待时间不一的线程节点,有个长时间的任务,还有一些短时间的任务,那使用公平锁时,会优先给到长时间的那个任务,导致后边短时间可以执行完的任务一直在等。

也正是因此,它只是适合某种场景。我们默认使用时,大多还是会选择非公平锁。

3.5 公平锁和非公平锁的总结

这里省略了关于等待时间细节判断,以整个加锁流程来总结:

3.5.1 公平锁

​ 在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会从尾结点加入队列。

3.5.2 非公平锁

​ 在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁,这里其实是一种插队的表现。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。

​ 此时,队列中被唤醒的线程并不一定能优先获得锁,当队列中被唤醒的线程被新来的线程抢占了资源,这种插队也就表现出了非公平的特性。

3.6 释放锁

在AQS中定义了 release方法,用于帮助实现释放锁。这里的参数在ReentrantLock的unlock方法中传递了1。

public final boolean release(int arg) {
    // 尝试释放锁,true表示释放成功
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 唤醒下一个节点(线程)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

ReentrantLock的内部类中的Sync对 tryRelease 方法进行了重写。

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    // 当前state减去1
    int c = getState() - releases;
    // 当前线程不是占有锁的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 释放成功
    if (c == 0) {
        free = true;
        // 设置没有线程占用锁了
        setExclusiveOwnerThread(null);
    }
    // 修改state值
    setState(c);
    return free;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/683784.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

突破边界:高性能计算引领LLM驶向通用人工智能AGI的创新纪元

AGI | AIGC | 大模型训练 | GH200 LLM | LLMs | 大语言模型 | MI300 ChatGPT的成功带动整个AIGC产业的发展&#xff0c;尤其是LLM&#xff08;大型语言模型&#xff0c;大语言模型&#xff09;、NLP、高性能计算和深度学习等领域。LLM的发展将为全球和中国AI芯片、AI服务器市场…

什么样的产品更适合做海外网红营销?

随着社交媒体和全球化的兴起&#xff0c;海外网红营销成为了一种非常受欢迎的推广方式。然而&#xff0c;并非所有产品都能够在海外市场成功借助网红营销实现推广目标。本文Nox聚星将和大家详细探讨什么样的产品更适合在海外进行网红营销&#xff0c;并提供相关的策略建议。 一…

汽车智能化进入赛点:城市NOA落地竞速,战至最后一公里

城市NOA的竞争正在加速进入落地阶段&#xff0c;6月即将结束&#xff0c;理想汽车计划在剩余几天内&#xff0c;在北京和上海开启城市辅助智能驾驶功能内测&#xff0c;并在下半年推出通勤智能驾驶功能。 其应用方法是&#xff0c;车主可用在日常使用中&#xff0c;基于智能化…

Linux系统编程(多进程编程深入1)

文章目录 前言一、进程参数和环境变量的意义二、子进程程序结束返回值到哪里去了&#xff1f;三、进程退出函数四、实际使用案例五、僵尸进程总结 前言 本篇文章我们深入的讲解多进程编程。 一、进程参数和环境变量的意义 进程参数和环境变量是两种不同的机制&#xff0c;但…

SuperMap GIS基础产品桌面GIS FAQ集锦(3)

SuperMap GIS基础产品桌面GIS FAQ集锦&#xff08;3&#xff09; 【iDesktop】如何获取倾斜摄影的边界线&#xff1f; 【解决办法】1、将倾斜摄影添加到球面场景&#xff0c;使用【三维分析】-【生成DSM】获取栅格数据集 2、使用【代数运算】功能&#xff0c;将大于0的栅格值统…

我做了10年的测试,由衷的建议年轻人别入这行了

两天前&#xff0c;有个做功能测试7年的同事被裁员了。这位老哥已经做到了团队中的骨干了&#xff0c;人又踏实&#xff0c;结果没想到刚刚踏入互联网“老龄化”大关&#xff0c;就被公司给无情优化了。 现在他想找同类型的工作&#xff0c;薪资也一直被压&#xff0c;考虑转行…

java(SpringBoot)中操作Redis的两种方式

前言 之前我们介绍过了redis的五中基本类型以及在可视化界面进行操作&#xff0c;那么在开发中&#xff08;在代码中&#xff09;我们通常使用&#xff0c;jedis进行操作redis,要是springboot 项目&#xff0c;我们通常使用redisTemplte进行操作 首先将redis启动 方式一 Jred…

smart Java——Netty实战(上):select/poll/epoll、NIOReactor模型

文章目录 1.多路复用——select、poll、epoll底层原理2.NIOReactor模型&#xff08;1&#xff09;单Reactor单线程模型&#xff08;2&#xff09;单Reactor多线程模型&#xff08;3&#xff09;主从Reactor多线程模型&#xff08;Netty&#xff09; 3.Netty核心组件&#xff08…

备战金九银十,互联网大厂1000道java高频面试知识点(附答案,赶紧收藏)

Java 面试八股文有必要背吗&#xff1f; 我的回答是&#xff1a;很有必要。你可以讨厌这种模式&#xff0c;但你一定要去背&#xff0c;因为不背你就进不了大厂。现如今&#xff0c;Java 面试的本质就是八股文&#xff0c;把八股文面试题背好&#xff0c;面试才有可能表现好。…

卷积实现—im2col+gemm

普通卷积 看卷积的实现&#xff0c;先看其普通的计算方式&#xff1a;滑窗计算和其计算shape大小的公式&#xff0c;以及各个卷积特性对其计算的影响&#xff0c;比如&#xff1a;stride&#xff0c;group&#xff0c;dilation&#xff0c;pad等。 H o u t ( H i n − k h p …

OpenGL 帧缓冲

1.简介 我们已经使用了很多屏幕缓冲了&#xff1a;用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲(Framebuffer)&#xff0c;它被储存在内存中。OpenGL允许我们定义我们自己的帧缓冲&#x…

Fiddler不仅可以抓包,还可以做接口测试喔

前言 Fiddler最大的优势在于抓包&#xff0c;我们大部分使用的功能也在抓包的功能上&#xff0c;Fiddler做接口测试也是非常方便的。对应没有接口测试文档的时候&#xff0c;可以直接抓完包后&#xff0c;copy请求参数&#xff0c;修改下就可以了。 Composer简介 点开右侧Co…

模拟电路系列分享-晶体管的四种状态

目录 概要 整体架构流程 技术名词解释 1.截止状态 2.放大状态 3.饱和状态 4.倒置状态 技术细节 小结 概要 提示&#xff1a;这里可以添加技术概要 晶体管有4种工作状态&#xff0c;分别是截止、放大、饱和&#xff0c;以及倒置。 整体架构流程 提示&#xff1a;这里可以添加…

黑马程序员前端 Vue3 小兔鲜电商项目——(九)购物车

文章目录 本地购物车添加购物车头部购物车模板代码渲染数据 删除功能实现购物车统计信息列表购物车-基础内容渲染模版代码路由配置渲染列表 列表购物车-单选功能实现列表购物车-全选功能实现列表购物车-统计数据功能实现 接口购物车加入购物车删除购物车 退出登录-清空购物车购…

Matplotlib - 绘制 带有对角线的散点图 (Diagonal Scatter Plots) 函数源码

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/131384440 Matplotlib 是一个用于绘制二维图形的 Python 库&#xff0c;提供了一个 pyplot 模块&#xff0c;用于创建各种类型的图表。其…

实战经验---好用的Midjourney提示词

说明&#xff1a;本文基于Midjourney&#xff08;下称MJ&#xff09;官方说明书整理&#xff0c;分2篇文章发布&#xff0c;这是第1篇。内容主要包括绘画指令&#xff08;Prompt&#xff09;、提示、参数、设置、模型选择等。 由于是整理文本&#xff0c;过程中个别地方为了显…

应对流量损耗:提升APP广告变现效果的关键策略!

​引言&#xff1a; 在APP广告变现的过程中&#xff0c;流量损耗是一个常见的问题&#xff0c;它不可避免地会发生。尽管开发者可以在合理的范围内承受这种损耗&#xff0c;但如果出现大范围的损耗&#xff0c;那就意味着在广告变现过程中出现了一些问题&#xff0c;限制了开发…

ThreadPoolExecutor的有参构造

核心要点 通过查看ThreadPoolExecutor里面的构造方法可以发现都是调用了方法参数最多的那个。 参数最多的构造方法展示 映入眼帘的是一些健壮性校验corePoolSize < 0&#xff1b;可以发现这个判断是核心线程的个数不能小于零。但是也就说明核心线程的个数可以等于零。maxim…

LLM - 搭建 DrugGPT 与药物分子领域结合的 ChatGPT 系统

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/131384199 论文&#xff1a;DrugChat: Towards Enabling ChatGPT-Like Capabilities on Drug Molecule Graphs DrugChat&#xff0c;基…

【简单认识Nginx服务性能与安全优化】

文章目录 Nginx隐藏版本相关信息1.隐藏版本号2.修改版本号及相关信息 二、修改Nginx运行时的属主和属组三、配置Nginx网页缓存时间四、配置Nginx站点日志分割五、设置Nginx长连接及超时时间六、配置Nginx网页压缩七、配置Nginx防盗链1.模拟盗链2.配置防盗链并测试 Nginx隐藏版本…