Java多线程性能调优

news2024/9/21 18:56:13

Synchronized同步锁优化方法

1.6之前比较重量级,1.6后经过优化性能大大提升
使用Synchronized实现同步锁住要是两种方式:方法、代码块。

1.代码块
Synchronized在修饰同步代码块时,是由 monitorenter和monitorexit指令来实现同步的。进入monitorenter 指令后,线程将持有Monitor对象,退出monitorenter指令后,线程将释放该Monitor对象。

2.方法
当Synchronized修饰同步方法时,并没有发现monitorenter和monitorexit指令,而是出现了一个ACC_SYNCHRONIZED标志。这是因为JVM使用了ACC_SYNCHRONIZED访问标志来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查该方法是否被设置ACC_SYNCHRONIZED访问标志。如果设置了该标志,执行线程将先持有Monitor对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该Mointor对象,当方法执行完成后,再释放该Monitor对象。

JVM中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个Monitor,Monitor可以和对象一起创建、销毁。Monitor是由ObjectMonitor实现,而ObjectMonitor是由C++的ObjectMonitor.hpp文件实现。

当多个线程同时访问一段同步代码时,多个线程会先被存放在ContentionList和_EntryList 集合中,处于block状态的线程,都会被加入到该列表。接下来当线程获取到对象的Monitor时,Monitor是依靠底层操作系统的Mutex Lock来实现互斥的,线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex,竞争失败的线程会再次进入ContentionList被挂起。

如果线程调用wait() 方法,就会释放当前持有的Mutex,并且该线程会进入WaitSet集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放Mutex。
在这里插入图片描述

因为涉及到线程的阻塞和挂起等操作,这也是Synchronized比较重量级的原因。下面看看jdk源码是怎么进行优化的。

JDK1.6引入了偏向锁、轻量级锁、重量级锁概念,来减少锁竞争带来的上下文切换,而正是新增的Java对象头实现了锁升级功能。当Java对象被Synchronized关键字修饰成为同步锁后,围绕这个锁的一系列升级操作都将和Java对象头有关。对象头内容如下:
在这里插入图片描述
锁升级过程如下:
在这里插入图片描述

🌟🌟🌟一句话概括总结,通过一些方式去竞争锁,在竞争中逐渐提高锁的级别,代价也越来越大。一开始只需查询对象头,然后是CAS竞争,最后直接挂起阻塞线程。

锁的不同重量级对应着不同的场景,我们需要根据实际的业务情况去具体优化。

1.偏向锁主要用来优化同一线程多次申请同一个锁的竞争。在某些情况下,大部分时间是同一个线程竞争锁资源,例如,在创建一个线程并在线程中执行循环监听的场景下,或单线程操作一个线程安全集合时,同一线程每次都需要获取和释放锁,每次操作都会发生用户态与内核态的切换。

因此,在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生stop the word后, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加JVM参数关闭偏向锁来调优系统性能,示例代码如下:

-XX:-UseBiasedLocking //关闭偏向锁(默认打开)
或
-XX:+UseHeavyMonitors  //设置重量级锁

2.轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。

3.自旋锁和重量级锁:在锁竞争不激烈且锁占用时间非常短的场景下,自旋锁可以提高系统性能。一旦锁竞争激烈或锁占用的时间过长,自旋锁将会导致大量的线程一直处于CAS重试状态,占用CPU资源,反而会增加系统性能开销。所以自旋锁和重量级锁的使用都要结合实际场景。

在高负载、高并发的场景下,我们可以通过设置JVM参数来关闭自旋锁,优化系统性能,示例代码如下:

-XX:-UseSpinning //参数关闭自旋锁优化(默认打开) 
-XX:PreBlockSpin //参数修改默认的自旋次数。JDK1.7后,去掉此参数,由jvm控制

4.动态编译优化,JIT编译器对锁的粒度增大或减小。例如,几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁”所带来的性能开销。而粒度减小的典型案例就是JDK8之前的ConcurrentHashMap中用的Segment分段锁,减小锁粒度实现增大并发量,避免锁被升级为重量级锁。


Lock同步锁优化方法

和synchronized的对比
在这里插入图片描述
Lock是一个接口,AQS(AbstractQueuedSynchronizer)是一个抽象类。Lock锁是基于Java实现的锁,Lock是一个接口类,常用的实现类有ReentrantLock、ReentrantReadWriteLock(RRW),它们都是依赖AbstractQueuedSynchronizer(AQS)类实现的。

AQS类结构中包含一个基于链表实现的等待队列(CLH队列),用于存储所有阻塞的线程,AQS中还有一个state变量,该变量对ReentrantLock来说表示加锁状态。

该队列的操作均通过CAS操作实现,我们可以通过一张图来看下整个获取锁的流程。简而言之,通过CAS竞争和队首节点去获得锁。
在这里插入图片描述
锁分离优化Lock同步锁,默认的ReentrantLock是独占锁,在大部分业务场景中,读业务操作要远远大于写业务操作。而在多线程编程中,读操作并不会修改共享资源的数据,如果多个线程仅仅是读取共享资源,那么这种情况下其实没有必要对资源进行加锁。如果使用互斥锁,反倒会影响业务的并发性能,那么在这种场景下,有没有什么办法可以优化下锁的实现方式呢?

