一、ConcurrentMap
ConcurrentMap 是 Java 中 java.util.concurrent 包的一部分,是 Map 接口的一个子接口。它提供了线程安全的 Map 实现,适用于多线程环境下进行并发读写时的高效操作。
1.特点
线程安全:通过内部锁和无锁技术(如乐观锁)来保证线程安全,允许多个线程并发地读取和写入。
原子操作:提供了一些原子操作的方法,例如 putIfAbsent、remove、replace 等,可以在不需要外部同步的情况下安全地操作映射。
2.关键方法
putIfAbsent(K key, V value):
如果指定的键不存在,则插入该键值对;
remove(Object key, Object value):
当且仅当指定的键存在,并且对应的值也匹配时,才会删除该键值对;
replace(K key, V value):
如果指定的键存在,则替换该键的值;
replace(K key, V oldValue, V newValue):
只有当指定的键存在,并且其当前值与 oldValue 匹配时,才会替换为 newValue;
compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction):
无论键是否存在,都会使用给定的 remappingFunction 来计算新的值。
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction):
如果指定的键不存在,则计算出一个值并将其插入到 Map 中。
computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction):
如果指定的键存在,则通过给定的 remappingFunction 来计算新的值。
forEach(BiConsumer<? super K, ? super V> action):
集合遍历。
3.实现类
ConcurrentHashMap:
最常用,实现了 ConcurrentMap 接口,支持高并发读写操作,通过分段锁机制来提高并发性能。
ConcurrentSkipListMap:
实现了 NavigableMap 和 ConcurrentMap 接口,基于跳表(Skip List)数据结构,支持高效的并发操作,并能提供有序的键值对遍历。
二、ConcurrentHashMap
1.底层实现
底层结构:Hash表+链表+红黑树。
2.特点
基本特点:不支持 null 键和值。
初始容量: 16(默认),负载因子:0.75(默认)。
分段锁定:JDK 8之前使用分段锁定机制(Segment),将数据分成多个段,每个段可以独立地进行锁定和访问;JDK 8 开始使用桶(bucket)级别的锁定。
无锁读取:在读取操作时不需要锁,使用了非阻塞算法(例如,使用 volatile 和 CAS 操作),允许多个线程同时读取数据,而不会造成锁竞争,极大提高了读操作的效率。
动态调整:支持动态调整容量和负载因子,避免了在高并发情况下的性能下降。
线程安全:通过合理的分段和锁机制,确保在多个线程并发访问的情况下,数据的一致性和线程的安全。
高并发性能:与 Hashtable 或 synchronizedMap 不同,ConcurrentHashMap 采用了细粒度的锁机制,避免了全局锁,允许多个线程在不同的桶(bucket)上并行工作,从而提高了并发性能。
3.核心实现和特点
(1)分段锁
采用了分段锁(Segment Locking)机制。它将哈希表分为多个段(Segment),每个段拥有独立的锁。每个段本质上是一个小的 HashMap。不同的线程可以并发访问不同的段,因此能够提高并发性能。每个段内的数据访问仍然是线程安全的。
*从 JDK 8 开始,ConcurrentHashMap 的实现不再使用 Segment,而是采用了更细粒度的锁和 CAS(Compare And Swap)操作。具体来说,它使用了桶(bucket)级别的锁定,而不需要在整个段上加锁,进一步提高了性能。
(2)CAS
使用 CAS操作来保证并发操作的安全性。例如,在插入或更新元素时,它会先读取当前值,如果没有其他线程修改这个值,再进行更新。这种机制避免了传统锁机制带来的性能开销。
(3)Bucket和Hash冲突
ConcurrentHashMap 内部依然是基于哈希表的,使用桶数组来存储数据。哈希冲突通过链表或红黑树解决。在 JDK 8 之后,如果一个桶内的链表长度超过一定阈值(默认是 8),链表会被转化为红黑树,从而提高查询性能。
(4)高效的并发读取
ConcurrentHashMap 提供的读取操作(如 get)是高效且无锁的。这是因为,读取操作并不需要加锁,它们通过 volatile 关键字确保最新值的可见性。此外,get 操作是无阻塞的,可以与其他线程的写操作并发执行。
(5)写操作和锁控制
对于写操作(如 put、remove 等),ConcurrentHashMap 会使用不同类型的锁机制来确保线程安全。具体的锁控制策略如下:
使用细粒度的分段锁或者桶级别的锁,来减少对其他线程的阻塞。
在 JDK 8 中,采用了 CAS(原子操作)和 synchronized 锁结合的方式,确保对单个桶或单个节点的操作是线程安全的。
4.扩容机制
ConcurrentHashMap 也会在哈希表负载因子超过一定阈值时进行扩容。与传统的 HashMap 相同,扩容操作会重新计算哈希表中每个元素的索引位置,并将元素重新分配到新的桶中。由于扩容是一个写操作,ConcurrentHashMap 会在进行扩容时通过加锁来保证线程安全。
5. 性能与使用场景
适用于多线程并发读写的场景,例如缓存系统、在线统计、计数器等。它能够有效避免对全局数据结构的加锁,从而在高并发的环境下显著提高性能。
由于它不支持 null 键和值,因此如果业务需求中必须允许 null,则应考虑使用其他线程安全的映射结构(如 Collections.synchronizedMap)。
三、ConcurrentSkipListMap
1.底层实现
基于跳表(Skip List)数据结构,有序映射,保证元素的排序。
2.特点
排序:ConcurrentSkipListMap 中的元素按照自然顺序或构造映射时提供的比较器进行排序。键(key)必须是可比较的(即实现了 Comparable 接口),或者在创建时提供一个比较器(Comparator)。
高并发性能:跳表的数据结构使得它在多线程环境中具有较好的并发性能。与其他锁竞争策略相比,跳表能够在读写操作上提供较低的锁粒度。
无阻塞性:ConcurrentSkipListMap 的操作通常不会阻塞其他线程,特别是对读取操作,多个线程可以并发地读取同一个元素而不会互相阻塞。
3.跳表
本质:是一种高效的查找数据结构,其通过多级索引的方式来优化查找速度。类似于平衡树。
基本结构:多级链表:跳表通过多个层次的链表来加速查找,每一层的链表包含了上一层链表的一部分元素,允许在多个层次上并行查找。
随机化:跳表的高度是随机决定的。每个元素以一定概率被提升到更高的层次,这样能够确保跳表的平衡性。
时间复杂度:查找时间复杂度是 O(log n)
4.核心实现原理
层级跳跃:每个元素在跳表中有多层索引,查找操作通过多层索引加速。
并发操作:跳表的节点通过 CAS(Compare-And-Swap)操作进行并发控制,从而避免了传统锁机制可能带来的性能瓶颈。通过这些并发控制技术,ConcurrentSkipListMap 能够实现高效的并发操作。
节点锁控制:在修改跳表时,ConcurrentSkipListMap 使用了细粒度的锁机制(如局部锁或 CAS)来确保并发修改不会引发数据不一致性。
5.特殊方法
subMap(K fromKey, K toKey):
获取指定范围内的子映射;
headMap(K toKey) 和 tailMap(K fromKey):
分别返回指定键之前或之后的子映射。
6.使用场景
实时数据处理系统、缓存系统、日志记录系统;
高效的范围查询;
线程安全的排序映射。
四、CAS
CAS(Compare and Swap,比较与交换)是一种常用于并发编程中的原子操作,主要用于解决多线程环境中的竞争条件问题。它是实现无锁数据结构和算法的基础之一。CAS 操作通过硬件支持来保证某些操作的原子性,即使在多个线程并发执行时,也能够避免数据的不一致性。
1.原理
比较(Compare):检查内存中某个位置(通常是一个变量)上的当前值与给定的预期值是否相等。
交换(Swap):如果当前值等于预期值,就将该内存位置的值更新为新值;如果当前值不等于预期值,则不进行任何操作。
2.应用
无锁算法:CAS 是实现无锁数据结构的核心。例如,链表、队列、栈等数据结构的插入、删除操作,可以通过 CAS 来避免使用传统的锁机制,从而提高并发性能。
原子操作:CAS 常用于实现原子变量操作,如计数器、标志位等。
乐观锁:CAS 是实现乐观锁的基础,乐观锁的思想是:在进行操作时,不加锁,而是通过 CAS 来判断是否有其他线程修改了数据。如果数据没有被其他线程修改过,就完成修改;如果被修改过,则重新尝试。
3.优缺点
(1)优点:
高效:CAS 操作是硬件原子操作,效率非常高,比传统的加锁机制要好很多,尤其是在高并发情况下。
避免死锁:因为 CAS 不需要使用互斥锁,所以避免了死锁问题。
无锁:无锁编程可以减少上下文切换,降低操作系统调度开销。
(2)缺点:
ABA 问题:CAS 操作可能会受到 ABA 问题的影响。ABA 问题指的是一个变量的值从 A 变成 B,再变回 A,CAS 判断时无法察觉值发生过变化,从而造成错误更新。为了解决这个问题,通常会引入版本号或者时间戳来标记变量的变化。
例如,可以将值与版本号(或时间戳)一起存储,在每次比较时不仅比较值,还比较版本号,以此来检测变量是否发生过改变。
自旋开销:CAS 是一个忙等操作,如果多个线程竞争同一个变量,它可能会导致“自旋”,即线程一直在进行 CAS 操作直至成功,这会消耗大量 CPU 时间。
只能操作单个值:CAS 操作只能对单个变量执行比较与交换,不能像锁机制那样同时操作多个变量。因此,对于一些复杂的数据结构,可能需要额外的技术来结合使用。
五、跳表
1.结构:
是一种多级链表结构;基本思想:在一个有序链表上建立多个索引层,每一层的链表都是上一层链表的子集,从而通过跳跃式查找提高查找效率。
(1)基本链表层
跳表的基础是一个有序的链表,所有元素都按顺序排列。这一层提供了最基本的查找能力。
(2) 多级索引层
除了基本链表层,跳表还维护了多层索引链表(通常是随机生成的)。每一层链表节点都会指向下一层链表中的节点,以此来加速查找过程。每层链表中,元素的分布是稀疏的,跳表的设计使得你可以快速跳过大量不相关的节点,从而更快地找到目标元素。
每个节点除了存储值之外,还存储指向下一层同层节点的指针。
具体:
第0层(基本层)包含所有元素。
第1层包含一些元素,且这些元素在第0层中的位置是有规律的。
第2层包含更少的元素,以此类推。
(3)随机层级
跳表的关键特点之一是每个元素有一个随机的“层级”,这个层级决定了该元素在多少层索引链表中出现。更高层级的元素会更稀疏。通常情况下,元素在第 k 层的概率是 pk,这里 p 是一个小于1的常数(例如 0.5),表示在某一层中出现的概率。这样,可以保证跳表的高度保持在 O(logn) 的级别。
(4)节点结构
每个跳表节点包含:值:存储数据值。
指针:指向当前层级下一个节点的指针,以及其他层级(更高层)的指针。例如,如果节点在第0层有指针指向下一个节点,那么它可能也在第1层、2层等拥有指向下层节点的指针。
2.结构演示
假设我们有一个跳表,其包含以下元素:1, 3, 5, 7, 9, 11, 13, 15
第0层:包含所有元素:1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15
第1层:包含较少的元素:1 -> 5 -> 9 -> 13
第2层:更稀疏:1 -> 9
第3层(如果有):可能只有一个元素:1
3.操作流程
查找:从最高层开始,沿着指针“跳跃”,直到找到目标元素或确认元素不在跳表中。每次跳跃都可以跳过许多不相关的元素,从而加快查找速度。
插入:首先决定该元素的层级,然后在每个层级的链表中插入元素。通常,层级是随机的,所以在每层中插入时需要维护指针的正确连接。
删除:删除元素时,需要从每一层链表中删除该元素,保持跳表的正确性。
4.时间复杂度
查找:跳表的查找时间复杂度为 O(logn),因为跳表是一个分层的结构,每层通过跳跃减少了需要检查的元素数量。
插入:插入操作也具有 O(logn) 的时间复杂度,因为插入时需要决定该元素所在的层级,并在每一层插入元素。
删除:删除的时间复杂度同样是O(logn)。
5.优缺点
(1)优点:
简单实现:跳表的实现比平衡树(如AVL树、红黑树)要简单很多,不需要复杂的旋转操作。
并发高效:跳表天然支持并发操作,因此适用于需要高并发读写的场景。
概率平衡:跳表通过随机化保证了平衡性,避免了像某些树结构可能出现的最坏情况(例如退化成链表)。
(2)缺点:
空间开销:跳表需要存储多个层级的指针,空间开销较大。
较差的常数因子:在实际应用中,跳表的常数因子可能比平衡树要大,所以在某些场景下性能可能不如平衡树。
6.应用场景
内存数据库:例如,Redis 中就使用了跳表来实现 Sorted Set(有序集合)类型。
高并发环境:跳表的简单结构和并发友好特性使其适用于高并发的应用场景,如多线程的并发数据库或者分布式缓存。