Java - AbstractQueuedSynchronizer

news2024/11/24 11:05:03

AQS简介

AQS全称AbstractQueuedSynchronizer,抽象队列同步器,是一个实现同步组件的基础框架。AQS使用一个int类型的成员变量state维护同步状态,通过内置的同步队列(CLH锁、FIFO)完成线程的排队工作,底层主要是通过CAS操作和volatile特性实现。

它主要包含两种模式:独占模式(例如ReentrantLock)和共享模式(例如CountDownLatch)。关键方法包括acquire和release,用于独占模式下的获取和释放操作,以及acquireShared和releaseShared,用于共享模式下的操作。

它简化了同步组件的实现方式,屏蔽了同步状态的管理、线程的排队等待与唤醒等底层操作。我们只需要通过继承实现AQS中的抽象方法,即可实现不同同步语义的同步组件。例如ReentrantLock这种独占式同步组件,核心逻辑仅重写了tryAcquire和tryRelease等少数方法,就实现了可重入独占式锁的功能。

AQS通过继承来扩展其功能,子类通过继承AQS实现抽象方法来管理同步状态,同步组件则通过聚合子类的方法来实现自己的同步特性,独占式或共享式。

使用AQS实现一个自定义排他锁

继承AQS:
创建一个新的类,继承AbstractQueuedSynchronizer。

实现核心方法:
实现tryAcquire和tryRelease方法(用于独占模式),或实现tryAcquireShared和tryReleaseShared方法(用于共享模式)。

内部类Sync:
通常创建一个内部静态类Sync,扩展AQS并实现上述方法。

对外方法:
在外部类中,提供获取和释放的方法,内部调用AQS的方法acquire、release、acquireShared和releaseShared。

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class SimpleLock {
    private final Sync sync = new Sync();

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
    }

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

    public void unlock() {
        sync.release(1);
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

使用AQS实现一个自定义共享锁

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class SimpleSemaphore {
    private final Sync sync;

    public SimpleSemaphore(int permits) {
        sync = new Sync(permits);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int permits) {
            setState(permits);
        }

        @Override
        protected int tryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 || compareAndSetState(available, remaining)) {
                    return remaining;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (compareAndSetState(current, next)) {
                    return true;
                }
            }
        }
    }

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public void release() {
        sync.releaseShared(1);
    }
}

同步队列和条件队列的区别


同步队列(synchronization queue)和条件队列(condition queue)是AbstractQueuedSynchronizer(AQS)中的两个不同的队列,它们有不同的用途和工作机制。

同步队列

  • 用途:管理所有等待获取锁或同步状态的线程。
  • 触发条件:当线程尝试获取锁或同步状态失败时进入队列。
  • 结构:双向链表。
  • 作用:维护线程的顺序,确保公平竞争。
  • 唤醒机制:当锁释放或同步状态改变时,唤醒队列中的下一个线程。

条件队列

  • 用途:管理等待特定条件的线程(通过ConditionObject)。
  • 触发条件:当线程调用await()方法时进入队列,并释放当前持有的锁。
  • 结构:单向链表。
  • 作用:等待特定条件的满足,如某个状态的变化。
  • 唤醒机制:当条件满足时(通过signal()或signalAll()),线程从条件队列移动到同步队列,等待重新获取锁。

为什么await方法一定要加锁才能调用

我理解条件队列现在的实现是在await方法先完全释放锁,再进入休眠状态等待唤醒,被唤醒后重新尝试加锁。先完全释放锁,是为了防止当前线程持有锁时错误休眠,若当前线程持有锁进入休眠状态,即使当前线程被唤醒,也是尝试加锁而不是解锁,那么其他线程永远无法拿到锁。因此先完全解锁再休眠,被唤醒后重新尝试加锁,应该是出于防止锁饥饿与死锁的考虑。

AQS独占式加锁(以ReentrantLock加锁为例)

加锁流程

加锁源码分析

解锁流程

解锁源码分析

CLH锁(同步队列的原型)

CLH锁是对自旋锁的一种改良,自旋锁只适合竞争不激烈,加锁时间短的场景,原因是自旋锁有2个问题,第一,存在锁饥饿问题,可能会出现某个线程一直拿不到锁的情况;第二,锁状态中心化,激烈竞争时会导致CPU的高速缓存频繁失效,导致性能降低。

CLH队列锁有效地解决了以上2个问题,首先,CLH锁通过增加队列进行排队,确保所有线程都有机会拿到锁;第二,CLH锁的每个线程监视的都是上一个节点的锁状态,因此锁状态是去中心化的,不会导致CPU高速缓存频繁失效。

CLH锁实现源码

import java.util.concurrent.atomic.AtomicReference;

public class CLH {
    private final ThreadLocal<Node> node = ThreadLocal.withInitial(Node::new);
    private final AtomicReference<Node> tail = new AtomicReference<>(new Node());

