Java性能优化(七)-多线程调优-并发容器的使用

本文涉及的产品
云原生大数据计算服务MaxCompute,500CU*H 100GB 3个月
简介: Java性能优化(七)-多线程调优-并发容器的使用
  • 作者简介:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名
  • ❤️觉得文章还不错的话欢迎大家点赞👍➕收藏⭐️➕评论,💬支持博主,记得点个大大的关注,持续更新🤞
    ————————————————-

并发容器的Map

业务场景

假设我们现在要给一个电商系统设计一个简单的统计商品销量TOP 10的功能。常规情况下,我们是用一个哈希表来存储商品和销量键值对,然后使用排序获得销量前十的商品。在这里,哈希表是实现该功能的关键。那么请思考一下,如果要你设计这个功能,你会使用哪个容器呢?

使用HashMap可以吗?

答案是不可以,我们切忌在并发场景下使用HashMap。因为在JDK1.7之前,在并发场景下使用HashMap会出现死循环,从而导致CPU使用率居高不下,而扩容是导致死循环的主要原因。虽然Java在JDK1.8中修复了HashMap扩容导致的死循环问题,但在高并发场景下,依然会有数据丢失以及不准确的情况出现。

这时为了保证容器的线程安全,Java实现了Hashtable、ConcurrentHashMap以及ConcurrentSkipListMap等Map容器。

其他的Map

Hashtable、ConcurrentHashMap是基于HashMap实现的,对于小数据量的存取比较有优势。

ConcurrentSkipListMap是基于TreeMap的设计原理实现的,略有不同的是前者基于跳表实现,后者基于红黑树实现,ConcurrentSkipListMap的特点是存取平均时间复杂度是O(log(n)),适用于大数据量存取的场景,最常见的是基于跳跃表实现的数据量比较大的缓存。

回归到开始的案例再看一下,如果这个电商系统的商品总量不是特别大的话,我们可以用Hashtable或ConcurrentHashMap来实现哈希表的功能。

Hashtable 🆚 ConcurrentHashMap

更精准的话,我们可以进一步对比看看以上两种容器。

在数据不断地写入和删除,且不存在数据量累积以及数据排序的场景下,我们可以选用Hashtable或ConcurrentHashMap。

Hashtable使用Synchronized同步锁修饰了put、get、remove等方法,因此在高并发场景下,读写操作都会存在大量锁竞争,给系统带来性能开销。

相比Hashtable,ConcurrentHashMap在保证线程安全的基础上兼具了更好的并发性能。在JDK1.7中,ConcurrentHashMap就使用了分段锁Segment减小了锁粒度,最终优化了锁的并发操作。

到了JDK1.8,ConcurrentHashMap做了大量的改动,摒弃了Segment的概念。由于Synchronized锁在Java6之后的性能已经得到了很大的提升,所以在JDK1.8中,Java重新启用了Synchronized同步锁,通过Synchronized实现HashEntry作为锁粒度。这种改动将数据结构变得更加简单了,操作也更加清晰流畅。

与JDK1.7的put方法一样,JDK1.8在添加元素时,在没有哈希冲突的情况下,会使用CAS进行添加元素操作;如果有冲突,则通过Synchronized将链表锁定,再执行接下来的操作。

综上所述,我们在设计销量TOP10功能时,首选ConcurrentHashMap。

但要注意一点,虽然ConcurrentHashMap的整体性能要优于Hashtable,但在某些场景中,ConcurrentHashMap依然不能代替Hashtable。例如,在强一致的场景中ConcurrentHashMap就不适用,原因是ConcurrentHashMap中的get、size等方法没有用到锁,ConcurrentHashMap是弱一致性的,因此有可能会导致某次读无法马上获取到写入的数据。

ConcurrentHashMap 🆚 ConcurrentSkipListMap

我们再看一个案例,我上家公司的操作系统中有这样一个功能,提醒用户手机卡实时流量不足。主要的流程是服务端先通过虚拟运营商同步用户实时流量,再通过手机端定时触发查询功能,如果流量不足,就弹出系统通知。

该功能的特点是用户量大,并发量高,写入多于查询操作。这时我们就需要设计一个缓存,用来存放这些用户以及对应的流量键值对信息。那么假设让你来实现一个简单的缓存,你会怎么设计呢?

你可能会考虑使用ConcurrentHashMap容器,但我在07讲中说过,该容器在数据量比较大的时候,链表会转换为红黑树。红黑树在并发情况下,删除和插入过程中有个平衡的过程,会牵涉到大量节点,因此竞争锁资源的代价相对比较高。

