JDK高并发——锁的优化

news2024/9/9 6:01:12

提高锁性能

减少锁持有时间

在锁竞争过程中,单个线程对锁的持有时间和系统性能有直接的关系。如果线程持有锁的时间越长,相对的,锁的竞争程度越激烈。

public sychronized void syncMethod(){
  othercode1();
  mutexMethod();
  othercode2();
}

假设方法中只有mutexMethod需要同步控制,则在并发量大的时候,使用上面的同步方法,则会导致等待线程的大量增加。

通过优化减少线程持有锁的时间,提高系统的吞吐量。

改进方法:

public void syncMethod2(){
 othercode1();
 sychronized(this){
  mutexMethod();
}
othercode2();
}

通过改进方法,只对mutexMethod方法做同步,锁占用的时间相对较短,因此能有更高的并行度。

减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。

减小锁粒度

对于HashMap来说,最重要的两个方法就是get()和put()。一种最自然的想法就是,对整个HashMap加锁从而得到一个线程安全的对象,但是,这样做加锁的粒度过大。

ConcurrentHashMap内部进一步细分了若干个小的HashMap,称为segment,在默认情况下,一个ConcurrentHashMap类可以细分为16个段。

当需要在ConcurrentHashMap类中增加一个新的项,并不是将整个HashMap加锁,而是首先根据hashcode的到该项应该被存放到哪个段中,然后对该段加锁,并完成put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要被加入的项不存放在同一个段中,线程间便可以做到真正的并行。

但是,减小锁粒度会带来一个新的问题,即当系统需要取得全局锁时,消耗的资源会比较多。例如,concurrentHashMap在put()时很好的分离了锁,但是试图访问concurrentHashMap的全局信息时,如size()就需要同时取得所有段的锁才能顺利实施。

for(int i=0;i<segments.length;i++){
 segments[i].lock();
}
for(int i=0;i<segments.length;i++){
 sum+=segments[i].count;
}
for(int i=0;i<segments.length;i++){
 segments[i].unlock();
}

所谓减小锁粒度,就是指缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力。

用读写分离锁来替换独占锁

使用读写分离锁ReadWriteLock可以提高系统的性能,使用读写分离锁来替代独占锁是减小锁粒度的一种特殊情况。如果说减小锁粒度是通过分割数据结构实现的,那么读写分离锁是对系统功能点的分割。

在读多写少的场合使用读写锁可以有效提升系统的并发能力。

锁分离

将读写锁的思想进一步延伸,就是锁分离。读写锁根据读写操作功能上的不同,进行了有效的锁分离。依据应用程序的功能特点,使用类似的分离思想,也可以对独占锁进行分离。一个典型的案例就是LinkedBlockingQueue的实现。

在LinkedBlockingQueue实现中,take()和put()分别实现了从队列中取得数据和往队列中增加数据的功能。虽然两个函数都是对当前队列进行修改操作,但是 LinkedBlockingQueue是基于链表的,因此两个操作分别作用于队列的尾端和前端。

如果使用独占锁,则要求在两个操作进行时获取当前队列的独占锁,那么take()方法和put()方法就不能真正的并发,在运行时,他们会彼此等待对方释放锁资源。在这种情况下,锁竞争会相对激烈,从而影响程序在高并发时的性能。

  1.  private final ReentrantLock takeLock = new ReentrantLock(); 
    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition(); 

    /** Lock held by put, offer, etc */
  2.  private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();
  1. take方法需要持有takeLock
  2. put方法需要持有putLock

take()方法实现

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        //不能有两个线程同时take
        takeLock.lockInterruptibly();
        try {
		   // 循环等待判断,防止出现虚假唤醒。
            while (count.get() == 0) {
            // 如果当前没有数据,则等待
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
            // 取出数据后,容量仍然大于1,通知其他等待take的线程
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
        	// 通知等待put的线程,可以进行put操作。
            signalNotFull();
        return x;
    }

put()方法实现

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);

        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //获取putLock,同一时间只能有一个put操作。
        putLock.lockInterruptibly();
        try {
           // 当容量已满时,put线程阻塞
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
            //放入后,容量小于capacity,唤醒其他等待put的线程
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
        // 放入前,容量为空,现在放入了一个元素,可以唤醒等待take的线程
            signalNotEmpty();
    }

通过takeLock和putLock两把锁,LinkedBlockingQueue实现了取数据和写数据的分离,使两者在真正意义上成为可并发的操作。

锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,如果对同一个锁不停的进行请求、同步、释放,其本身也会消耗系统的宝贵资源,反而不利于性能的优化。

为此,虚拟化在遇到一连串连续的对同一个锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步的次数,这个操作称为锁粗化。

在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其是在循环中请求锁时,如下:

for(int i=0;i<n;i++){
 sychronized(lock){
  //do sth....
 }
}

可以优化成下面的结构:

sychronized(lock){
for(int i=0;i<n;i++){
  //do sth....
 }
}

JVM锁优化

偏向锁

