在多线程编程中,高效地访问和操作数据结构是一个重要的挑战。Java 提供了并发集合容器(Concurrent Collection Containers)来解决这个问题。这些容器通过内部的同步机制实现了线程安全,使得开发者无需显式同步代码就能在并发环境下安全使用。本文将详细介绍 Java 并发集合容器中的几个重要类,包括 ConcurrentHashMap
、阻塞队列和 CopyOnWrite
容器。
1 并发 Map
1.1 ConcurrentMap 接口
ConcurrentMap
接口继承自 Map
接口,并在其基础上增加了四个方法:
public interface ConcurrentMap<K, V> extends Map<K, V> {
//插入元素
V putIfAbsent(K key, V value);
//移除元素
boolean remove(Object key, Object value);
//替换元素
boolean replace(K key, V oldValue, V newValue);
//替换元素
V replace(K key, V value);
}
- putIfAbsent(K key, V value): 如果指定的键不存在,则插入键值对。如果键已存在,则不替换原有的值。
- remove(Object key, Object value): 删除指定的键值对。只有当键和值都匹配时,才会删除。
- replace(K key, V oldValue, V newValue): 替换指定的键值对。只有当键和旧值都匹配时,才会替换。
- replace(K key, V value): 替换指定的键值对。如果键存在,则直接替换。
1.2 ConcurrentHashMap
ConcurrentHashMap
是 ConcurrentMap
接口的主要实现类,它提供了高效的并发性和伸缩性。与 Hashtable
不同,ConcurrentHashMap
采用了更细粒度的锁策略,避免了全局锁,从而提高了并发性能。
ConcurrentHashMap
适用于读密集型操作,因为它允许在读操作时无需加锁。对于写操作,ConcurrentHashMap
使用了分段锁(Segment)来减少锁的竞争。
1.3 ConcurrentSkipListMap
ConcurrentSkipListMap
是 ConcurrentNavigableMap
接口的实现类,底层使用跳表(SkipList)数据结构。跳表是一种“空间换时间”的数据结构,能够提供有序的键值对存储。
与 ConcurrentHashMap
相比,ConcurrentSkipListMap
的读写性能相对较低,但提供了元素的有序性。适用于需要线程安全且元素有序的场景。
2 并发 Queue
JDK 并没有提供线程安全的 List
类,因为对于 List
来说,很难开发一个通用且没有并发瓶颈的线程安全的 List
。因此,JDK 提供了线程安全的队列和双端队列:ConcurrentLinkedQueue
和 ConcurrentLinkedDeque
。
这两个类使用 CAS(Compare-And-Swap)操作来实现线程安全,适用于高并发的场景。
3 并发 Set
ConcurrentSkipListSet
是线程安全的有序集合,底层使用 ConcurrentSkipListMap
实现。它适用于需要线程安全且元素有序的场景。
此外,谷歌的 Guava 库提供了一个线程安全的 ConcurrentHashSet
:
Set<String> s = Sets.newConcurrentHashSet();
4 阻塞队列
阻塞队列(BlockingQueue)是 Java util.concurrent
包下的重要数据结构,适用于生产者-消费者模式。阻塞队列提供了线程安全的队列访问方式,简化了并发编程的复杂性。
4.1 BlockingQueue 的操作方法
阻塞队列提供了四组不同的方法用于插入、移除、检查元素:
方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | - | - |
- 抛出异常: 如果操作无法立即执行,会抛出异常。
- 返回特殊值: 如果操作无法立即执行,会返回一个特殊值(通常是
true
或false
)。 - 一直阻塞: 如果操作无法立即执行,则一直阻塞或者响应中断。
- 超时退出: 如果操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。
4.2 BlockingQueue 的实现类
BlockingQueue
是 Java util.concurrent
包下的一个重要接口,提供了线程安全的队列访问方式,适用于生产者-消费者模式。BlockingQueue
有多个实现类,每个实现类都有其特定的用途和特性。下面将详细介绍 BlockingQueue
的几个主要实现类:ArrayBlockingQueue
、LinkedBlockingQueue
、DelayQueue
、PriorityBlockingQueue
和 SynchronousQueue
。
4.2.1 ArrayBlockingQueue
ArrayBlockingQueue
是一个由数组结构组成的有界阻塞队列。其内部结构是数组,具有数组的特性,如固定大小和随机访问。
public ArrayBlockingQueue(int capacity, boolean fair) {
// 省略代码
}
- 初始化队列大小: 一旦初始化,队列大小将不能改变。
- 公平锁: 构造方法中的
fair
参数表示控制对象的内部锁是否采用公平锁。默认是非公平锁。
4.2.2 LinkedBlockingQueue
LinkedBlockingQueue
是一个由链表结构组成的有界阻塞队列。其内部结构是链表,具有链表的特性,如动态大小和顺序访问。
- 默认队列大小: 默认队列的大小是
Integer.MAX_VALUE
,也可以指定大小。 - 先进先出原则: 此队列按照先进先出的原则对元素进行排序。
4.2.3 DelayQueue
DelayQueue
是一个没有大小限制的阻塞队列,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。注入其中的元素必须实现 java.util.concurrent.Delayed
接口。
- 插入操作: 往队列中插入数据的操作(生产者)永远不会被阻塞。
- 获取操作: 只有获取数据的操作(消费者)才会被阻塞。
4.2.4 PriorityBlockingQueue
PriorityBlockingQueue
是一个基于优先级的无界阻塞队列。优先级的判断通过构造函数传入的 Comparator
对象来决定。内部控制线程同步的锁采用的是非公平锁。
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
this.lock = new ReentrantLock(); // 默认构造方法-非公平锁
// 其余代码略
}
- 无界队列: 不会阻塞数据生产者(因为队列是无界的)。
- 阻塞消费者: 只会在没有可消费的数据时阻塞数据的消费者。
注意: 使用时要注意生产者生产数据的速度不能快于消费者消费数据的速度,否则会耗尽可用堆内存空间。
4.2.5 SynchronousQueue
SynchronousQueue
是一个比较特殊的队列,没有任何内部容量,甚至连一个队列的容量都没有。每个 put
操作必须等待一个 take
操作,反之亦然。
- 无容量: 没有任何内部容量。
- 配对操作: 每个
put
必须等待一个take
,反之亦然。
以下方法的返回值可以帮助理解这个队列:
- iterator(): 永远返回空,因为里面没有东西。
- peek(): 永远返回
null
。 - put(): 往队列放进去一个元素以后就一直
wait
直到有其他线程进来把这个元素取走。 - offer(): 往队列里放一个元素后立即返回,如果碰巧这个元素被另一个线程取走了,
offer
方法返回true
,否则返回false
。 - take(): 取出并且
remove
掉队列里的元素,取不到东西他会一直等。 - poll(): 取出并且
remove
掉队列里的元素,只有到碰巧另外一个线程正在往队列里offer
数据或者put
数据的时候,该方法才会取到东西。否则立即返回null
。 - isEmpty(): 永远返回
true
。 - remove() & removeAll(): 永远返回
false
。
4.2.6 小结
BlockingQueue
的实现类各有其特点和适用场景:
- ArrayBlockingQueue: 适用于固定大小的队列,内部使用数组结构。
- LinkedBlockingQueue: 适用于动态大小的队列,内部使用链表结构。
- DelayQueue: 适用于需要延迟处理的场景,元素必须实现
Delayed
接口。 - PriorityBlockingQueue: 适用于需要优先级排序的场景,内部使用非公平锁。
- SynchronousQueue: 适用于需要严格配对操作的场景,没有任何内部容量。
5 CopyOnWrite 容器
CopyOnWrite
容器是一种写时复制的容器。当有多个调用者同时请求一个资源数据时,如果某个调用者需要对数据进行修改,系统会复制一个当前数据源的副本供其修改,然后将原容器的引用指向新容器。
CopyOnWrite
容器适用于“读多写少”的并发场景,因为它允许在读操作时无需加锁,从而提高了读操作的性能。
JDK 提供了两个 CopyOnWrite
容器:CopyOnWriteArrayList
和 CopyOnWriteArraySet
。
6 总结
本文详细介绍了 Java 并发集合容器中的几个重要类,包括 ConcurrentHashMap
、阻塞队列和 CopyOnWrite
容器。这些容器通过内部的同步机制实现了线程安全,简化了并发编程的复杂性。在实际开发中,根据具体的应用场景选择合适的并发容器,可以显著提高程序的性能和可维护性。
7 思维导图
8 参考链接
聊聊Java的并发集合容器ConcurrentHashMap、阻塞队列和 CopyOnWrite 容器
标签:容器,Java,队列,元素,阻塞,并发,线程 From: https://blog.csdn.net/gaosw0521/article/details/144021181