    private static class Node {
        // ①锁定状态必须用volatile修饰以确保内存可见性
        private volatile boolean locked;
    }

    public void lock() {
        Node node = this.node.get();
        node.locked = true;
        Node pre = this.tail.getAndSet(node);
        while (pre.locked) ;
    }

    public void unlock() {
        Node node = this.node.get();
        node.locked = false;
        // ②重置当前线程对应的Node节点避免死锁
        this.node.set(new Node());
    }
}

原理分析

观察代码可以发现,CLH没有前驱或后继指针,因为CLH锁是一种隐式队列,入队只需要添加新的tail节点,出队只需要修改头部状态。注释①处必须用volatile修饰,以确保内存可见性与代码有序性。注释②处必须重置线程Node,否则当一个线程重复加锁时,就有可能死锁,死锁原因可以看这篇文章的解锁分析部分。

CLH锁的优点是性能优异、实现简单、加锁公平、扩展性强。但缺点是有自旋操作,长时间加锁会浪费CPU性能,再就是功能单一,不支持复杂的功能。

针对以上两个缺点,AQS都做了改进。针对第一个缺点,将自旋改为了线程阻塞等待;针对第二个缺点,AQS做了很多改造和扩展,例如增加了每个节点的状态waitStatus、显式维护了前驱和后继节点等等,基于这些扩展,AQS实现了独占锁、共享锁、线程排队、状态传播等复杂功能。

参考链接

从ReentrantLock的实现看AQS的原理及应用 - 美团技术团队

AQS 详解 | JavaGuide

AQS的前菜—详解CLH队列锁_clh锁-CSDN博客

Java AQS 核心数据结构-CLH 锁

JUC锁:核心类AQS源码详解 - 拿了桔子跑-范德依彪 - 博客园

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

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

相关文章

AI大模型如何赋能智能座舱

AI 大模型如何赋能智能座舱 从上海车展上&#xff0c;我们看到由于智能座舱配置性价比较高&#xff0c;已经成为车企的核心竞争点之一&#xff0c;随着座舱硬件规模化装车&#xff0c;蔚小理、岚图、极狐等新势力开始注重座舱多模态交互&#xff0c;通过集成语音/手势/触控打造…

Vue速成学习笔记

这两天速成了一下Vue&#xff0c;在这里记录一下相关的笔记&#xff0c;之后有时间详细学Vue的时候再来回顾一下&#xff01; 一、Vue理解 1、Vue的核心特征&#xff1a;双向绑定。 在网页中&#xff0c;存在视图和数据。在Vue之前&#xff0c;需要使用JavaScript编写复杂的逻…

电脑同时配置两个版本mysql数据库常见问题

1.配置时&#xff0c;要把bin中的mysql.exe和mysqld.exe 改个名字&#xff0c;不然两个版本会重复&#xff0c;当然&#xff0c;在初始化数据库的时候&#xff0c;如果时57版本的&#xff0c;就用mysql57(已经改名的)和mysqld57 代替 mysql 和 mysqld 例如 mysql -u root -p …

Redis(十二) 持久化

文章目录 前言Redis实现数据的持久化Redis实现持久化的策略RDB手动触发RDB持久化操作自动触发RDB持久化操作 AOFAOF重写机制 前言 众所周知&#xff0c;Redis 操作数据都是在内存上操作的&#xff0c;而我们都知道内存是易失的&#xff0c;服务器重启或者主机掉电都会导致内存…

面试八股之MySQL篇4——事务篇

&#x1f308;hello&#xff0c;你好鸭&#xff0c;我是Ethan&#xff0c;一名不断学习的码农&#xff0c;很高兴你能来阅读。 ✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。 &#x1f3c3;人生之义&#xff0c;在于追求&#xff0c;不在成败&#xff0c;勤通…

生命线上的高效传递:了解下医院内、外网文件交互方式的革新之路

在医院的日常运营中&#xff0c;普遍采用内外网隔离的建设方式。内网集信息管理、通讯协作、资源共享、业务流程管理于一身&#xff0c;承载了医院的医疗核心业务&#xff0c;如HIS&#xff08;医院信息系统&#xff09;、LIS&#xff08;实验室信息系统&#xff09;、EMR&…

如何灵活运用keil工具进行问题分析(1)— 解决日常程序卡死问题

前言 &#xff08;1&#xff09;如果有嵌入式企业需要招聘湖南区域日常实习生&#xff0c;任何区域的暑假Linux驱动实习岗位&#xff0c;可C站直接私聊&#xff0c;或者邮件&#xff1a;zhangyixu02gmail.com&#xff0c;此消息至2025年1月1日前均有效 &#xff08;2&#xff0…

山脉数组的峰顶索引 ---- 二分查找

题目链接 题目: 分析: 我们很明显, 可以从峰值位置将数组分成两段, 具有"二段性", 所以可以用二分查找因为arr是山峰数组, 不存在相等的情况如果arr[mid] > arr[mid 1], 说明mid的位置可能是峰值, 移动right mid如果arr[mid] < arr[mid 1], 说明mid的位置…