核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作。这样就节省了大量有关锁申请的操作。因此,对于几乎没有锁竞争的场合,偏向锁有比较好的效果,因为连读多次极有可能是同一个线程请求相同的锁。而对于锁竞争比较激烈的场合,最有可能是每次都是不同的线程来请求相同的锁,这样偏向模式会失效,因此还不如不启用偏向锁。

轻量级锁

如果偏向锁失败,那么虚拟机并不会立即挂起线程,他还会使用一种称为轻量级锁的优化手段。轻量级锁的操作,只是将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁,如果线程获得轻量级锁成功,则可以顺利进入临界区,如果轻量级锁加锁失败,则表示其他线程优先抢到了锁,那么当前线程的锁请求就会膨胀为重量级锁。

自旋锁

锁膨胀后,为了避免线程真实的在操作系统层面挂起,虚拟机还会做最后的努力——自旋锁,当前线程暂时无法获得锁,而且什么时候可以获得锁是一个未知数,系统会假设在不久的将来,线程就可以得到这把锁。因此,虚拟机会让当前线程做几个空循环,再经过若干次循环后,如果可以得到锁,那么就顺利进入临界区,如果还不能获得锁,将在操作系统层面挂起。

锁消除

锁消除是一种更彻底的优化,Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。

如下:

public String createStrings(){
 Vector<String> v = new Vector<String>();
 for(int i=0;i<100;i++){
  v.add(Integer.toString(i));
 }
 return v.toArray(new String[]{});
}

代码中,使用到Vector的局部变量,局部变量是在线程方法栈上分配,属于线程私有的数据,因此不可能存在其他线程访问。在这种情况下,Vector内部所有加锁的同步都是没有必要的,如果虚拟机检测到这种情况,就会将这些无用的锁操作去除。

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

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

相关文章

高等数学 第七讲 一元函数积分学的概念和性质_不定积分_定积分_变限积分_反常积分

1.不定积分 文章目录 1.不定积分1.1 原函数1.1.1 原函数与不定积分的定义1.1.2 原函数存在定理 2.定积分2.1 定积分的定义2.2 定积分的精确定义2.3 定积分的几何意义2.4 定积分的存在定理2.5 定积分的性质 3.变限积分3.1 变限积分的定理3.2 变限积分的性质 4.反常积分(待更新) …

springboot集团门户网站--论文源码调试讲解

第2章 开发环境与技术 开发集团门户网站需要搭建编程的环境&#xff0c;也需要通过调查&#xff0c;对各个相关技术进行分析&#xff0c;选取适合本系统开发的技术与工具。 2.1 MySQL数据库 MySQL是一种具有安全系数、安全系数、混合开发性、高效化等特征的轻量关联数据库智…

2024.07纪念一 debezium : spring-boot结合debezium

使用前提&#xff1a; 一、mysql开启了logibin 在mysql的安装路径下的my.ini中 【mysqlid】下 添加 log-binmysql-bin # 开启 binlog binlog-formatROW # 选择 ROW 模式 server_id1 # 配置 MySQL replaction 需要定义&#xff0c;不要和 canal 的 slaveId 重复 参考gitee的项目…

mysql超大分页问题处理~

大家好&#xff0c;我是程序媛雪儿&#xff0c;今天咱们聊mysql超大分页问题处理。 超大分页问题是什么&#xff1f; 数据量很大的时候&#xff0c;在查询中&#xff0c;越靠后&#xff0c;分页查询效率越低 例如 select * from tb_sku limit 0,10; select * from tb_sku lim…

专治408开始的晚!8月一定要完成这些事!

八月份才开始408&#xff0c;那到考试最多也只有4-5个月的时间 别担心&#xff0c;可以复习两轮&#xff01; 其实我一直建议大家408复习三轮&#xff0c;但是如果时间不够&#xff0c;那就要在复习质量上下功夫&#xff01; 考408有一个好处&#xff0c;就是不用先确定学校…

【错误总结】Ubuntu系统中执行 sudo apt-get update报错

Ubuntu系统中执行 sudo apt-get update报错 命令行描述升级sudo报错并解决错误描述错误解决原因1&#xff1a;系统网络问题 原因2&#xff1a;设置清华源后/etc/apt/sources.list不匹配原因3&#xff1a;ubuntu自带的源/etc/apt/sources.list有问题 apt-get update成功log参考 …

【Story】《程序员面试的“八股文”辩论:技术基础与实际能力的博弈》

目录 程序员面试中的“八股文”&#xff1a;助力还是阻力&#xff1f;1. “八股文”的背景与定义1.1 “八股文”的起源1.2 “八股文”的常见类型 2. “八股文”的作用分析2.1 理论基础的评价2.1.1 助力2.1.2 阻力 3. 实际工作能力的考察3.1 助力3.2 阻力 4. 面试中的背题能力4.…

利用代理IP助力社媒营销的指南来了!

文章目录 前言一、有效数据收集二、建立流量矩阵三、精准定制内容选择正确的代理类型定时监测和更新代理IP遵守平台政策 总结 前言 在当今数字化时代&#xff0c;社交媒体营销已成为企业推广品牌、增强用户互动不可或缺的一环。从本质上看&#xff0c;社媒营销是公共关系和客户…

