从同步到并发:Java并发集合的剖析与使用!| 多线程篇(八)本文我将会详细介绍Java中的三种并发集合,通过实际案例,
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
一、前言
- 原子变量的基本概念:了解原子变量的工作原理和它们如何保证操作的原子性。
- 原子变量的使用场景:探讨在何种情况下使用原子变量比传统同步机制更加合适。
- 原子变量的源码解析:深入分析
AtomicInteger
、AtomicLong
和AtomicReference
等类的内部实现。 - 原子变量的案例分析:通过实际的编程案例,展示原子变量在多线程程序中的应用。
- 原子变量的优点与局限:评估原子变量在不同场景下的性能表现和潜在的使用限制。
然而,并发编程的广度远不止于此。除了原子变量,Java还提供了另一组强大的工具——并发集合。这些集合类不仅提供了线程安全的操作方法,还通过高度优化的并发控制机制,大大提高了多线程程序的性能。它们的产生,为构建高效、可伸缩并发应用走下了坚实的一步。
现在,让我们带着对原子变量的理解,进一步探索Java并发编程的另一个重要领域——并发集合。在本章节中,我们将一起学习以下关于并发集合的知识点,列举如下:
- 并发集合-基本概念:了解并发集合的工作机制和它们如何保证在多线程环境下的线程安全性。
- 并发集合-使用场景:探讨在何种情况下使用并发集合比传统集合更加合适。
- 并发集合-源码解析:深入剖析
ConcurrentHashMap
、ConcurrentLinkedQueue
和ConcurrentSkipListSet
等类的内部实现。 - 并发集合-案例分析:通过实际的编程案例,展示并发集合在多线程程序中的应用。
- 并发集合-优点与局限:评估并发集合在不同场景下的性能表现和潜在的使用限制。
正如原子变量在处理单个共享变量时的高效性,并发集合在处理复杂数据结构的并发访问时同样表现出色。它们适用于更广泛的应用场景,如分布式缓存、实时消息队列、高频交易系统等,这些你们在日常开发中也极其容易使用到。
此刻,我们废话不多说,直接上手学习并发集合的功能,并掌握它们在Java并发编程中的使用技巧。这将为构建高效、稳定和可伸缩的并发应用程序提供坚实的基础,也是为日后我们开发高质量项目打下坚实基础,这一步,我们迟早是要踏出的。
二、摘要
本文我将会详细介绍Java中的三种并发集合:ConcurrentHashMap
、ConcurrentLinkedQueue
和ConcurrentSkipListSet
。通过实际案例,源码分析等,我们将了解这些并发集合的工作原理、使用场景以及如何有效地利用它们,最终通过测试案例来实际使用它,将它彻底的通过理论与实际相结合,把它吃透。
三、正文
1.何为并发集合??
日常项目开发中,我们可能都了解,在多线程设计中,数据共享是一个常见且复杂的问题。传统的集合类在多线程环境下使用时,由于它们大多数不是线程安全的,很容易导致数据不一致性的问题。幸运的是,Java它就提供了一套并发集合框架,它们不仅线程安全,而且性能优异,完美的能够避开这个问题。而在本文中,我会深入探讨ConcurrentHashMap
、ConcurrentLinkedQueue
和ConcurrentSkipListSet
这三种并发集合,具体的了解每一种并发集合的原理及特点分析,最终让大家拥有直观地鉴别能力,不同场景,应用最合适的集合计划,将性能发挥到最优。
2.并发集合有哪些?
Java并发集合是java.util.concurrent
包的一部分,它们提供了一种线程安全的方式来处理共享数据。以下是本文将重点讨论这三种并发集合,分别如下:
- ConcurrentHashMap:线程安全的哈希表实现。
- ConcurrentLinkedQueue:线程安全的非阻塞队列。
- ConcurrentSkipListSet:线程安全的跳表实现的集合。
2.1 ConcurrentHashMap
我们先来介绍ConcurrentHashMap
,它是一个线程安全的哈希表,它在多线程环境下提供了高性能的键值对存取功能。相比Hashtable
和Collections.synchronizedMap
,ConcurrentHashMap
提供了更好的并发性能,因为它允许多个线程同时读写不同段的映射,而不需要对整个映射进行锁定。
特点:
如下是我梳理总结出来的一些主要特点,仅供参考:
- 高性能:分段锁机制使得并发访问更加高效。
- 动态扩容:在需要时自动扩容以适应不断增长的数据量。
- 灵活的配置:可以自定义初始容量、加载因子等参数。
2.2 ConcurrentLinkedQueue
接着是ConcurrentLinkedQueue
,它是一个线程安全的非阻塞队列,它支持高并发的入队和出队操作。由于它是一个无锁的设计,所以它在多线程环境中表现出色,没有因锁竞争导致的性能损耗。
特点:
如下是我梳理总结出来的一些主要特点,仅供参考:
- 非阻塞:利用CAS操作实现无锁的队列,提高并发性能。
- 先进先出:保证了FIFO(先进先出)的顺序。
- 无限容量:理论上可以处理无限数量的元素。
2.3 ConcurrentSkipListSet
接着是来讲解一下ConcurrentSkipListSet
,它是基于跳表(Skip List)实现的线程安全集合。跳表是一种能够快速查找的数据结构,它通过多层链表结构提供了对数级的查找性能。
特点:
如下是我梳理总结出来的一些主要特点,仅供参考:
- 有序性:保持元素的有序性,便于进行范围查询和有序遍历。
- 快速查找:提供接近于二分查找的效率。
- 可预测的性能:跳表的性能不会受到最坏情况的影响。
3.源码解析
并发集合的实现大多基于现代多核处理器和无锁(lock-free)编程技术。以下是对这仨种集合的简要源码分析,仅供参考:
3.1 ConcurrentHashMap
ConcurrentHashMap
使用分段锁(segmentation)的概念来提高并发性能。如下是其源码的核心部分
public V put(K key, V value) {
Segment<K,V> s;
if (count++ > loadFactor * capacity)
rehash();
// ...
return s.put(key, value, hash);
}
部分源码截图如下:
3.1.1 主要特点
- 线程安全:
ConcurrentHashMap
在多线程环境下保证了操作的线程安全性,无需开发者手动同步。 - 高性能:通过分段锁技术,
ConcurrentHashMap
允许并发的读写操作,极大提高了性能。 - 动态扩容:与
HashMap
类似,ConcurrentHashMap
支持动态扩容以适应不断增长的数据量。
3.1.2 内部实现
ConcurrentHashMap
内部使用分段锁(Segment
)来控制对不同段的访问。每个段实际上是一个HashEntry
数组,其中包含了实际存储键值对的数据。- 分段锁的设计减少了锁的竞争,提高了并发性。在多线程环境下,不同线程可以同时访问不同段的数据。
3.1.3 源码解析
在提供的其源码片段中,如下所示:
public V put(K key, V value) {
Segment<K,V> s;
if (count++ > loadFactor * capacity)
rehash();
// ...
return s.put(key, value, hash);
}
如下是我对其源码的解读,仅供参考。
count++
:每次插入操作前,先递增操作计数器。loadFactor * capacity
:当操作计数器超过加载因子与容量的乘积时,会触发rehash()
操作,即重新计算哈希表的容量和重新分配元素。s.put(key, value, hash)
:将键值对放入对应的段中。这里s
是通过哈希值计算得到的相应段。
3.1.4 它为什么要使用分段锁?
如上多次提到分段锁,那你们知道为啥要使用分段锁吗?这个问题值得深思。
其实这个问题很简单, 使用分段锁,而不是单个全局锁的原因是为了减少并发访问时的锁竞争。在全局锁的情况下,所有访问哈希表的线程都必须获取同一个锁,这会大大降低并发性能。而分段锁允许多个线程同时访问哈希表的不同部分,从而提高了并发性。
3.1.5 使用场景
- 当需要在多线程环境中存储和访问键值对时,
ConcurrentHashMap
是一个很好的选择。 - 它适用于计数器、缓存实现、并发数据聚合等场景。
3.1.6 优缺点
接着是对其集合的优缺点分析:
- 优点:提供了高并发的线程安全操作,性能优异。
- 缺点:与非线程安全的
HashMap
相比,有一定的性能开销;实现相对复杂,增加了学习和维护的难度。
通过深入剖析 ConcurrentHashMap
的工作原理和特性,方便大家更直观有效地利用这个强大的并发集合工具,构建高效且稳定的多线程应用程序。
3.2 ConcurrentLinkedQueue
我们应该都了解到了一点,ConcurrentLinkedQueue
它是一个基于链接节点的无锁队列。部分源码展示如下:
public boolean offer(E e) {
checkNotNull(e);
for (Node<E> t = new Node<>(e), p = tail;
p != null && p.next != null; p = p.next)
;
if (p == null)
head = t;
else {
p.next = t;
tail = t;
}
return true;
}
3.2.1 主要特点
ConcurrentLinkedQueue
是 Java 中提供的一个线程安全的无界非阻塞队列。它基于链接节点的有序集合,并且遵循先进先出(FIFO)原则。具体概括如下:
-
线程安全:
ConcurrentLinkedQueue
采用无锁算法(Lock-Free),确保了多个线程可以安全地进行并发访问,而不会发生数据不一致或出现死锁的情况。它使用了CAS(Compare-And-Swap)操作来实现原子性更新。
-
非阻塞:
ConcurrentLinkedQueue
是非阻塞的,这意味着当一个线程在执行插入或删除操作时,不会阻塞其他线程的操作。这个特性使得它特别适用于高并发的场景。
-
无界:
- 该队列没有容量限制,可以根据需要动态扩展,因此不存在队列满的情况。内存使用只受限于 JVM 可用内存。
-
FIFO(先进先出):
ConcurrentLinkedQueue
保证了元素按照入队的顺序出队,符合 FIFO 原则。
-
高效性:
- 对于大多数操作,如插入和删除,
ConcurrentLinkedQueue
提供了 O(1) 的时间复杂度,因此它在高并发场景下的性能表现良好。
- 对于大多数操作,如插入和删除,
3.2.2 内部实现剖析
ConcurrentLinkedQueue
的内部实现基于 Michael & Scott 在 1996 年提出的无锁队列算法。其核心数据结构是一个基于链表的节点(Node)。
- 节点(Node):
ConcurrentLinkedQueue
的内部节点定义如下:
static class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
this.item = item;
this.next = null;
}
}
每个节点包含一个数据项 item
和一个指向下一个节点的引用 next
。
-
头节点和尾节点:队列通过两个引用
head
和tail
分别指向链表的头部和尾部。初始时,head
和tail
都指向一个哑节点(dummy node),即一个空节点。 -
CAS 操作:插入和删除操作都是通过 CAS 操作实现的。CAS 操作比较当前值与预期值是否相同,如果相同则将其更新为新值。这种操作确保了多个线程在没有锁的情况下可以安全地进行并发修改。
-
入队(offer):插入元素时,
ConcurrentLinkedQueue
会创建一个新的节点,并尝试通过 CAS 操作将其添加到链表的尾部。它先尝试更新tail
指向新节点的next
引用,然后再更新tail
本身指向新节点。 -
出队(poll):删除元素时,它会尝试通过 CAS 操作更新
head
指向链表的下一个节点,同时返回被删除的节点的数据项。
3.3 ConcurrentLinkedQueue 详解
ConcurrentLinkedQueue
,它也是 Java 中提供的线程安全队列之一,采用了无锁(lock-free)的设计,基于 CAS(Compare-And-Swap)操作来保证线程安全。这种设计使得队列能够在高并发环境下表现出色,避免了锁竞争导致的性能问题。
3.3.1 主要特点
- 无锁设计:利用 CAS 操作实现无锁的队列,减少了锁竞争。
- 高并发:适合在多线程环境中使用,特别是当有大量线程进行入队和出队操作时。
- 先进先出(FIFO):保证了元素的顺序,按照元素入队顺序进行出队。
3.3.2 内部实现
ConcurrentLinkedQueue
内部使用链表结构存储元素,每个节点由Node
类表示。- 队列由
head
和tail
两个指针标识,分别指向链表的第一个和最后一个节点。
3.3.3 源码解析
在现Java1.8的源码片段中,部分展示如下:
public boolean offer(E e) {
checkNotNull(e);
for (Node<E> t = new Node<>(e), p = tail;
p != null && p.next != null; p = p.next)
;
if (p == null)
head = t;
else {
p.next = t;
tail = t;
}
return true;
}
checkNotNull(e)
:确保入队元素不为null
。Node<E> t = new Node<>(e)
:创建一个新的节点t
,包含要入队的元素e
。for
循环:遍历链表直到最后一个节点p
。if (p == null)
:如果p
为null
,说明队列为空,新节点t
将成为头节点。else
:如果p
不为null
,将新节点t
链接到p
之后,并更新tail
指针。return true
:offer
操作总是成功的,返回true
。
如下是其对应的源码截图:
3.3.4 为什么使用无锁设计?
那么这里就会有个问题,无锁?为什么要使用无锁设计?这个问题,听起来很难,其实一点也不简单。对于无锁设计,它的优势在于减少线程间协调的开销,提高并发性能。在锁设计中,线程需要等待获取锁,这可能就会导致线程阻塞和上下文切换,从而影响性能,而无锁设计通过 CAS 操作可保证操作的原子性,无需阻塞线程,所以这就是它为什么要采用无锁的缘由了!
3.3.5 使用场景
- 当需要在多线程环境中安全地共享数据时,
ConcurrentLinkedQueue
是一个很好的选择。 - 它适用于任务调度、消息传递、日志收集等场景。
3.3.6 优缺点分析
接着是对其集合的优缺点分析:
- 优点:无锁设计提高了并发性能,减少了锁竞争。
- 缺点:无锁编程模型相对复杂,增加了代码理解和维护的难度。
通过深入理解 ConcurrentLinkedQueue
的工作原理和特性,同学们可以更有效地利用这个并发工具,开发更高效且稳定的程序应用。
3.4 ConcurrentSkipListSet
ConcurrentSkipListSet
是基于ConcurrentSkipListMap
实现的跳表。
public boolean add(E e) {
return this.map.put(e, Boolean.TRUE) == null;
}
如下是对于其ConcurrentSkipListSet
的剖析与理解,它也是 Java 提供的线程安全并发集合之一,是基于跳表(Skip List)的数据结构实现的。其中跳表是一种概率型数据结构,顾名思义,它通过多层链表结构提供了类似平衡树的性能,但实现上更为简单。
3.4.1 主要特点
- 有序性:
ConcurrentSkipListSet
保证了元素的有序性,可以进行有序遍历。 - 高性能:跳表结构提供了对数级的查找、插入和删除操作。
- 线程安全:多线程环境下,
ConcurrentSkipListSet
保证了操作的线程安全性。
3.4.2 内部实现
ConcurrentSkipListSet
内部使用ConcurrentSkipListMap
来存储元素,其中键是集合中的元素,值是一个布尔值(通常为Boolean.TRUE
)。- 这种方法允许
ConcurrentSkipListSet
利用ConcurrentSkipListMap
的并发控制机制和有序性。
3.4.3 源码解析
在提供的源码片段中:
public boolean add(E e) {
return this.map.put(e, Boolean.TRUE) == null;
}
this.map
:ConcurrentSkipListSet
中的内部ConcurrentSkipListMap
实例。put(e, Boolean.TRUE)
:将元素e
作为键,Boolean.TRUE
作为值放入映射中。return this.map.put(e, Boolean.TRUE) == null
:如果元素e
已存在于集合中,则put
操作不会替换现有键值对,而是返回null
,表示添加操作失败。如果e
不存在,则put
操作会插入新的键值对,并返回true
,表示添加成功。
3.4.4 为什么使用跳表?
为什么使用跳表?这个问题请大家先思考三秒。跳表作为一种概率型数据结构,通过多级索引来加快查找速度,类似于平衡树但实现更简单。在并发环境下,跳表可以提供高性能的有序数据集操作。
3.4.5 使用场景
- 当需要在多线程环境中维护有序的数据集合时,
ConcurrentSkipListSet
是一个很好的选择。 - 它适用于需要有序遍历、范围查询等操作的场景。
3.4.6 优缺点分析
接着是对其集合的优缺点分析:
-
优点:
- 提供了有序性,便于进行范围查询和有序遍历。
- 高性能的查找、插入和删除操作。
-
缺点:
- 相比于非线程安全的有序集合,可能存在一定的性能开销。
- 内存占用相对较高,因为需要维护额外的索引层。
通过如上分析,我们可以深入理解其 ConcurrentSkipListSet
的工作原理和特性,希望可以帮助大家更有效地利用这个并发集合,为需要有序性保证的并发程序提供强有力的支持。
4. 应用场景案例列举
既然学习了如上几种集合,那么如下我就简单得列举几个常使用如上几种并发集合的场景,以帮助大家提高对其的使用率。
- ConcurrentHashMap:它比较适用于高并发的缓存系统和计数器等。
- ConcurrentLinkedQueue:它比较适用于任务调度和线程间的数据传输等。
- ConcurrentSkipListSet:它比较适用于需要有序处理的并发集合等。
5. 测试用例
通过如上长达几千字的理论学习,这里终于是告一段落了,接下来,我将手把手带着大家实操,通过结合使用main
函数,具体把如上几种集合来手敲代码使用一遍。
5.1 测试代码
具体测试代码如下:
/**
* 并发集合使用示例
*
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024年7月2日10:11:33
*/
public class ConcurrentCollectionTest {
public static void main(String[] args) {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
ConcurrentSkipListSet<Integer> set = new ConcurrentSkipListSet<>();
// 测试并发集合的基本操作
map.put(1, "one");
queue.offer("two");
set.add(3);
// 打印结果
System.out.println(map);
System.out.println(queue);
System.out.println(set);
}
}
5.2 代码解析
接着我将对上述代码进行详细的一个逐句解读,希望能够帮助到同学们,能以更快的速度对其知识点掌握学习,这也是我写此文的初衷,授人以鱼不如授人以渔,只有将其原理摸透,日后应对场景使用,才能得心应手,所以如果有基础的同学,可以略过如下代码分析步骤,然而没基础的同学,还是需要加强对代码的理解,方便你深入理解并掌握其常规使用。
如上测试案例代码主要是想演示三种来自java.util.concurrent
包的并发集合的使用,看上去使用起来是不是非常的简单,与常规的集合类使用无二差别,实际上是有区别的,重点就是作用及效果,这点大家需要掌握,而不是滥用,接着可以听我一一解析,首先针对如上三种集合,如下:
- ConcurrentHashMap
- ConcurrentLinkedQueue
- ConcurrentSkipListSet
这仨集合都设计为线程安全的,意味着它们可以被多个线程同时访问而不会导致数据不一致。下面我将对这段测试用例代码进行剖析解读:
5.2.1 ConcurrentHashMap
其中对于ConcurrentHashMap
,它是一种线程安全的哈希表实现,它允许多个线程同时执行读操作和一定程度的写操作,而不会发生冲突。这里创建了一个ConcurrentHashMap
对象,键的类型为Integer
,值的类型为String
。
map.put(1, "one");
这行代码将键值对(1, "one")
插入到map
中。
5.2.2 ConcurrentLinkedQueue
其中对于ConcurrentLinkedQueue
,它是一个基于链表的无界线程安全队列。它遵循先进先出(FIFO)的顺序。这里创建了一个ConcurrentLinkedQueue
对象,元素类型为String
。
queue.offer("two");
这行代码将字符串"two"
插入到队列中。
5.2.3 ConcurrentSkipListSet
其中对于ConcurrentSkipListSet
,它是一个基于跳表的线程安全有序集合。它按照自然顺序排序元素,并且支持高效的并发访问。这里创建了一个ConcurrentSkipListSet
对象,元素类型为Integer
。
set.add(3);
这行代码将整数3
插入到集合中。
5.2.4 打印结果句
System.out.println(map);
System.out.println(queue);
System.out.println(set);
最后,这三行代码分别打印出map
、queue
和set
的内容。由于这段代码在主线程中运行且没有其他线程干扰,输出将显示这些集合中包含的元素。
5.2.5 预期输出
假设没有异常发生,输出结果将如下所示:
{1=one}
[two]
[3]
这表示map
包含一个键值对1=one
,queue
包含一个元素two
,而set
包含一个元素3
。
5.3 实际运行结果展示
根据如上的案例,作者在本地进行测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加其他的测试数据或测试方法,以便于进行熟练学习以此加深知识点的理解。
大家也可以本地运行验证下,是否与我们所预期的结果一致?
四、小结
通过本章的学习,我们不仅掌握了Java并发集合的使用,还提升了解决并发问题的能力。这些集合是Java并发编程中的重要组成部分,它们以一种高效且易于管理的方式来保证多线程环境下数据的一致性和完整性。
五、总结
在本章内容中,我们深入探讨了Java并发集合的概念、应用和实现细节。通过学习ConcurrentHashMap
、ConcurrentLinkedQueue
和ConcurrentSkipListSet
这三种并发集合,我们不仅理解了它们的工作原理,还掌握了它们在多线程环境下的实际应用。
六、结语
并发编程是一场既充满挑战又极具魅力的旅程。并发集合作为这条路上的重要里程碑,值得我们每一位开发者去深入探索和学习。希望本章的内容能够帮助大家更好地理解和运用Java中的并发集合,编写出更高效、更健壮的并发程序。记住,不断学习和实践是成为并发编程高手的唯一途径。让我们一起享受这场旅程,不断前行,不断进步。
ok,以上就是我这期的全部内容啦,若想学习更多,你可以持续关注我,我会把这个多线程篇系统性的更新,保证每篇都是实打实的项目实战经验所撰。只要你每天学习一个奇淫小知识,日积月累下去,你一定能成为别人眼中的大佬的!功不唐捐,久久为功!
「学无止境,探索无界」,期待在技术的道路上与你再次相遇。咱们下期拜拜~~
七、往期推荐
转载自:https://juejin.cn/post/7405243925337128987