JUC基础(四)—— 并发集合

简介: JUC基础(四)—— 并发集合

前言

我们在JUC的开篇讲过,juc包主要包含以下部分:线程池 并发集合 同步器 原子变量 锁 并发工具类


那么本次我们就来讲一讲并发集合,这其实也是个面试常见问题,比如你可能会被问到:


  1. 说出几个你知道的并发集合,它们都是在什么包下面?
  2. 并发集合是绝对线程安全的吗?并发场景有没有可能出现问题?
  3. 并发集合是利用了什么来保证线程安全的?
  4. 你了解并发集合对并发场景做的优化吗?请举例说明

我们将在下文简要介绍并发集合,并回答上述问题


一、基础、同步、并发集合?

相信很多人最开始接触到的集合,就是类似HashMap、ArrayList这种基础集合;


后来在准备面试的时候,明白这些集合类型并不具备多线程安全的能力,又逐渐接触到了Vector、HashTable、ConcurrentHashMap等集合。但其实这些集合并不属于一类。像Vector、HashTable 以及Collection类下如 List synArrayList = Collections.synchronizedList(new ArrayList()) 生成的 SynchronizedList 等。


其原理都是利用了 同步关键字 synchronized,也就是以集合对象本身为锁,调用方法需要先获得锁,所以实际上一次只有一个线程能对集合进行操作,这种集合称为同步集合


基础集合和同步集合都是在 java.util.concurrent.* 包下的,而我们今天要讲的则是像 ConcurrentHashMap 一样,一种同样支持线程安全,但效率更高的集合类型——JUC包下的并发集合


二、常用并发集合

List/Set

CopyOnWriteArrayList : 线程安全的ArrayList

CopyOnWriteArraySet : 线程安全的HashSet

Map

ConcurrentHashMap: 线程安全的HashMap

ConcurrentSkipListMap 线程安全的TreeMap

Queue

ArrayBlockingQueue : 线程安全的有界的阻塞队列 (数组实现)

LinkedBlockingQueue 线程安全的阻塞队列 (l链表实现)

我们从这些类的命名上不难看出,这些类能够 线程安全且支持并发的原理,关键字就是


1.CopyOnWrite —— 写时复制技术

支持多线程读,在写操作时会复制一份数据来写,不影响其他线程读,但此时读的可能是旧数据


2.Concurrent —— 并发技术

使用更小粒度的锁,使其在线程安全的情况下能够支持更高的并发


3.Blocking——阻塞线程

只有队列用了此技术,因为队列只有头尾元素能变动,此时利用 ReentrantLock 锁以及Condition 条件队列 的功能来阻塞或唤醒线程,


三、CHM 如何支持并发

我们想要详细剖析CHM,首先就应该理解其与HashMap有什么不一样,正是这些不同点造就了其支持并发安全的能力,我们先看往里面插入值,差别代码如下:


1.对空key,空value的处理

CHM:
if (key == null || value == null) throw new NullPointerException();
HM: 无

出现这样的原因是hashmap用于单线程,当一次get操作获取到null的时候,还能继续用contains辅助判断这个null代表的是值是value,还是该键值对值不存在。


而 ConcurrentHashMap 用于多线程,当一次 get 操作获取到 null 的时候,再使用 contains 辅助判断时,可能数据已经发生了改变,那么我们就无法确认第一次 get 到 null 值的实际含义,即产生了歧义


2.每个桶的首节点

代码如下(示例):

CHM:
 else if ((fh = f.hash) == MOVED)
       tab = helpTransfer(tab, f);
HM: 无

HashMap的桶里,首节点就是普通的节点。而ConcurrentHashMap在扩容时,会在桶的头节点前插入新的头节点,且指定他的hash值为MOVED = -1,代表该桶正在迁移数据。此时本线程会协助其迁移,协助后再在新的table里去寻找并赋值

3.对空桶赋值

CHM:
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
       if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
       break;                   // no lock when adding to empty bin
  }
HM: 
if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);

在HashMap里,如果通过hash计算获得的位置是空桶,那就直接在该位置赋值一个新节点。而在CHM中,对该位置的占据会使用一个CAS操作


4.进入桶后的操作

CHM:
 synchronized (f) {
  XXXXX
 }
HM: 无

CHM会使用 synchronized 锁住该桶的头节点,而hashmap则直接桶中找值



5.put后的数目统计

CHM:
  addCount(1L, binCount);
HM: 
  ++modCount;

hashmap直接对总数加1,而CHM则是执行一个方法,该方法会尝试在基数或计数数组里找个位置进行加1,目的是尽可能提高并发


总结来看:我们不难得出一个结论,ConcurrentHashMap的线程安全主要是因为,使用了synchronized关键字和CAS操作,而其在带锁的情况下效率高则是采用了锁细化的技术,比如不锁整个数组,只锁数组中某一个桶。又比如在插入数据后计数时,采用基数和计数数组等多个地方来寻找位置加一,大大降低了并发冲突的可能


四、并发集合的局限性

我们刚刚讲到了一些并发集合用到的技术点,说了并发集合线程安全。但是,我们必须知道,线程安全不是一个是非题,而是一个大的范围,总的来说,安全的范围从严谨到宽松依次为:


  • 不可变:变量完全不可变,任何场景都不会有并发安全问题
  • 绝对线程安全:允许多线程同时访问修改但保证数据安全
  • 相对线程安全:该对象的单个操作是线程安全的,如ConcurrentHashMap
  • 线程兼容:通过添加同步措施,可以保证在多线程环境中安全使用这些类的对象,如HashMap
  • 线程对立:无法在多线程环境中并发使用代码

