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.mzph.cn/news/107784.shtml

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

相关文章

如何通过Photoshop将视频转换成GIF图片

一、应用场景 1、将视频转有趣动图发朋友圈 2、写CSDN无法上传视频,而可以用GIF动图替代 3、其他 二、实现步骤 1、打开Photoshop APP 2、点击文件——导入——视频帧到图层 3、选择视频文件 4、配置视频信息,按照图片提示配置完毕之后点击确定&…

C# Winform编程(4)多文档窗口(MDI)

多文档窗口(MDI) 添加菜单,IsMdiContainer设为True: From窗口添加菜单 Form1.cs using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using S…

snk-给github界面加一个有趣的动画

How to enable GitHub Actions on your Profile README for a snake-eating contribution graph 🐍 - DEV Community Platane/Platane (github.com) ① 创建New repository 名字和你自己的Github 用户名一样。 ② 创建之后,再这个Zero-coder仓库下创建…

学信息系统项目管理师第4版系列29_信息系统治理

1. IT治理 1.1. 描述组织采用有效的机制对信息技术和数据资源开发利用,平衡信息化发展和数字化转型过程中的风险,确保实现组织的战略目标的过程 1.2. 驱动因素 1.2.1. 信息孤岛 1.2.2. 信息资源整合目标空泛 1.3. 高质量IT治理因素 1.3.1. 良好的I…

Flask框架配置celery-[1]:flask工厂模式集成使用celery,可在异步任务中使用flask应用上下文,即拿即用,无需更多配置

一、概述 1、celery框架和flask框架在运行时,是在不同的进程中,资源是独占的。 2、celery异步任务如果想使用flask中的功能,如orm,是需要在flask应用上下文管理器中执行orm操作的 3、使用celery是需要使用到中间件的&#xff0…

内容分发网络CDN分布式部署真的可以加速吗?原理是什么?

Cdn快不快?她为什么会快?同样的带宽为什么她会快?原理究竟是什么,同学们本着普及知识的想法,我了解的不是很深入,适合小白来看我的帖子,如果您是大佬还请您指正错误的地方,先谢谢大佬…

nodejs基于vue网上考勤系统

本网上考勤系统是针对目前考勤的实际需求, 采用计算机系统来管理信息,取代人工管理模式,查询便利,信息准确率高,节省了开支,提高了工作的效率。 本网上考勤系统主要包括个人中心、员工请假管理、员工考勤管…

asp.net酒店管理系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio

一、源码特点 asp.net酒店管理系统是一套完善的web设计管理系统,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为vs2010,数据库为sqlserver2008,使用c#语言开发 asp.net 酒店管理系统1 二、功能介绍 …

如何使用FME开发自动化分析报告功能

目录 前言 一、使用的技术栈 二、技术难点解析 1.专题图 2.WORD文档实现 2.1 动态标题 2.3动态表格和文本 2.3专题图插入 三、完成NewGIS部署 四、模板总览图 总结 前言 一个标准项目分析报告需要需要包括3个方面: 文本叙述,主要体现在对某项专项数据的…

Radius OTP完成堡垒机登录认证 安当加密

Radius OTP(One-Time Password)是一种用于身份验证的协议,它通过向用户发送一个一次性密码来验证用户的身份。使用Radius OTP可以实现堡垒机登录,以下是一些实现步骤: 1、安装Radius服务器 首先需要安装Radius服务器…

数字化转型“同群效应”(2000-2022年)

参照霍春辉等(2023)的做法,团队对上市公司-数字化转型“同群效应”进行测算。将同行业、同省的其他企业定义为同群企业,并以该群体数字化转型程度均值、中位数作为衡量 一、数据介绍 数据名称:数字化转型“同群效应”…

c++视觉检测------Shi-Tomasi 角点检测

Shi-Tomasi 角点检测 :goodFeaturesToTrack() goodFeaturesToTrack() 函数是 OpenCV 中用于角点检测的功能函数。它的主要作用是检测图像中的良好特征点,通常用于计算机视觉任务中的光流估算、目标跟踪等。 函数签名: void goodFeaturesTo…

构建高性能物联网数据平台:EMQX和CnosDB的完整教程

CnosDB 是一款高性能、高压缩率、高易用性的开源分布式时序数据库。主要应用场景为物联网、工业互联网、车联网和IT运维。所有代码均已在GitHub开源。本文将介绍如何使用EMQX 这一MQTT 服务器 CnosDB 构建物联网数据平台,实现物联网数据的实时流处理。 前言 在物联…

使用paddleX体验

首先paddlex的网址链接是:飞桨AI Studio星河社区-人工智能学习与实训社区 (baidu.com) 进入paddlex以后的界面如下所示: 首先说明paddlex的作用是: PaddleX是PaddlePaddle深度学习框架的一个扩展库,专注于为深度学习任务提供强大…

ITextRenderer将PDF转换为HTML详细教程

引入依赖 <dependency><groupId>org.xhtmlrenderer</groupId><artifactId>flying-saucer-pdf-itext5</artifactId><version>9.1.18</version></dependency> 问题一&#xff1a;输出中文字体 下载字体simsun.ttc 下载链接&am…

使用html2canvas将html转pdf,由于table表的水平和竖直有滚动条导致显示不全(或者有空白)

结果&#xff1a; 业务&#xff1a;将页面右侧的table打印成想要的格式的pdf&#xff0c;首先遇到的问题是table表上下左右都有滚轮而html2canvas相当于屏幕截图&#xff0c;那滚动区域如何显示出来是个问题&#xff1f; gif有点模糊&#xff0c;但是大致功能可以看出 可复制…

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

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

SQL Server向表中插入数据

SQL Server向表中插入数据 切换到对应的数据库 use DBTEST插入数据 方式1 insert into 表名&#xff08;列名1,列名2) values&#xff08;数据1&#xff0c;数据2&#xff09;注意&#xff1a; 列名就算是字符类型也不用加引号&#xff0c;数据如果对应的字段是字符串类型&…

图像语义分割 pytorch复现U2Net图像分割网络详解

图像语义分割 pytorch复现U2Net图像分割网络详解 1、U2Net网络模型结构2、block模块结构解析RSU-7模块RSU-4Fsaliency map fusion module U2Net网络结构详细参数配置RSU模块代码实现RSU4F模块代码实现u2net_full与u2net_lite模型配置函数U2Net网络整体定义类损失函数计算评价指…

Unity之ShaderGraph如何实现上下溶解

前言 我们经常在电影中见到的一个物体或者人物&#xff0c;从头上到脚下&#xff0c;慢慢消失的效果&#xff0c;我么今天就来体验一下这个上下溶解。 主要节点 Position节点&#xff1a;提供对网格顶点或片段的Position 的访问 Step节点&#xff1a;如果输入In的值大于或等…