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

概述

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实现生产者-消费者问题(在后文并发实践篇会有相关示例)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

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

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

Jenkins 修改默认管理员帐号

1、新增一个新的超级管理员用户&#xff0c;并验证能正常登录 2、进入 Jenkins 用户管理目录&#xff1a; /data/software/jenkins/users 3、修改超级管理文件夹的名称为其他名称&#xff0c;如&#xff1a;mv admin_*** ifadm_*** 4、重启Jenkins容器

「C/C++」C/C++搭建程序框架

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「UG/NX」NX二次开发「UG/NX」BlockUI集合「VS」Visual Studio「QT」QT5程序设计「C/C」C/C程序设计「Win」Windows程序设计「DSA」数据结构与算法「File」数据文件格式 目录 术语介绍…

记录一下Java实体转json字段顺序问题

特殊需求&#xff0c;和C交互他们那边要求字段顺序要和他们定义的一致(批框架) 如下&#xff1a; Data public class UserDto {private String name;private Integer age;private String addr; }未转换前打印&#xff1a; 转换后打印&#xff1a; 可以看到转换为json顺序打印…

029 - integer types 整数类型

MySQL支持SQL标准整数类型 INTEGER&#xff08;或INT&#xff09;和 SMALLINT。作为一个可扩展标准&#xff0c;MySQL也支持整数类型 TINYINT&#xff0c;MEDIUMINT和 BIGINT。下表显示了每种整数类型所需的存储空间和范围。 表11.1 MySQL支持的整数类型的必需存储和范围 类型…

PLY模型格式详解【3D】

本文介绍PLY 多边形文件格式&#xff0c;这是一种用于存储被描述为多边形集合的图形对象。 PLY文件格式的目标是提供一种简单且易于实现但通用的格式足以适用于各种模型。 PLY有两种子格式&#xff1a;易于入门的 ASCII 表示形式和用于紧凑存储和快速保存和加载的二进制格式。 …

案例14 Spring MVC文件上传案例

基于Spring MVC实现文件上传&#xff1a; 使用commons-fileupload实现上传文件到本地目录。 实现上传文件到阿里云OSS和从阿里云OSS下载文件到本地。 1. 创建项目 选择Maven快速构建web项目&#xff0c;项目名称为case14-springmvc03。 ​ 2. 配置Maven依赖 <?xml ver…

点淘的MCN机构申请详细入驻指南!

消费趋势的变化&#xff0c;来自消费人群的变化。 后疫情时代&#xff0c;经济复苏的反弹力度不足&#xff0c;人们开始怀疑我们正从前几年的消费升级&#xff0c;跌入消费降级的时代&#xff0c;但这并不能准确概括消费市场的变化。 仔细翻看各大奢侈品集团的财报&#xff0…

nvm下载node导致npm报错无法使用

有个依赖库需要更新下node&#xff0c;用nvm下载后项目跑不起来了&#xff0c;npm -v 还报错 其实一开始是npm下载不来&#xff0c;然后换了淘宝镜像后还是报错 然后就只能手动下载下了 进入node.js官网 https://nodejs.org/en/download 下载后注意要安装在你nvm目录中&#x…

绕过 open_basedir

目录 0x01 首先了解什么是 open_basedir 0x02 通过命令执行绕过 0x03 通过symlink 绕过 &#xff08;软连接&#xff09; 0x04利用glob://绕过 方式1——DirectoryIteratorglob:// 方式2——opendir()readdir()glob:// 0x05 通过 ini_set和chdir来绕过 在ctfshow 72遇到…

实践|Linux 中查找和删除重复文件

动动发财的小手&#xff0c;点个赞吧&#xff01; 如果您习惯使用下载管理器从互联网上下载各种内容&#xff0c;那么组织您的主目录甚至系统可能会特别困难。 通常&#xff0c;您可能会发现您下载了相同的 mp3、pdf 和 epub&#xff08;以及各种其他文件扩展名&#xff09;并将…

OpenCV实例(九)基于深度学习的运动目标检测(一)YOLO运动目标检测算法

基于深度学习的运动目标检测&#xff08;一&#xff09; 1.YOLO算法检测流程2.YOLO算法网络架构3.网络训练模型3.1 训练策略3.2 代价函数的设定 2012年&#xff0c;随着深度学习技术的不断突破&#xff0c;开始兴起基于深度学习的目标检测算法的研究浪潮。 2014年&#xff0c;…

【Leetcode】155. 最小栈、JZ31 栈的压入、弹出序列

作者&#xff1a;小卢 专栏&#xff1a;《Leetcode》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 155. 最小栈 155. 最小栈 题目描述; 设计一个支持 push &#xff0c;pop &#xff0c;top …

【Java学习】System.Console使用

背景 在自学《Java核心技术卷1》的过程中看到了对System.Console的介绍&#xff0c;编写下列测试代码&#xff0c; public class ConsoleTest {public static void main(String[] args) {Console cs System.console();String name cs.readLine("AccountInfo: ");…

【正点原子STM32连载】 第二章 APM32简介摘自【正点原子】APM32F407最小系统板使用指南

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html# 第二…

excel将主信息和明细信息整理为多对多(每隔几行空白如何填充)

excel导出的数据是主信息和明细信息形式。 方法如下:1、首先&#xff0c;从第一个单元格开始选中要填充的数据区域。2、按CtrlG或者F5调出定位对话框&#xff0c;点击左下角的【定位条件】。3、在【定位条件】中选择【空值】&#xff0c;然后点击【确定】按钮。4、按照上述操作…

Vue输入框或者选择框无效,或者有延迟

问题剖析 使用Vue这种成熟好用的框架&#xff0c;一般出现奇奇怪怪的问题都是因为操作不当导致的&#xff0c;例如没有合理调用组件、组件位置不正确、没有合理定义组件或者变量、样式使用不当等等... 解决方案 如果你也出现了输入框输入东西&#xff0c;但是没有效果…

【idea】点击idea启动没反应

RT 点击idea启动的时候没反应&#xff0c;接着百度报错&#xff0c;基本跟他们的也不一样。 首先我是做版本升级。其次&#xff0c;我之前是破解的。如果你也是跟我一样的话&#xff0c;那问题可能就处在破解上了 解决方式 首先&#xff0c;是跟大部分解决思路一样。先找到项…

项目部署(前后端分离)

1、前端项目 &#xff08;打包成dist文件,放到nginx的html目录下面&#xff09;&#xff0c;然后配置nginx 2、后端项目部署 使用之前的shell脚本&#xff08;然后赋予用户权限&#xff09;&#xff0c;最后运行脚本 查看进程

网络安全 Day28-运维安全项目-加密隧道

运维安全项目-加密隧道 1. 加密隧道服务概述2. openVPN应用场景3. 虚拟机环境准备3.0 准备知识3.1 添加网卡![请添加图片描述](https://img-blog.csdnimg.cn/f155ca2804d84118b89a69da3688911e.png)3.2 配置内网&#xff08;LAN区段)3.3 虚拟机选择LAN区段3.4 书写eth1网卡配置…