1.读写锁ReentrantReadWriteLock

RRW也是继承AQS实现,内部维护了两个锁读锁和写锁,实现的关键是将AQS的同步变量state分为高16位和低16位,分别表示读写。

2.读写锁再优化之StampedLock

RRW被很好地应用在了读大于写的并发场景中,然而RRW在性能上还有可提升的空间。在读取很多、写入很少的情况下,RRW会使写入线程遭遇饥饿(Starvation)问题,也就是说写入线程会因迟迟无法竞争到锁而一直处于等待状态。

在JDK1.8中,Java提供了StampedLock类解决了这个问题。StampedLock不是基于AQS实现的,但实现的原理和AQS是一样的,都是基于队列和锁状态实现的。与RRW不一样的是,StampedLock控制锁有三种模式: 写、悲观读以及乐观读,并且StampedLock在获取锁时会返回一个票据stamp,获取的stamp除了在释放锁时需要校验,在乐观读模式下,stamp还会作为读取共享资源后的二次校验,后面我会讲解stamp的工作原理。

我们先通过一个官方的例子来了解下StampedLock是如何使用的,代码如下:

public class Point {
    private double x, y;
    private final StampedLock s1 = new StampedLock();

    void move(double deltaX, double deltaY) {
        //获取写锁
        long stamp = s1.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            //释放写锁
            s1.unlockWrite(stamp);
        }
    }

    double distanceFormOrigin() {
        //乐观读操作
        long stamp = s1.tryOptimisticRead();  
        //拷贝变量
        double currentX = x, currentY = y;
        //判断读期间是否有写操作
        if (!s1.validate(stamp)) {
            //升级为悲观读
            stamp = s1.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                s1.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

我们可以发现:一个写线程获取写锁的过程中,首先是通过WriteLock获取一个票据stamp,WriteLock是一个独占锁,同时只有一个线程可以获取该锁,当一个线程获取该锁后,其它请求的线程必须等待,当没有线程持有读锁或者写锁的时候才可以获取到该锁。请求该锁成功后会返回一个stamp票据变量,用来表示该锁的版本,当释放该锁的时候,需要unlockWrite并传递参数stamp。

接下来就是一个读线程获取锁的过程。首先线程会通过乐观锁tryOptimisticRead操作获取票据stamp ,如果当前没有线程持有写锁,则返回一个非0的stamp版本信息。线程获取该stamp后,将会拷贝一份共享资源到方法栈,在这之前具体的操作都是基于方法栈的拷贝数据。

之后方法还需要调用validate,验证之前调用tryOptimisticRead返回的stamp在当前是否有其它线程持有了写锁,如果是,那么validate会返回0,升级为悲观锁;否则就可以使用该stamp版本的锁对数据进行操作。

相比于RRW,StampedLock获取读锁只是使用与或操作进行检验,不涉及CAS操作,即使第一次乐观锁获取失败,也会马上升级至悲观锁,这样就可以避免一直进行CAS操作带来的CPU占用性能的问题,因此StampedLock的效率更高。


乐观锁优化并行操作


优化多线程上下文切换


并发容器的选择


线程池的设置


协程的使用

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

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

相关文章

mysql的事务,你弄懂了吗?(Innodb)

目录 1.事务的ACID原则 2. 事务的隔离级别 2.1 数据库的脏读问题 2.2 数据库不可重复读问题 2.3 数据库幻读问题 2.4 数据库脏写问题 3.Mysql的锁 3.1 以锁粒度的维度划分 3.2 以互斥性的维度划分: 3.3 以操作类型的维度划分: 3.4 以加锁方式…

树的概念与二叉树的实现

目录 一. 树的概念 二. 访问树的方法 1. 左孩子右兄弟法 2. 双亲表示法 3. 顺序表存孩子的指针(孩子表示法) 三. 二叉树 1. 二叉树的定义 2. 特殊二叉树 3. 二叉树的性质 4. 存储方式 四. 二叉树的前中后序遍历 1. 前序遍历 2. 中序遍历 3. …

C 语言中如何实现图结构?

🍅关注博主🎗️ 带你畅游技术世界,不错过每一次成长机会! 📙C 语言百万年薪修炼课程 【https://dwz.mosong.cc/cyyjc】通俗易懂,深入浅出,匠心打磨,死磕细节,6年迭代&…

基于docker-compose部署zabbix7.0

1.安装docker和docker-compose 已有可跳过,没有参照我的docker一件安装脚本连接放在下方 一键安装dockerv24.0.6以及docker-compose可离线_docker 24对应docker-compose-CSDN博客 2.运行zabbix-server 1.创建zabbix工作目录 mkdir /zabbix 2.编写docker-compos…

【人工智能】Transformers之Pipeline(一):音频分类(audio-classification)

​​​​​​​ 目录 一、引言 二、音频分类(audio-classification) 2.1 概述 2.2 技术原理 2.2.1 Wav2vec 2.0模型 2.2.1 HuBERT模型 2.3 pipeline参数 2.3.1 pipeline对象实例化参数 2.3.2 pipeline对象使用参数 2.4 pipeline实战 2.4.1 …

【python】Python中常见的KeyError报错分析

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…

ESP32FreeRTOS开发笔记:1.双核并行

ESP32 的 Arduino 框架内部集成了 FreeRTOS,允许开发者利用其多任务处理功能。在代码中,xTaskCreatePinnedToCore 函数是 FreeRTOS 提供的 API,用于创建任务并指定任务在哪个核心上运行。 FreeRTOS 是一个流行的实时操作系统内核,…

信息打点web篇--语言开发框架,组件识别

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本章节主要整理 识别语言开发框的打点内容 框架简介 高效理解:把用于做某些事的代码封装起来,使用者无需自己写代码直接一个函数就能完成本该很多行才能完成的功能。 例子:我们要写网站,…

Open3D 点云Kmeans聚类算法

目录 一、概述 1.1算法介绍 1.2实现步骤 二、代码实现 三、实现效果 3.1原始点云 3.2聚类后点云 前期试读,后续会将博客加入该专栏,欢迎订阅Open3D与点云深度学习的应用_白葵新的博客-CSDN博客 一、概述 1.1算法介绍 聚类是一种将数据集分组的方…

Qml 图片和加载器操作

学习目标:Qml 图片和加载器编程 学习前置 Qt Qml编程 基础部分 认识qml-CSDN博客 实现效果 对图片的基本操作 加载器 核心代码 加载器 思路: 创建一个加载器 默认是几个圆点,我们重写加载器元素(contentItem)&…

文献阅读:高效和稳健的 π-FISH rainbow 用于多种生物分子的多重原位检测

文献介绍 文献题目: Highly efficient and robust π-FISH rainbow for multiplexed in situ detection of diverse biomolecules 研究团队: 曹罡(华中农业大学)、戴金霞(华中农业大学) 发表时间&#xff…

RSA算法(C++)

RSA加解密过程 RSA为非对称加密算法,由一对公钥和一对私钥构成,私钥加密公钥解密,公钥加密私钥解密 如下图,D为私密的,假设传输英文字母,我们给英文字母编号A1,B2,C3… RSA加解密过程 两对密钥产生方法如下 C Op…

网络通信基本知识

网络通信 什么是网络通信? 通信网络是指将各个孤立的设备进行物理连接,实现人与人,人与计算机,计算机与计算机之间进行信息交换的链路,从而达到资源共享和通信的目的。 什么是网络协议? 网络协议是计算机…

Python函数 之 参数

1.参数的简单介绍 参数 形式参数(形参):在函数定义的时候,在括号中写⼊变量,这个变量就称为是函数的参数。实际参数(实参):在函数调⽤的时候,可以给定义时候的形参传递具体的数据值,供其使⽤。注: 在函数调⽤的时候,会将函数的实…

wps 将列的内容转换为一个单元格内容,并以逗号分隔

wps 将列的内容转换为一个单元格内容,并以逗号分隔 1.首先在一个空白单元格输入 2.输入函数TEXTJOIN 这个函数有三个参数,第一个:以什么分隔符分隔,第二个:是否忽略空白格,true:忽略 false:不忽略 第三个&…

怎么使用代理IP?如何检测代理IP是否有效?

代理IP是一种网络代理技术,它是通过中间服务器来转发网络请求的IP地址。当我们使用代理IP时,我们的真实IP地址会被隐藏起来,而代理服务器的IP地址会被用作我们的身份标识。使用代理IP的步骤如下: 1.选择合适的代理服务器 考虑服务…

【微信小程序知识点】转发功能的实现

转发功能,主要帮助用户更流畅地与好友分享内容与服务。 想实现转发功能,有两种方式: 1.页面js文件必须声明onShareAppMessage事件监听函数,并自定义转发内容。只有定义了此事件处理函数,右上角菜单才会显示“转发”按…

WebStorm 使用 ESLint 自动格式化代码

WebStorm 不能像 VSCode 那样在保存的时候自动 Fix-ESLint,不能自动格式化代码,需要安装一个插件 安装 ESLint 插件 进入设置快捷键 win:CtrlAltS mac: command, 找到 Plugins,搜索eslint 在这里插入图片描述 安装后配置一下 …

【通信协议-RTCM】MSM语句(1) - 多信号GNSS观测数据消息格式

注释: RTCM响应消息1020为GLONASS星历信息,暂不介绍,前公司暂未研发RTCM消息类型版本的DR/RTK模块,DR/RTK模块仅NMEA消息类型使用 注释: 公司使用的多信号语句类型为MSM4&MSM7,也应该是运用最广泛的语句…

从新手到进阶:高效设计 Tableau 可视化的 5 种技巧 | 数据可视化分析

让我们一起跟着大神学习五个超实用的技巧,加速你的可视化分析之旅! 在日常分析中,人人都想实现可视化最佳实践。然而,对于很多初学者来说,在还未熟练掌握 Tableau 的情况下,这种愿望貌似不太符合实际。 为…