17 - 并发容器的使用:识别不同场景下最优容器

在并发编程中,我们经常会用到容器。今天我要和你分享的话题就是:在不同场景下我们该如何选择最优容器。

1、并发场景下的 Map 容器

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

在 07 讲中,我曾详细讲过 HashMap 的实现原理,以及 HashMap 结构的各个优化细节。我说过 HashMap 的性能优越,经常被用来存储键值对。那么这里我们可以使用 HashMap 吗?

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

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

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

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

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

2、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 是弱一致性的,因此有可能会导致某次读无法马上获取到写入的数据。

3、ConcurrentHashMap 🆚 ConcurrentSkipListMap

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

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

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

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

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

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

3.1、什么是跳跃表

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

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

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

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

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

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

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

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

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

4、并发场景下的 List 容器

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

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

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

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

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

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

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

5、总结

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

6、思考题

在抢购类系统中,我们经常会使用队列来实现抢购的排队等待,如果要你来选择或者设计一个队列,你会怎么考虑呢?

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【UBOOT】1-使用与烧写

​一、uboot简介 1)uboot是一个裸机程序,比较复杂 2)最主要的作用是引导Linux内核启动; 初始化DDR; 因为Linux是运行在DDR里面的;而Linux镜像(zImage或uImagedtb)一般存放在SD EMM…

理解LoadRunner,基于此工具进行后端性能测试的详细过程(下)

5、录制并增强虚拟用户脚本 从整体角度看,用LoadRunner 开发虚拟用户脚本主要包括下面四步骤: 识别测试应用使用的协议 录制脚本 完善录制得到的脚本 验证脚本的正确性 识别被测应用使用的协议 如果明确知道了被测系统所采用的协议,可…

网卡介绍篇

目录 1.什么是网卡? 2.网卡的演进 3.网卡的主要功能 4.服务器网卡介绍 5.服务器网卡的分类 6.网卡接口介绍 7.业界网卡主流厂商 8.SmartNIC概念介绍 9.SmartNIC产业发展趋势 10.SmartNIC实现 10-1.实现形式 10-2.SmartNIC不同实现技术对比 11.Mellanox…

怎么把flac音频变为mp3?

怎么把flac音频变为mp3?FLAC音频格式在许多平台和应用程序中都得到支持和应用。FLAC音频格式被广泛支持和应用。许多平台、设备和应用程序都支持FLAC格式,如Windows、macOS和Linux操作系统、各种音乐播放器软件、智能手机和平板电脑、在线音乐平台和流媒…

python+django学生选课管理系统_wxjjv

1)前台:首页、课程信息、校园论坛、校园公告、个人中心、后台管理。 (2)管理员:首页、个人中心、学生管理、教师管理课、程信息管理、课程分类管理、选课信息管理、作业信息管理、提交作业管理、学生成绩管理、校园论…

Linux常见的指令合集

Linux指令合集 认识linuxlinux基础指令1.pwd 命令2. ls 命令3.cd 命令4. man 命令5. grep 命令6. ps 命令7. kill 命令8. netstat 命令9. date 查看当前系统时间10. echo 打印选项 -e linux文件操作指令1. mkdir 命令2. rmdir 命令3. touch 命令4. rm 命令5. mv 命令6. cp 命令…

ImgPlus:基于CodeFormer的图片增强

背景 最近参与了华为云开发者大会AI赛道,做了一个AI图片增强作品,本片文章来简单介绍一下。 正文 作品名称:ImgPlus 赛题技术领域选择: AI,图片增强 使用技术名称: CodeFormer,ECS&#xff0…

Puppeteer结合测试工具jest使用(四)

Puppeteer结合测试工具jest使用(四) Puppeteer结合测试工具jest使用(四)一、简介二、与jest结合使用,集成到常规测试三、支持其他的几种四、总结 一、简介 Puppeteer是一个提供自动化控制Chrome或Chromium浏览器的Node…

session认证

目录 前言 http协议的无状态性 session的工作原理 在express中使用session认证 在session中存数据 在session中取数据 清空session 结尾 前言 session是一种记录客户状态的机制,客户端浏览器法访问服务器的时候,服务器把客户端信息以某种形式记录…