而跳跃表的操作针对局部,需要锁住的节点少,因此在并发场景下的性能会更好一些。你可能会问了,在非线程安全的Map容器中,我并没有看到基于跳跃表实现的SkipListMap呀?这是因为在非线程安全的Map容器中,基于红黑树实现的TreeMap在单线程中的性能表现得并不比跳跃表差。

因此就实现了在非线程安全的Map容器中,用TreeMap容器来存取大数据;在线程安全的Map容器中,用SkipListMap容器来存取大数据。

那么ConcurrentSkipListMap是如何使用跳跃表来提升容器存取大数据的性能呢?我们先来了解下跳跃表的实现原理。

什么是跳跃表

跳跃表是基于链表扩展实现的一种特殊链表,类似于树的实现,跳跃表不仅实现了横向链表,还实现了垂直方向的分层索引。

一个跳跃表由若干层链表组成,每一层都实现了一个有序链表索引,只有最底层包含了所有数据,每一层由下往上依次通过一个指针指向上层相同值的元素,每层数据依次减少,等到了最顶层就只会保留部分数据了。

跳跃表的这种结构,是利用了空间换时间的方法来提高了查询效率。程序总是从最顶层开始查询访问,通过判断元素值来缩小查询范围。我们可以通过以下几张图来了解下跳跃表的具体实现原理。

首先是一个初始化的跳跃表:

当查询key值为9的节点时,此时查询路径为:

当新增一个key值为8的节点时,首先新增一个节点到最底层的链表中,根据概率算出level值,再根据level值新建索引层,最后链接索引层的新节点。新增节点和链接索引都是基于CAS操作实现。

当删除一个key值为7的结点时,首先找到待删除结点,将其value值设置为null;之后再向待删除结点的next位置新增一个标记结点,以便减少并发冲突;然后让待删结点的前驱节点直接越过本身指向的待删结点,直接指向后继结点,中间要被删除的结点最终将会被JVM垃圾回收处理掉;最后判断此次删除后是否导致某一索引层没有其它节点了,并视情况删除该层索引 。

小结

通过以上两个案例,我想你应该清楚了Hashtable、ConcurrentHashMap以及ConcurrentSkipListMap这三种容器的适用场景了。

如果对数据有强一致要求,则需使用Hashtable;在大部分场景通常都是弱一致性的情况下,使用ConcurrentHashMap即可;如果数据量在千万级别,且存在大量增删改操作,则可以考虑使用ConcurrentSkipListMap。

并发场景下的List容器

下面我们再来看一个实际生产环境中的案例。在大部分互联网产品中,都会设置一份黑名单。例如,在电商系统中,系统可能会将一些频繁参与抢购却放弃付款的用户放入到黑名单列表。想想这个时候你又会使用哪个容器呢?

首先用户黑名单的数据量并不会很大,但在抢购中需要查询该容器,快速获取到该用户是否存在于黑名单中。其次用户ID是整数类型,因此我们可以考虑使用数组来存储。那么ArrayList是否是你第一时间想到的呢?

我讲过ArrayList是非线程安全容器,在并发场景下使用很可能会导致线程安全问题。这时,我们就可以考虑使用Java在并发编程中提供的线程安全数组,包括Vector和CopyOnWriteArrayList。

Vector&CopyOnWriteArrayList

Vector也是基于Synchronized同步锁实现的线程安全,Synchronized关键字几乎修饰了所有对外暴露的方法,所以在读远大于写的操作场景中,Vector将会发生大量锁竞争,从而给系统带来性能开销。

相比之下,CopyOnWriteArrayList是java.util.concurrent包提供的方法,它实现了读操作无锁,写操作则通过操作底层数组的新副本来实现,是一种读写分离的并发策略。我们可以通过以下图示来了解下CopyOnWriteArrayList的具体实现原理。

回到案例中,我们知道黑名单是一个读远大于写的操作业务,我们可以固定在某一个业务比较空闲的时间点来更新名单。

这种场景对写入数据的实时获取并没有要求,因此我们只需要保证最终能获取到写入数组中的用户ID就可以了,而CopyOnWriteArrayList这种并发数组容器无疑是最适合这类场景的了。

总结

在并发编程中,我们经常会使用容器来存储数据或对象。Java在JDK1.1到JDK1.8这个漫长的发展过程中,依据场景的变化实现了同类型的多种容器。我将今天的主要内容为你总结了一张表格,希望能对你有所帮助,也欢迎留言补充。