f411c8f2fe484238aee3609582ef63d8.png

比如 final 修饰的变量就是”不可变“级别,比如常用的String对象,我们对其的使用不需担心任何线程安全的问题。

绝对线程安全的比如synchronized关键字修饰的部分,允许多线程访问和修改,但不会有线程安全问题

而我们前面提到的并发集合,则处于相对线程安全级别,这个级别的特征是,对该对象单独的操作是线程安全的,但对于特定顺序的连续调用,可能就会出现问题,所谓问题,其实就是结果不合预期或无法确定。


比如对于 ConcurrentHashMap,我们用100个线程,对某个值进行get(),获取后再加一,最后put()回去,我们不难想象,最后这个值的结果,很可能不是原值+100。而 CopyOnWriteArrayList 这种写时复制的集合,也存在延时问题,最终读到的数据难以确定


所以我们必须搞清楚,类似 ConcurrentHashMap 这种并发集合,仅仅是相对线程安全,如果是对其连续调用且伴随逻辑处理,还需要外部代码提供额外的线程安全保障,才能使其结果符合预期


五、总结 - 前言问题回答

1.说出几个你知道的并发集合,它们都是在什么包下面?

比如 CopyOnWriteArrayList、ConcurrentHashMap、ArrayBlockingQueue等都是并发集合,它们都在JUC(java.util.concurrent) —— java并发包下


2.并发集合是绝对线程安全的吗?并发场景有没有可能出现问题?

java中没有绝对的并发安全的集合,真正安全的只有final修饰的不可变量,如果是final修饰的对象,则需要保证该对象各属性被final修饰。其次则是使用锁和CAS等同步概念的单次操作也可以称为绝对安全,而并发集合虽然采用了上面的技术,但毕竟不是单次操作,在一些特定顺序下去调用方法,出来的结果也不可预料,比如用100个线程并发对ConcurrentHashMap某个值加1,最后可能达不到+100的效果,此时还需要在业务代码中额外添加并发控制的逻辑,因此,尽管并发集合单次的操作是安全的,但多个步骤就无法保证安全了


3.并发集合是利用了什么来保证线程安全的?

主要是利用了锁,包括 ReentrantLock和 synchronized 关键字,还有CAS原子操作,在对可能冲突的位置进行写前,会进行一次竞争来获取操作权


4.你了解并发集合对并发场景做的优化吗?请举例说明

比如读写并发的场景,有的集合用到了写时复制技术,将集合复制一份来化解冲突。而对于会发生写冲突的位置,则主要是将锁细化,把一个锁拆成可伸缩的多个锁,然后针对细化的锁,大量利用for循环 + CAS原语 来取代同步阻塞,减少内核用户态的切换开销


目录
相关文章
|
8月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
NoSQL Java Redis
开发实战:使用Redisson实现分布式延时消息,订单30分钟关闭的另外一种实现!
本文详细介绍了 Redisson 延迟队列(DelayedQueue)的实现原理,包括基本使用、内部数据结构、基本流程、发送和获取延时消息以及初始化延时队列等内容。文章通过代码示例和流程图,逐步解析了延迟消息的发送、接收及处理机制,帮助读者深入了解 Redisson 延迟队列的工作原理。
Idea引入thymeleaf失败解决方法
Idea引入thymeleaf失败解决方法
322 0
|
12月前
|
Prometheus 监控 Cloud Native
高频面题: 你们线上 QPS 多少?你 怎么知道的?
本文由45岁资深架构师尼恩撰写,针对高级开发和架构师面试中的高频问题提供详细解答。文章涵盖了QPS、TPS、RT等性能指标的定义及计算方法,详解了如何配置Prometheus与Grafana监控系统QPS,并提供了应对高并发场景(如双十一抢购)的系统部署策略。此外,还分享了多个大厂面试真题及解决方案,帮助读者在面试中充分展示技术实力,提升求职竞争力。建议收藏并深入学习,为面试做好充分准备。更多内容可参考《尼恩Java面试宝典》及相关技术圣经系列PDF。
|
10月前
|
NoSQL 数据库 Redis
如何保证MQ幂等性?或 如何防止消息重复消费?
如何保证MQ幂等性?或 如何防止消息重复消费?
|
开发工具 git
idea结合git回到某个提交点
本文介绍了如何在IntelliJ IDEA中使用Git工具回退到之前的提交点,通过Git管理界面查看提交日志,选择特定提交并进行软重置、混合重置或硬重置以撤销后续的更改。
1562 1
idea结合git回到某个提交点
|
设计模式 前端开发 C#
使用 Prism 框架实现导航.NET 6.0 + WPF
使用 Prism 框架实现导航.NET 6.0 + WPF
531 10
|
并行计算 Java 应用服务中间件
JUC并发编程超详细详解篇(一)
JUC并发编程超详细详解篇
2205 1
JUC并发编程超详细详解篇(一)
|
Linux 开发者
Linux的诞生:Linus Torvalds的“惊天一敲”与Linux内核的“首秀”
在科技界璀璨星辰中,Linus Torvalds以一次“惊天一敲”悄然点燃了革命之火——Linux就此诞生。1991年,不满现状的Linus决定创造更好的操作系统,这一敲不仅开启了个人传奇,更奏响了技术革新的序章。他将Linux内核低调发布网络,随即吸引了全球开发者的目光与贡献,使之迅速成长为开源世界的巨星。Linus的故事告诉我们:伟大创举常源于微小想法,也许下一个改变世界的“一敲”就出自你手。
620 1
|
自然语言处理 算法 PyTorch
Transformers 4.37 中文文档(十五)(4)
Transformers 4.37 中文文档(十五)
238 1