Java并发编程(四)线程同步 中 [AQS/Lock]

news2025/1/13 15:49:48

概述

Java中可以通过加锁,来保证多个线程访问某一个公共资源时,资源的访问安全性。Java提出了两种方式来加锁

  • 第一种是我们上文提到的通过关键字synchronized加锁,synchronized底层托管给JVM执行的,并且在java 1.6 以后做了很多优化(偏向锁、自旋、轻量级锁),使用很方便且性能也很好,所以在非必要的情况下,建议使用synchronized做同步操作;
  • 第二种是本文将要介绍的通过java.util.concurrent包下的Lock来加锁(lock大量使用CAS+自旋。因此根据CAS特性建议在低锁冲突的情况下使用lock)

AQS

概述

  • AQS全称AbstractQueuedSynchronizer,译为抽象队列同步器
  • AQS底层数据结构是被volatile修饰state和一个Node双向队列
  • Lock下的实现类包括ReentrantLock、ReadLock、WriteLock底层都是基于AQS实现锁资源获取或释放

内部结构

根据源码我们可以知道AQS维护了一个volatile的state和一个CLH(FIFO)双向队列

state是一个由volatile修饰的int型互斥变量,state=0表示没有任务线程使用该资源,而state>=1表示已经有线程正在持有锁资源。CLH队列是由内部类Node来维护的FIFO队列

实现原理

当一个线程获取锁资源时首先会判断state是否等于0(无锁状态),如果是0则把这个state更新为1,此时该锁资源被占用。在这个过程中,如果多个线程同时进行state更新操作,就会导致线程的安全性问题。因此AQS底层采用了CAS机制,来保证互斥变量state更新的原子性。未获得锁的线程通过Unsafe类中的park方法去进行阻塞,把阻塞的线程按照先进先出的原则放到CLH双向链表中,当获得锁的线程释放锁后,会从这个双向链表的头部去唤醒下一个等待的线程再去竞争锁。

公平锁和非公平锁

在竞争锁资源时,公平锁要判断双向链表中是否有阻塞的线程,如果有则需要去排队等待。而非公平锁的处理方式是,不管双向链表中是否有阻塞的线程在排队等待,它都会去尝试修改state变量去竞争锁,这对链表中排队的线程来说是非公平的。

Lock接口

Lock实现类

  • JDK8中,除了StampedLock为不可重入锁,其他包括ReentrantLock、ReentrantReadWriteLock以及Synchronized关键字都是可重入锁
  • 可重入锁是指一个线程抢占到了互斥锁资源且在锁释放之前可以重复获取该锁资源,只需要记录重入次数state递增1即可
  • lock实际上是通过更新AQS中的state来控制锁的持有情况

Lock方法

// 尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock();
// 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
// 尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

ReentrantLock

  • 根据源码可以看到实现锁功能的关键成员变量Sync类型的sync继承AQS
  • Sync在ReentrantLock中有两个实现类NonfairSync公平锁类型和FairSync非公平锁类型
  • ReentrantLock默认是非公平锁实现,在实例化时可以指定选择公平锁或者非公平锁

ReentrantLock获取锁流程

//.lock()调用的是AQS的acquire()
public void lock() {
    sync.acquire(1);
}

