(十)Java-ConcurrentMap

一、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(有序集合)类型。

高并发环境:跳表的简单结构和并发友好特性使其适用于高并发的应用场景,如多线程的并发数据库或者分布式缓存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值