WLAN 无线案例(华为AC控制器配置模板)

实验说明: 无线用户VLAN 30 192.168.30.0/24 AP和AC用VLAN 20 192.168.20.0/24 有线网段 VLAN 10 192.168.10.0/24 步骤一:全网互通 sw1: sysname sw1 # vlan batch 10 20 30 # dhcp enable # ip pool 20 gateway-list 192.168.20.1…

JAVA学习(6)-全网最详细~

🌈write in front🌈 🧸大家好,我是Aileen🧸.希望你看完之后,能对你有所帮助,不足请指正!共同学习交流. 🆔本文由Aileen_0v0🧸 原创 CSDN首发🐒 如…

UDP与TCP协议

很抱歉,我之前写好的UDP与TCP文章不小心被删了,所以,这篇文章只有一半,后面我会尽快补全。 在完成HTTPS的学习后,我们就完成了应用层的所有讲解,下面我们开始讲解传输层,这一层常用的协议为TCP…

MySQL的index merge(索引合并)导致数据库死锁分析与解决方案 | 京东云技术团队

背景 在DBS-集群列表-更多-连接查询-死锁中,看到9月22日有数据库死锁日志,后排查发现是因为mysql的优化-index merge(索引合并)导致数据库死锁。 定义 index merge(索引合并):该数据库查询优化的一种技术&#xff0…

JOSEF约瑟 漏电继电器 JD1-200 工作电压:380V 孔径:45mm 50~500mA

JD1系列漏电继电器 系列型号 JD1-100漏电继电器 JD1-200漏电继电器 JD1-250漏电继电器 JD1系列漏电继电器原为分体式固定式安装,为适应现行安装场合需要,上海约瑟继电器厂在产品原JD1一体式漏电继电器基础上进行产品升级,开发出现在较为…

从零开始学习调用百度地图网页API:二、初始化地图,鼠标交互创建信息窗口

目录 代码结构headbodyscript 调试 代码 <!DOCTYPE html> <html> <head><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><meta name"viewport" content"initial-scale1.0, user-scalable…

黑马JVM总结(三十四)

&#xff08;1&#xff09;JMM概述 &#xff08;2&#xff09;JMM-原子性-synchronized java内存模型是如何保证原子性的呢&#xff0c;它是通过synchroized关键字&#xff0c;来达到这个目的的 第一个线程来了进入同步代码块之后&#xff0c;把这个对象加上锁了&#xff0c;…

LockSupport-LockSupport是什么及等待唤醒机制对比

4.2 LockSupport是什么 LockSupport是用来创建锁和其他同步类的基本线程阻塞原语&#xff0c;其中park()和unpack()而作用分别是 阻塞线程和解除阻塞线程. 4.3 线程等待唤醒机制 4.3.1 三种让线程等待和唤醒的方法 方式一&#xff1a;使用Object中的wait()方法让线程等待&a…

【广州华锐互动】人体血管器官3D动态展示为医学生提供哪些便利?

人体血管器官3D动态展示是一种采用先进的计算机图形技术和立体成像技术&#xff0c;对人体内部结构和功能进行三维可视化的教学方法。这种教学方式以其独特的优势&#xff0c;正在改变传统的解剖学教学模式&#xff0c;为医学教育带来了革新。 首先&#xff0c;3D动态演示能够提…

ROS系列(二):rosbag 中提取视频数据

一、环境安装 当前环境在上一篇文章的基础上进行配置。 ROS系列&#xff08;一&#xff09;&#xff1a;【环境配置】rosbag 包安装_安装rosbag-CSDN博客 继续安装 sudo apt install ffmpeg python 包如下 pip install sensor_msgs --extra-index-url https://rospypi.gi…

互联网Java工程师面试题·Java 总结篇·第三弹

20、重载&#xff08;Overload&#xff09;和重写&#xff08;Override&#xff09;的区别。重载的方法能否根据返回类型进行区分&#xff1f; 方法的重载和重写都是实现多态的方式&#xff0c;区别在于前者实现的是编译时的多态性&#xff0c;而后者实现的是运行时的多态性。重…