public final void acquire(int arg) {
    //tryAcquire:会尝试通过CAS获取一次锁。
    //addWaiter:将当前线程加入双向链表(等待队列)中
    //acquireQueued:通过自旋,判断当前队列节点是否可以获取锁
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//---------------------非公平锁尝试获取锁的过程---------------------
protected final boolean tryAcquire(int acquires) {
	// AQS的nonfairTryAcquire()方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state
    int c = getState();
    if (c == 0) {
    	// 目前没有线程获取锁,通过CAS(乐观锁)去修改state的值
        if (compareAndSetState(0, acquires)) {
        	// 设置持有锁的线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 锁的持有者是当前线程(重入锁)
    else if (current == getExclusiveOwnerThread()) {
    	// state + 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

//---------------------当前线程加入双向链表的过程---------------------
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
    	// 获取末位节点
        Node oldTail = tail;
        if (oldTail != null) {
        	// 当前节点的prev设置为原末位节点
            node.setPrevRelaxed(oldTail);
            // CAS确保在线程安全的情况下,将当前线程加入到链表的尾部
            if (compareAndSetTail(oldTail, node)) {
            	// 原末位节点的next设置为当前节点
                oldTail.next = node;
                return node;
            }
        } else {
        	// 链表为空则初始化
            initializeSyncQueue();
        }
    }
}

//---------------------首节点自旋过程---------------------
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 首节点线程去尝试竞争锁
            if (p == head && tryAcquire(arg)) {
            	// 成功获取到锁,从首节点移出(FIFO)
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

ReentrantLock释放锁流程

释放锁本质就是对AQS中的状态值State进行逐步递减操作

//.unlock()调用AQS的release()方法释放锁资源
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
	// Sync的tryRelease()方法
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
	// 获取状态
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 修改锁的持有者为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

ReentrantReadWriteLock

  • ReentrantReadWriteLock读写锁可以分别获取读锁或写锁,即将数据的读写操作分开;
  • writeLock():获取写锁,readLock():获取读锁
  • 读锁使用共享模式,写锁使用独占模式。即不存在写锁时,读锁可以被多个线程同时持有;存在写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁;而当有读锁时,写锁就不能获得
  • 适用于读多写少应用场景,如缓存

Condition

  • Condition也是一种线程通信的机制,通过await和singalAll()实现线程阻塞和唤醒
  • 底层数据结构是复用AQS的Node类,由不带头结点的链表实现的队列
  • await实现原理:通过LockSupport.park将当前线程置于Waiting阻塞状态,直到其他线程调用signal或signalAll将等待队列的队头结点移入到同步队列中,使其有机会通过自旋获取到锁
  • signal/signalAll:将等待队列的队头结点移入到同步队列中,并通过LockSupport.unpark唤醒该线程
  • 与Object的wait/notify机制对比
    • Condition支持不响应中断,而object不能
    • Lock可以支持多个condition等待队列,object只能支持一个
    • Condition能够对await设置超时时间,而object不能
  • 可以通过Lock+Condition实现生产者-消费者问题(在后文并发实践篇会有相关示例)

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

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

相关文章

stringstream常见用法

目录 构造函数 输出字符串 修改和清空字符串 利用 stringstream 去除字符串空格 利用stringstream去除指定的字符 stringstream 数据库 <sstream> 构造函数 创建一个对象&#xff0c;向对象输入字符串&#xff1a; string x"abcdefg";stringstream s…

运维监控学习笔记1

1、监控对象&#xff1a; 1、监控对象的理解&#xff1b;CPU是怎么工作的&#xff1b; 2、监控对象的指标&#xff1a;CPU使用率&#xff1b;上下文切换&#xff1b; 3、确定性能基准线&#xff1a;CPU负载多少才算高&#xff1b; 2、监控范围&#xff1a; 1、硬件监控&#x…

选择最适合自己的笔记本

选择最适合自己的笔记本电脑 一、了解笔记本品牌一线品牌准一线品牌二线品牌三线品牌 二、笔记本入手渠道笔记本入手渠道 三、根据需求选择机型使用需求1.日常使用2.商务办公、财务3.轻度剪辑、ps4.代码5.创意设计6.游戏 四、笔记本电脑配置如何选1.cpu2.显卡&#xff08;GPU&a…

在x86下运行的Ubuntu系统上部署QEMU用于模拟RISC-V硬件环境

1.配置工作环境 sudo apt install gcc bison flex libncurses-dev ninja-build \pkg-config build-essential zlib1g-dev pkg-config libglib2.0-dev \binutils-dev libboost-all-dev autoconf libtool libssl-dev \libpixman-1-dev python-capstone virtualenv software-prop…

第三章 图论 No.10无向图的双连通分量

文章目录 定义Tarjan求e-DCCTarjan求v-DCC395. 冗余路径1183. 电力396. 矿场搭建 定义 无向图有两种双连通分量 边双连通分量&#xff0c;e-DCC点双连通分量&#xff0c;v-DCC 桥&#xff1a;删除这条无向边后&#xff0c;图变得不连通&#xff0c;这条边被称为桥 边双连通分…

(文章复现)基于灰狼算法(GWO)的交直流混合微网经济调度matlab代码

参考文献&#xff1a; [1]高瑜,黄森,陈刘鑫等.基于改进灰狼算法的并网交流微电网经济优化调度[J].科学技术与工程, 2020,20(28):11605-11611. [2]邓长征,冯朕,邱立等.基于混沌灰狼算法的交直流混合微网经济调度[J].电测与仪表, 2020, 57(04):99-107. 这两篇文章不管是从模型、…

【JavaScript】new 的原理以及实现

网道 - new 命令的原理 使用new命令时&#xff0c;它后面的函数依次执行下面的步骤。 创建一个空对象&#xff0c;作为将要返回的对象实例。将这个空对象的原型&#xff0c;指向构造函数的prototype属性。将这个空对象赋值给函数内部的this关键字。如果构造函数返回了一个对象…

[内网渗透]CFS三层靶机渗透

文章目录 [内网渗透]CFS三层靶机渗透网络拓扑图靶机搭建Target10x01.nmap主机探活0x02.端口扫描0x03.ThinkPHP5 RCE漏洞拿shell0x04.上传msf后门(reverse_tcp)反向连接拿主机权限 内网渗透Target2&#xff08;1&#xff09;路由信息探测&#xff08;2&#xff09;msf代理配置&a…

Air001基于Keil环境点灯和调试输出工程配置

Air001基于Keil环境点灯和调试输出工程配置 &#x1f4cc;官方环境搭建教程介绍&#xff1a;https://wiki.luatos.com/chips/air001/Air001-MDK.html&#x1f516;本人使用的是基于HAL库环境搭建的。&#x1f4cd;SDK开发资源链接&#xff1a;https://gitee.com/openLuat/luato…

私域流量宝工具源码搭建-含详细使用说明

&#x1f44b;私域流量宝致力于为个人、团队提供基于微信私域流量的推广、引流的效率工具。可减轻人力&#xff0c;有效降低资源损失、流量流失的几率。引流宝完全开源&#xff0c;免费&#xff0c;可商用、可任意二次开发。引流宝可以辅助你更好地开展营销活动推广&#xff01…

【Docker系列】push镜像报错问题解决方案

1 问题描述 docker push 报这个错&#xff0c;unknown blob 详细报错内容&#xff1a; Use docker scan to run Snyk tests against images to find vulnerabilities and learn how to fix them The push refers to repository [192.******/*******/*************] 3b3341e9d03…

2498. 青蛙过河 II;2568. 最小无法得到的或值;1954. 收集足够苹果的最小花园周长

2498. 青蛙过河 II 核心思想&#xff1a;这题有点开脑洞&#xff0c;就是如果想让代价最小只能是隔一个石头跳&#xff0c;因为其他方法的路径都会形成比这种方法大的结果&#xff0c;然后我们只需要统计出间隔石头的最大值即可。 2568. 最小无法得到的或值 核心思想&#xf…

【第二阶段】kotlin语言的匿名函数类型推断

1.常规匿名函数写法&#xff1a; 如果使用了":",必须给定参数类型和 返回值类型如下&#xff1a; val meThod:()->Int{}2.匿名函数“”&#xff0c;返回类型推断 使用类型推断“”&#xff0c;根据返回值的类型推断 fun main() {/** 常规匿名函数写法&#xff1a…

2023年最新最全软件测试面试题大全

一、面试基础题 简述测试流程: 1、阅读相关技术文档&#xff08;如产品PRD、UI设计、产品流程图等&#xff09;。 2、参加需求评审会议。 3、根据最终确定的需求文档编写测试计划。 4、编写测试用例&#xff08;等价类划分法、边界值分析法等&#xff09;。 5、用例评审(…

哈希unordered系列介绍(上)

一.Unordered_map,Unordered_set介绍 在之前我们已经介绍过set,map,multiset等等关联式容器&#xff0c;它们的底层是红黑树进行模拟实现的&#xff0c;在查询时效率可达到 l o g 2 N log_2 N log2​N&#xff0c;即最差情况下需要比较红黑树的高度次&#xff0c;当树中的节点…

【Linux】IP协议——网络层

目录 IP协议 基本概念 IP协议格式 分片与组装 网段划分 特殊的IP地址 IP地址的数量限制 私网IP地址和公网IP地址 路由 路由表生成算法 IP协议 IP协议全称为“网际互连协议&#xff08;Internet Protocol&#xff09;”&#xff0c;IP协议是TCP/IP体系中的网络层协议…

【LangChain】Memory

概要 大多数LLM应用都有对话界面。对话的一个重要组成部分是能够引用对话中先前介绍的信息。至少&#xff0c;对话系统应该能够直接访问过去消息的某些窗口。更复杂的系统需要有一个不断更新的世界模型&#xff0c;这使得它能够执行诸如维护有关实体及其关系的信息之类的事情。…

【D3S】REST接口文档自动生成 - 集成smart-doc并同步配置到Torna

目录 一、引言二、maven插件三、smart-doc.json配置四、smart-doc-maven-plugin相关命令五、推送文档到Torna六、通过Maven Profile简化构建 一、引言 D3S&#xff08;DDD with SpringBoot&#xff09;为本作者使用DDD过程中开发的框架&#xff0c;目前已可公开查看源码&#…

Object.assign详解

一、Object.assign是什么&#xff1f; Object.assign( )方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 二、用法 Object.assign(target, ...sources) 参数&#xff1a;target ——>目标对象 source ——>源对象 返回值&#xff1a;…

使用生成式 AI 模仿人类行为

推荐&#xff1a;使用 NSDT场景编辑器 助你快速搭建可编辑的3D应用场景 这项研究被 2023 年学习表征国际会议 &#xff08;ICLR&#xff09; 接受&#xff0c;该会议致力于推进通常称为深度学习的人工智能分支。 图 1&#xff1a;我们的方法概述。 扩散模型已成为一类强大的生…