借助 NGINX 对本地的 Kubernetes 服务进行自动化的 TCP 负载均衡

原文作者&#xff1a;Chris Akker - F5 技术解决方案架构师&#xff0c;Steve Wagner - F5 NGINX 解决方案架构师 原文链接&#xff1a;借助 NGINX 对本地的 Kubernetes 服务进行自动化的 TCP 负载均衡 转载来源&#xff1a;NGINX 中文官网 NGINX 唯一中文官方社区 &#xff0c…

Windows11安装MongoDB7.0.12详细教程

下载 地址&#xff1a;https://www.mongodb.com/try/download/community 我使用的是迅雷下载&#xff1a; 安装 选择自定义安装&#xff1a; 选择安装目录&#xff1a; 开始安装&#xff1a; 这个玩意会卡比较长的时间&#xff1a; 最后这一步如果没有科学上网&#…

虾皮笔试0620-选择题

虚拟存储的基础是程序局部性理论&#xff0c;它的基本含义是程序执行时对内存访问的不均匀性。这一理论具体体现在两个方面&#xff1a; 时间局部性&#xff1a;时间局部性是指如果程序中的某个数据项被访问&#xff0c;那么在不久的将来它可能再次被访问。这通常是因为程序存在…

thinkphp框架远程代码执行

一、环境 vulfocus网上自行下载 启动命令&#xff1a; docker run -d --privileged -p 8081:80 -v /var/run/docker.sock:/var/run/docker.sock -e VUL_IP192.168.131.144 8e55f85571c8 一定添加--privileged不然只能拉取环境首页不显示 二、thinkphp远程代码执行 首页&a…

【吊打面试官系列-Dubbo面试题】Dubbo SPI 和 Java SPI 区别?

大家好&#xff0c;我是锋哥。今天分享关于 【Dubbo SPI 和 Java SPI 区别&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; Dubbo SPI 和 Java SPI 区别&#xff1f; JDK SPI JDK 标准的 SPI 会一次性加载所有的扩展实现&#xff0c;如果有的扩展吃实话很耗时&…

Javascript前端面试基础(九)

浏览器缓存 浏览器缓存分为强缓存和协商缓存。当客户端请求某个资源时&#xff0c;获取缓存的流程如下 先根据这个资源的一些http header判断它是否命中强缓存&#xff0c;如果命中则直接从本地获取缓存资源&#xff0c;不会发请求到服务器;当强缓存没有命中时&#xff0c;客户…

通过进程协作显示图像-C#

前言 如果一个软件比较复杂或者某些情况下需要拆解&#xff0c;可以考试将软件分解成两个或多个进程&#xff0c;但常规的消息传递又不能完全够用&#xff0c;使用消息共享内存&#xff0c;实现图像传递&#xff0c;当然性能这个方面我并没有测试&#xff0c;仅是一种解决思路…

Anaconda配置记录-linux环境

Anaconda Distribution 是一个 Python/R 数据科学分发&#xff0c;其中包含&#xff1a; conda - 用于命令行界面的包和环境管理器 Anaconda Navigator - 基于 conda 构建的桌面应用程序&#xff0c;具有从托管环境中启动其他开发应用程序的选项 超过 300 个自动安装的软件包…

记录一次Dump文件分析之旅

背景 在生产环境中&#xff0c;服务运行一段时间后&#xff0c;我们遇到了JVM内存使用率超过90%的告警。考虑到我们的服务正常情况下每周都会进行重启&#xff0c;通常不应该出现如此高的内存使用率问题。 前置操作 在检查JVM相关配置时&#xff0c;我们使用Jinfo命令发现当…

Covalent 启动面向 CXT 质押者的生态伙伴空投计划

Covalent Network&#xff08;CXT&#xff09;是模块化人工智能数据基础设施&#xff0c;其宣布了合作伙伴生态系统空投计划的首个项目&#xff1a;TAIKO。此举旨在为 CXT 代币质押者提供来自其庞大生态系统的空投机会。首次空投将于 2024 年 8 月 1 日进行&#xff0c;向 CXT …

疯狂交互学习的BM3推荐算法(论文复现)

疯狂交互学习的BM3推荐算法&#xff08;论文复现&#xff09; 本文所涉及所有资源均在传知代码平台可获取 文章目录 疯狂交互学习的BM3推荐算法&#xff08;论文复现&#xff09;多模态推荐系统优点 示例对比学习什么是对比学习&#xff1f;关键思想优点 自监督学习什么是自监督…

【只出现一次的数字 III】python刷题记录

R2-位运算专题. 目录 哈希表 位运算 ps: 一眼哈希表啊 哈希表 class Solution:def singleNumber(self, nums: List[int]) -> List[int]:dictdefaultdict(int)ret[]for num in nums:dict[num]1for key in dict.keys():if dict[key]1:ret.append(key)return ret怎么用位…