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

文章介绍了JUC包中的并发集合,包括基础概念、常用并发集合如ConcurrentHashMap、CopyOnWriteArrayList等,以及它们如何通过锁和CAS保证线程安全。同时,文章强调并发集合并非绝对线程安全,连续操作仍需额外的并发控制。最后,讨论了并发集合的优化策略,如写时复制和锁细化。
摘要由CSDN通过智能技术生成

JUC系列文章目录

JUC基础——线程池
JUC基础(二)—— Future接口 及其实现
JUC基础(三)—— Lock锁 及 AQS



前言

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

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

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

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


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

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

后来在准备面试的时候,明白这些集合类型并不具备多线程安全的能力,又逐渐接触到了Vector、HashTable、ConcurrentHashMap等集合。但其实这些集合并不属于一类。像VectorHashTable 以及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 —— 写时复制技术

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

  1. Concurrent —— 并发技术

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

  1. 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
  • 线程对立:无法在多线程环境中并发使用代码
    在这里插入图片描述

比如 final 修饰的变量就是”不可变“级别,比如常用的String对象,我们对其的使用不需担心任何线程安全的问题。
绝对线程安全的比如synchronized关键字修饰的部分,允许多线程访问和修改,但不会有线程安全问题
而我们前面提到的并发集合,则处于相对线程安全级别,这个级别的特征是,对该对象单独的操作是线程安全的,但对于特定顺序的连续调用,可能就会出现问题,所谓问题,其实就是结果不合预期或无法确定

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

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

五、总结 - 前言问题回答

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

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

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

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

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战斧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值