目录
5. ConcurrentSkipListMap 和 ConcurrentSkipListSet
Collections
并发集合是 Java 中为了解决多线程并发问题而提供的一组集合类,它们属于 Java 并发包 (java.util.concurrent
),这些集合类通过内部的同步机制确保了在多线程环境下对集合进行操作时的安全性。
一.为什么需要并发集合?
在多线程编程中,多个线程可能同时对共享数据进行读写操作。如果使用普通的集合类(例如 ArrayList
或 HashMap
),在多个线程并发访问时可能会出现数据不一致、线程不安全等问题。为了解决这些问题,Java 提供了线程安全的并发集合类。
主要的并发集合:
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentLinkedQueue
ConcurrentSkipListMap
ConcurrentSkipListSet
三.Collections集合类的使用:
下图是大体集合框架:
1. ConcurrentHashMap
特点:
- 支持并发读写,保证线程安全。
- 使用了分段锁机制,而不是对整个集合加锁,从而提高了并发性能。
- 它的读操作不需要锁定(完全并发),而写操作在需要时会锁定段。
- 对于多线程并发访问 HashMap 的场景,
ConcurrentHashMap
是最佳选择。
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(2);
// 多线程写入数据
executor.submit(() -> map.put("Apple", 1));
executor.submit(() -> map.put("Banana", 2));
// 多线程读取数据
executor.submit(() -> System.out.println(map.get("Apple")));
executor.submit(() -> System.out.println(map.get("Banana")));
executor.shutdown();
}
}
内部实现原理:
ConcurrentHashMap
通过使用 分段锁(Java 8 之后使用 CAS 和synchronized
)来减少线程冲突,每个线程可以独立锁定某个段进行修改,而无需对整个 Map 加锁。- 读操作不需要锁定,写操作会在段级别加锁,因此多个线程可以同时进行读操作和不同段的写操作,从而提高了并发性能。
为什么HashTable慢?
Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。
适用场景:
- 适用于多线程环境下对
HashMap
的高频访问(尤其是读操作多,写操作少的情况)。 - 典型应用场景:缓存、频繁的读操作场景。
2. CopyOnWriteArrayList
特点:
- 在写操作(如
add()
或set()
)时,会创建当前数组的一个副本并修改副本,写完之后将副本替换掉原数组。 - 读操作不需要加锁,因为读操作永远不会修改现有的数组。
- 由于其写时复制的特性,适合读多写少的场景。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
// 线程安全的读取和修改操作
for (String item : list) {
System.out.println(item);
}
list.add("C");
System.out.println(list);
}
}
内部实现原理:
- 每次进行写操作时,会复制整个数组,修改后再将引用指向新数组。
- 读操作无需加锁,因为读取的是一份不会被修改的旧数组。
- 写操作的代价较高,因为每次写入时需要复制整个数组,适用于写操作较少的场景。
CopyOnWriteArrayList的缺陷和使用场景
CopyOnWriteArrayList 有几个缺点:
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用
因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
3. ConcurrentLinkedQueue
特点:
- 基于 无界非阻塞队列,采用 CAS 操作,是一个线程安全的队列。
- 适合高并发场景下的队列实现。
- 其性能较好,且不需要加锁,因为使用了乐观锁和 CAS 操作。
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConcurrentLinkedQueueExample {
public static void main(String[] args) {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.add("Task1");
queue.add("Task2");
System.out.println(queue.poll()); // 输出并移除元素
System.out.println(queue.peek()); // 仅输出元素,不移除
}
}
内部实现原理:
- 使用 CAS (Compare and Swap) 操作来确保队列的线程安全,依赖于原子操作,无需加锁。
- 头节点和尾节点 使用引用变量来进行连接,队列的操作(如
offer()
、poll()
)都依赖 CAS 操作来保证线程安全。
适用场景:
- 适用于高并发的队列处理场景,如消息队列、任务调度等。
4. CopyOnWriteArraySet
特点:
- 基于
CopyOnWriteArrayList
实现,具有相同的写时复制特性。 - 通过
CopyOnWriteArrayList
的特性来确保线程安全的 Set。 - 由于 Set 的性质,
CopyOnWriteArraySet
不允许存储重复元素。
代码示例:
import java.util.concurrent.CopyOnWriteArraySet;
public class CopyOnWriteArraySetExample {
public static void main(String[] args) {
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
set.add("A");
set.add("B");
set.add("A"); // 无效操作,Set不允许重复
for (String s : set) {
System.out.println(s);
}
}
}
内部实现原理:
- 继承自
CopyOnWriteArrayList
,通过对列表的写时复制来实现线程安全的 Set 集合。 - 写操作时,会复制整个底层数组并进行修改。
适用场景:
- 适用于需要高并发读取,但写操作相对较少的 Set 操作场景。
5. ConcurrentSkipListMap
和 ConcurrentSkipListSet
特点:
- 有序的并发集合,基于跳表(Skip List)实现,提供了有序的
Map
和Set
操作。 - 它们保证了所有元素按自然顺序排序或通过提供的比较器进行排序。
- 提供较高的并发性,适合对有序数据进行并发访问的场景。
import java.util.concurrent.ConcurrentSkipListMap;
public class ConcurrentSkipListMapExample {
public static void main(String[] args) {
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
map.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
内部实现原理:
- 使用 跳表 数据结构来实现有序的存储,跳表是一种随机化数据结构,能在 O(log n) 时间内完成查找、插入和删除操作。
- 并发性通过 CAS 操作和分段锁机制来保证。
适用场景:
- 适用于需要按顺序存储和访问数据的并发环境,如有序缓存、排序任务调度等。
6. BlockingQueue
BlockingQueue
是一个支持阻塞操作的队列,常用于生产者-消费者模型。常见的实现有 ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityBlockingQueue
等。
使用步骤:
- 创建
BlockingQueue
实例,如ArrayBlockingQueue
或LinkedBlockingQueue
。 - 可以使用
put()
和take()
方法在多线程环境下实现安全的生产者-消费者模型。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put(i); // 阻塞操作,队列满时等待
System.out.println("Produced: " + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
int value = queue.take(); // 阻塞操作,队列空时等待
System.out.println("Consumed: " + value);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
特点:
BlockingQueue
适用于需要阻塞行为的场景,如生产者等待队列有空闲空间、消费者等待队列有元素。- 常用的实现包括
ArrayBlockingQueue
(定长)和LinkedBlockingQueue
(可变长)。
总结
并发集合是 Java 中处理多线程并发问题的有效工具。根据不同的使用场景,可以选择合适的并发集合来提高系统的并发性能。
集合类型 | 适用场景 | 特点 |
---|---|---|
ConcurrentHashMap |
多线程访问共享的哈希映射表,读多写少 | 分段锁,提升并发性能 |
CopyOnWriteArrayList |
读多写少,修改较少的列表场景 | 写时复制,读操作无锁 |
CopyOnWriteArraySet |
读多写少,修改较少的集合场景 | 基于 CopyOnWriteArrayList 实现 |
ConcurrentLinkedQueue |
高并发的队列操作 | 无锁非阻塞队列,使用 CAS 实现 |