欢迎一键三连(关注+点赞+收藏),技术的路上一起加油!!!代码改变世界

  • 关于我:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法公众号同名),回复暗号,更能获取学习秘籍和书籍等
相关实践学习
基于MaxCompute的热门话题分析
本实验围绕社交用户发布的文章做了详尽的分析,通过分析能得到用户群体年龄分布,性别分布,地理位置分布,以及热门话题的热度。
SaaS 模式云数据仓库必修课
本课程由阿里云开发者社区和阿里云大数据团队共同出品,是SaaS模式云原生数据仓库领导者MaxCompute核心课程。本课程由阿里云资深产品和技术专家们从概念到方法,从场景到实践,体系化的将阿里巴巴飞天大数据平台10多年的经过验证的方法与实践深入浅出的讲给开发者们。帮助大数据开发者快速了解并掌握SaaS模式的云原生的数据仓库,助力开发者学习了解先进的技术栈,并能在实际业务中敏捷的进行大数据分析,赋能企业业务。 通过本课程可以了解SaaS模式云原生数据仓库领导者MaxCompute核心功能及典型适用场景,可应用MaxCompute实现数仓搭建,快速进行大数据分析。适合大数据工程师、大数据分析师 大量数据需要处理、存储和管理,需要搭建数据仓库?学它! 没有足够人员和经验来运维大数据平台,不想自建IDC买机器,需要免运维的大数据平台?会SQL就等于会大数据?学它! 想知道大数据用得对不对,想用更少的钱得到持续演进的数仓能力?获得极致弹性的计算资源和更好的性能,以及持续保护数据安全的生产环境?学它! 想要获得灵活的分析能力,快速洞察数据规律特征?想要兼得数据湖的灵活性与数据仓库的成长性?学它! 出品人:阿里云大数据产品及研发团队专家 产品 MaxCompute 官网 https://www.aliyun.com/product/odps 
相关文章
|
2月前
|
自然语言处理 Java 关系型数据库
Java|小数据量场景的模糊搜索体验优化
在小数据量场景下,如何优化模糊搜索体验?本文分享一个简单实用的方案,虽然有点“土”,但效果还不错。
43 0
|
16天前
|
算法 Java 调度
Java多线程基础
本文主要讲解多线程相关知识,分为两部分。第一部分涵盖多线程概念(并发与并行、进程与线程)、Java程序运行原理(JVM启动多线程特性)、实现多线程的两种方式(继承Thread类与实现Runnable接口)及其区别。第二部分涉及线程同步(同步锁的应用场景与代码示例)及线程间通信(wait()与notify()方法的使用)。通过多个Demo代码实例,深入浅出地解析多线程的核心知识点,帮助读者掌握其实现与应用技巧。
|
8天前
|
机器学习/深度学习 监控 算法
局域网行为监控软件 C# 多线程数据包捕获算法:基于 KMP 模式匹配的内容分析优化方案探索
本文探讨了一种结合KMP算法的多线程数据包捕获与分析方案,用于局域网行为监控。通过C#实现,该系统可高效检测敏感内容、管理URL访问、分析协议及审计日志。实验表明,相较于传统算法,KMP在处理大规模网络流量时效率显著提升。未来可在算法优化、多模式匹配及机器学习等领域进一步研究。
20 0
|
2月前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
95 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
|
1月前
|
Java
java 多线程异常处理
本文介绍了Java中ThreadGroup的异常处理机制,重点讲解UncaughtExceptionHandler的使用。通过示例代码展示了当线程的run()方法抛出未捕获异常时,JVM如何依次查找并调用线程的异常处理器、线程组的uncaughtException方法或默认异常处理器。文章还提供了具体代码和输出结果,帮助理解不同处理器的优先级与执行逻辑。
|
25天前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
66 0
|
25天前
|
安全 Java API
【Java性能优化】Map.merge()方法:告别繁琐判空,3行代码搞定统计累加!
在日常开发中,我们经常需要对Map中的值进行累加统计。}else{代码冗长,重复调用get()方法需要显式处理null值非原子操作,多线程下不安全今天要介绍的方法,可以让你用一行代码优雅解决所有这些问题!方法的基本用法和优势与传统写法的对比分析多线程安全版本的实现Stream API的终极优化方案底层实现原理和性能优化建议一句话总结是Java 8为我们提供的Map操作利器,能让你的统计代码更简洁、更安全、更高效!// 合并两个列表});简单累加。
163 0
|
4月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
84 26
|
4月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
91 17
|
6月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
502 2