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

本文涉及的产品
云原生大数据计算服务MaxCompute,500CU*H 100GB 3个月
云原生大数据计算服务 MaxCompute,5000CU*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 
相关文章
|
5天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
55 23
|
12天前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
81 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
16天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
142 60
【Java并发】【线程池】带你从0-1入门线程池
|
1月前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
106 14
|
算法 安全 Java
Java 性能优化:35个小细节,让你提升Java代码运行的效率
  代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。   代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。
275 0
|
机器学习/深度学习 算法 Java
11月27日云栖精选夜读 | Java性能优化的50个细节
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 第一,控制资源的使用,通过线程同步来控制资源的并发访问; 第二,控制实例的产生,以达到节约资源的目的; 第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
2985 0
|
Java 程序员 Android开发
10月31日云栖精选夜读 | Java性能优化的50个细节(珍藏版)
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 第一,控制资源的使用,通过线程同步来控制资源的并发访问; 第二,控制实例的产生,以达到节约资源的目的; 第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
3046 0
|
1月前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
57 13

热门文章

最新文章