明天你会感谢今天奋力拼搏的你。
ヾ(o◕∀◕)ノヾ
在 Java 中,并发容器是为了在多线程环境下高效、安全地处理数据而设计的容器类,主要位于java.util.concurrent包中。它们通过更高效的并发控制机制(如CAS、分段锁、写时复制等)替代传统的同步方法(如synchronized),显著提升了高并发场景下的性能。这些容器类提供了线程安全的操作,避免了在多线程环境中使用传统容器(如 ArrayList、HashMap 等)时可能出现的并发问题。
常用的并发容器:
实现原理:
适用场景:高频读写的键值存储(如缓存)。
在 JDK 1.7 中,ConcurrentHashMap 采用分段锁(Segment)机制来实现并发操作,这种设计可以在保证线程安全的同时,提高并发性能。下面详细介绍其实现原理。
整体结构:一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构,它们都继承自 ReentrantLock,可以独立进行加锁操作。不同的 Segment 之间可以并发访问,从而提高了并发性能。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,HashEntry用于存储键值对数据。每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
分段锁的这种结构,就意味着有多少个Segment就有多少并发。
初始化逻辑:在创建 ConcurrentHashMap 时,需要指定初始容量、加载因子和并发级别。并发级别决定了 Segment 的数量,默认值为 16。初始化过程会创建一个 Segment 数组,并初始化第一个 Segment,其他 Segment 会在需要时进行懒加载。
put方法逻辑:
get方法逻辑:
扩容操作逻辑:当某个 Segment 内部的元素数量超过阈值(容量 * 加载因子)时,该 Segment 会进行扩容操作。扩容过程会创建一个新的 HashEntry 数组,其容量为原来的 2 倍,然后将原数组中的所有 HashEntry 重新哈希到新数组中。
优缺点:
在 JDK 1.8 中,ConcurrentHashMap 的实现有了较大的改进,摒弃了 JDK 1.7 中的分段锁(Segment)机制,采用了 CAS(Compare-And-Swap)和 synchronized 来保证并发操作的线程安全性,并且在数据结构上引入了红黑树以提高查找效率。
整体结构:JDK 1.8 的 ConcurrentHashMap 采用数组 + 链表 + 红黑树的结构。数组中的每个元素称为一个桶(bucket),当一个桶中存储的元素较少时,采用链表来存储这些元素;当链表长度超过一定阈值(默认为 8)且数组长度达到 64 时,链表会转换为红黑树,以提高查找效率;当红黑树中的元素数量减少到一定阈值(默认为 6)时,红黑树会转换回链表。
核心组件:
初始化逻辑:在创建 ConcurrentHashMap 时,并不会立即初始化数组,而是在第一次插入元素时进行懒加载。初始化过程会创建一个指定大小的数组,默认初始容量为 16。
put方法逻辑:
get方法逻辑:
扩容操作逻辑:当数组中的元素数量超过阈值(容量 * 加载因子)时,ConcurrentHashMap 会进行扩容操作。扩容过程会创建一个新的数组,其容量为原来的 2 倍,然后将原数组中的所有元素重新哈希到新数组中。为了提高扩容效率,采用了多线程并发扩容的方式,多个线程可以同时帮助进行扩容操作。
优点:
ConcurrentSkipListMap 和 ConcurrentSkipListSet 是 Java 并发包 java.util.concurrent 中提供的两个线程安全的数据结构,它们都基于跳表(Skip List)数据结构实现。
跳表是在有序链表的基础上发展而来的。普通的有序链表在进行查找操作时,需要从头节点开始逐个遍历,时间复杂度为 (O(n))。为了提高查找效率,跳表通过在原始链表的基础上增加多层索引,使得在查找元素时可以跳过一些不必要的节点,从而加快查找速度。
传统意义的单链表是一个线性结构,向有序的链表中插入一个节点需要O(n)的时间,查找操作需要O(n)的时间。如果我们使用上图所示的跳跃表,就可以减少查找所需时间为O(n/2),因为我们可以先通过每个节点的最上面的指针先进行查找,这样子就能跳过一半的节点。
比如我们想查找19,首先和6比较,大于6之后,在和9进行比较,然后在和12进行比较......最后比较到21的时候,发现21大于19,说明查找的点在17和21之间,从这个过程中,我们可以看出,查找的时候跳过了3、7、12等点,因此查找的复杂度为O(n/2)。
跳跃表其实也是一种通过“空间来换取时间”的一个算法,通过在每个节点中增加了向前的指针,从而提升查找的效率。
Java对List、Set、Queue也提供了一系列的并发容器:
CopyOnWriteArrayList 是ArrayList的并发实现,它通过在每次修改操作(如 add、remove 等)时创建底层数组的一个新副本,然后在新数组上进行修改操作,最后将新数组替换原来的数组,从而实现线程安全。读操作则直接在原数组上进行,不需要加锁。因此在多线程环境下,读操作可以并发执行,性能较高。
优点:
缺点:
Vector的比较:
与ArrayList比较:
场景:适用于读极多写极少的场景,比如:白名单、黑名单、商品类目的访问和更新等。如果对数据版本及其严格,要实时读到数据,就不能用此类,还是得用读写锁。
注意:从下面的代码可以看出,每次添加删除都是对数组的拷贝,这样消耗性能很多,只能适用于读极多写极少的场景,且如果数据量很大也不能用,因为是通过copy进行的,copy的时候内存容量*2。
CopyOnWriteArraySet内部使用 CopyOnWriteArrayList 来存储元素,利用 CopyOnWriteArrayList 的写时复制机制来保证线程安全。所以很多特性可以之间参考上文的CopyOnWriteArrayList。
ConcurrentLinkedQueue 位于 java.util.concurrent 包下,是一个使用链表结构实现的无界线程安全队列。可以理解为是LinkedList的并发版本,在多线程环境下,多个线程可以同时对该队列进行读写操作,而不需要额外的同步机制,其内部通过使用 CAS(Compare-And-Swap)操作来保证线程安全。
特点
缺点:并发极高的情况下可能导致自旋很多次(成百上千)影响性能。
扩展:用锁可以解决自旋的问题,但是也不能解决高并发的问题。高并发情况下要考虑用其他方式进行限流。
BlockingQueue 是一个阻塞队列接口,在 Java 中,阻塞队列(BlockingQueue)接口的实现类都是线程安全的,它提供了阻塞式的插入和移除操作。当队列满时,插入操作会阻塞;当队列空时,移除操作会阻塞。
| 类名 | 描述 | 数据结构 | 线程安全实现方式 | 适用场景 |
|---|---|---|---|---|
ArrayBlockingQueue |
有界阻塞队列,创建时需指定容量,按 FIFO(先进先出)原则对元素排序。 | 数组 | 使用一个 ReentrantLock 以及两个 Condition 对象(分别用于控制插入和移除的等待)来保证线程安全。当队列满时,插入线程会被阻塞;当队列空时,移除线程会被阻塞。 |
适用于需要固定大小缓冲区的生产者 - 消费者场景,例如限制资源使用量的情况,像固定数量的任务处理池。 |
LinkedBlockingQueue |
可选有界的阻塞队列,默认最大容量为 Integer.MAX_VALUE,按 FIFO 原则对元素排序。 |
链表 | 使用两个 ReentrantLock(一个用于入队,一个用于出队)和相应的 Condition 对象,允许入队和出队操作并发执行,提高了并发性能。 |
适用于生产者和消费者速度差异较大的场景,因其可以有较大容量来缓冲数据,如 Web 服务器中请求的处理。 |
PriorityBlockingQueue |
无界阻塞队列,元素根据自然顺序或指定的比较器进行排序。 | 优先级堆 | 使用 ReentrantLock 和 Condition 来保证线程安全,插入元素时会根据优先级调整堆结构。 |
适用于需要根据元素优先级进行处理的场景,如任务调度系统,高优先级任务可以优先被处理。 |
DelayQueue |
无界阻塞队列,用于存放实现了 Delayed 接口的元素,只有在元素的延迟时间到期后才能被取出。 |
优先级堆(根据延迟时间排序) | 使用 ReentrantLock 和 Condition 保证线程安全,在取出元素时会检查延迟时间是否到期。 |
适用于实现定时任务调度,如缓存过期清理、任务超时处理等场景。 |
SynchronousQueue |
没有容量的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。 | 基于内部的交换机制(没有实际存储元素的数据结构) | 通过内部的状态管理和锁机制保证线程安全,确保插入和移除操作的同步。 | 适用于生产者和消费者紧密协作的场景,一个生产操作必须立即被消费操作处理,常用于线程池中的任务传递。 |
LinkedTransferQueue |
无界阻塞队列,支持 transfer 方法,可直接将元素传递给等待的消费者。 |
链表 | 使用 CAS(Compare - And - Swap)操作和内部锁机制保证线程安全,高效地处理元素的插入和移除。 | 适用于高并发场景下的生产者 - 消费者模式,特别是需要快速传递元素的场景。 |
LinkedBlockingDeque |
可选有界的双向阻塞队列,默认最大容量为 Integer.MAX_VALUE,支持从队列两端进行插入和移除操作。 |
双向链表 | 使用 ReentrantLock 和 Condition 保证线程安全,允许从队列的头部和尾部进行并发操作。 |
适用于需要双向操作队列的场景,如工作窃取算法中的任务队列,工作线程可以从队列头部或尾部获取任务。 |
全部评论