Java基础之异常(简单易懂)

异常 1.JAVA异常体系 &#xff08;1&#xff09;Throwable类(表示可抛)是所有异常和错误的超类&#xff0c;两个直接子类为Error和Exception,分别表示错误和异常;其中异常类Exception又分为运行时异常和非运行时异常&#xff0c;这两个异常有很大区别&#xff0c;运行时异常也…

【分享笔记】符尧:预训练、指令微调、对齐、专业化——论大语言模型能力的来源

分享时间&#xff1a;2023.2 目录 模型家族scaling law和涌现能力模型不同阶段pretrainingintruction tuningalignment upper bound和lower bound 模型家族 看模型要从演化家族来看&#xff0c;而不能单独看&#xff0c;很多人认为一些能力并不是RLHF激发出来的&#xff0c;而…

【区块链】智能合约漏洞测试

打开Ganache vscode打开智能合约漏洞工程 合约内容 pragma solidity >0.8.3;contract EtherStore {mapping(address > uint) public balances;function deposit() public payable {balances[msg.sender] msg.value;emit Balance(balances[msg.sender]);}function with…

前端工程化07-常见的包管理工具npm、yarn、cnpm、npx、pnpm

8、包管理工具 8.1、包管理工具概述 npm包管理工具、在安装node的时候这个东西就已经安装过了&#xff0c;通过npm去管理包的时候这个时候回有一个配置文件叫做package.json,他是以json的方式来书写对应的一个配置文件&#xff0c;这个配置文件是可以添加特别多的一些字段的&…

d3dx9_41.dll是个什么东西?d3dx9_41.dll文件丢失的解决方法

随着软件技术的不断发展&#xff0c;电脑用户可能会遇到各种系统错误和问题&#xff0c;其中之一就是动态链接库&#xff08;DLL&#xff09;文件的丢失。d3dx9_41.dll文件丢失是一个常见的问题&#xff0c;它通常会在运行依赖于DirectX图形技术的游戏或应用程序时被报告。这个…

MySQL中如何知道数据库表中所有表的字段的排序规则是什么?

查看所有表的字段及其排序规则&#xff1a; 你可以查询 information_schema 数据库中的 COLUMNS 表&#xff0c;来获取所有表的字段及其排序规则。以下是一个示例查询&#xff1a; SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLLATION_NAME FROM information_schema.COL…

影视解说5.0版零基础视频课程

课程简介 现在还能做解说吗、不会写解说文案怎么解决、不会配音怎么解决、如何找到合适的素材资源、如何变现…这是很多想做解说的伙伴最关心的几大问题。比如文案&#xff0c;我们推荐一个网站&#xff0c;10分钟搞定一篇文案&#xff0c;配音可以真人配音也可以软件配音。5.…

Windows11的这个地方暴露着你的隐私,把它关掉避免尴尬

前言 现在的电脑真的是越来越智能化&#xff01;现在有很多小伙伴都是用着Windows11的吧&#xff01;用习惯了Windows11之后&#xff0c;突然发现它还是挺顺手的。 但不知道你有没有发现&#xff0c;Windows11上面有个地方暴露着你的隐私。这个隐私可能是某个小姐姐的图片&am…

Android 12系统源码_多窗口模式(二)系统实现分屏的功能原理

前言 上一篇我们具体分析了系统处于多窗口模式下&#xff0c;Android应用和多窗口模式相关方法的调用顺序&#xff0c;对于应用如何适配多窗口模式有了一个初步的认识&#xff0c;本篇文章我们将会结合Android12系统源码&#xff0c;具体来梳理一下系统是如何触发多窗口分屏模…

StringMVC

目录 一&#xff0c;MVC定义 二&#xff0c;SpringMVC的基本使用 2.1建立连接 - RequestMapping("/...") ​编辑 2.2请求 1.传递单个参数 2.传递多个参数 3.传递对象 4.参数重命名 5.传递数组 6. 传递集合 7.传递JSON数据 8. 获取url中数据 9. 传递文…

mysql实战——异步复制(gtid复制)

一、搭建前准备 主库 192.168.1.76 从库 192.168.1.78 二、搭建 1、编辑配置文件 主库 server-id76 gtid_modeon enforce_gtid_consistencyon log_binmaster-binlog log-slave-updates1 binlog_formatrow 从库 gtid_modeon enforce_gtid_consistencyon server_id7…

huggingface笔记:LLama 2

1 前提tip 1.1 使用什么数据类型训练模型&#xff1f; Llama2模型是使用bfloat16训练的 上传到Hub的检查点使用torch_dtype float16&#xff0c;这将通过AutoModel API将检查点从torch.float32转换为torch.float16。在线权重的数据类型通常无关紧要&#xff0c;这是因为模型…