码到三十五 :
个人主页
心中有诗画,指尖舞代码,目光览世界,步履越千山,人间尽值得 !
在Java的并发编程中,
ConcurrentSkipListSet
是一个提供高并发访问能力的有序集合实现。它基于Skip List(跳表)数据结构,并通过精细的锁策略实现了高效的并发操作。本文将深入探讨ConcurrentSkipListSet
的内部实现、特性、使用场景以及与其他并发集合的比较。
目录
- 1️⃣Skip List简介
- 2️⃣ConcurrentSkipListSet的特性
- 3️⃣内部实现
- 4️⃣使用场景
- 5️⃣与其他并发集合的比较
- 5.1 ConcurrentSkipListSet vs. CopyOnWriteArraySet
- 5.2 ConcurrentSkipListSet vs. Collections.synchronizedSortedSet
- 6️⃣ConcurrentSkipListSet模拟调度系统
- 总结
1️⃣Skip List简介
在了解ConcurrentSkipListSet
之前,我们首先需要了解Skip List(跳表)数据结构。Skip List是一种可以在对数期望时间内完成搜索、插入、删除等操作的数据结构。它通过维护多个指向其他元素的“跳跃”引用,实现了在多个层次上的快速访问。
2️⃣ConcurrentSkipListSet的特性
ConcurrentSkipListSet
是Java并发包java.util.concurrent
中的一个类,它实现了NavigableSet
接口。这个类的主要特性包括:
- 并发性:
ConcurrentSkipListSet
的设计允许多个线程同时访问集合,并且可以在不阻塞其他线程的情况下进行插入、删除和查找操作。 - 有序性:集合中的元素根据它们的自然顺序或者通过构造函数提供的
Comparator
进行排序。 - 高效的查找和遍历:基于Skip List数据结构,
ConcurrentSkipListSet
提供了对数级别的查找时间复杂度,同时支持高效的顺序和逆序遍历。 - 不支持
null
元素:与大多数集合实现一样,ConcurrentSkipListSet
也不允许插入null
元素。
3️⃣内部实现
ConcurrentSkipListSet
的内部实现基于ConcurrentSkipListMap
。实际上,ConcurrentSkipListSet
只是对ConcurrentSkipListMap
的一个简单封装,其中键(Key)是集合中的元素,而值(Value)则是一个固定的占位符对象(通常是Boolean.TRUE
)。
ConcurrentSkipListMap
的实现非常复杂,涉及多个内部类和精细的锁策略。它使用了一种称为“分段锁”的技术,将Skip List分成多个段,每个段都可以独立地加锁和解锁。这种设计允许多个线程并发地访问不同的段,从而提高了并发性能。
此外,ConcurrentSkipListMap
还使用了一种称为“乐观锁”的技术来优化读操作。在读操作时,它不会立即获取锁,而是先尝试无锁地读取数据。只有当可能存在并发修改时,才会加锁并重新读取数据。这种优化可以减少不必要的锁竞争,从而提高读操作的性能。
4️⃣使用场景
ConcurrentSkipListSet
适用于需要高并发访问和有序性的场景。例如,在一个多线程的系统中,如果有一个需要频繁插入、删除和查找有序元素的集合,那么ConcurrentSkipListSet
可能是一个很好的选择。
然而,需要注意的是,由于ConcurrentSkipListSet
的内部实现相对复杂,因此在某些情况下,它的性能可能不如其他简单的并发集合实现(如ConcurrentHashMap
的keySet()
视图)。因此,在选择并发集合实现时,需要根据具体的使用场景和需求进行权衡。
5️⃣与其他并发集合的比较
5.1 ConcurrentSkipListSet vs. CopyOnWriteArraySet
CopyOnWriteArraySet
是另一个提供并发访问能力的有序集合实现。然而,与ConcurrentSkipListSet
不同的是,CopyOnWriteArraySet
是通过在每次修改时复制整个底层数组来实现并发性的。这种设计使得CopyOnWriteArraySet
的读操作非常高效(不需要加锁),但写操作的性能会随着集合大小的增加而下降。因此,CopyOnWriteArraySet
更适合于读多写少的场景。
5.2 ConcurrentSkipListSet vs. Collections.synchronizedSortedSet
Collections.synchronizedSortedSet
是Java标准库提供的一个同步的有序集合包装器。它可以通过在任意SortedSet
实现上调用Collections.synchronizedSortedSet()
方法来创建。然而,与ConcurrentSkipListSet
相比,synchronizedSortedSet
的并发性能通常要低得多,因为它在每个方法调用上都会获取全局锁。因此,在高并发的场景下,ConcurrentSkipListSet
通常是一个更好的选择。
6️⃣ConcurrentSkipListSet模拟调度系统
下面的代码模拟了一个多线程环境下的任务调度系统,其中任务按照优先级进行排序,并且可以随时添加新任务或取消已有任务。我们使用ConcurrentSkipListSet
来存储和管理这些任务。
import java.util.concurrent.ConcurrentSkipListSet;
// 任务类,实现Comparable接口以便排序
class Task implements Comparable<Task> {
private final int priority; // 优先级
private final String description; // 任务描述
public Task(int priority, String description) {
this.priority = priority;
this.description = description;
}
public int getPriority() {
return priority;
}
public String getDescription() {
return description;
}
// 根据优先级进行排序,优先级高的任务排在前面
@Override
public int compareTo(Task other) {
// 注意这里我们使用Integer.compare进行比较,以避免整数溢出问题
return Integer.compare(other.priority, this.priority);
}
@Override
public String toString() {
return "Task{" + "priority=" + priority + ", description='" + description + '\'' + '}';
}
}
// 任务调度器类
class TaskScheduler {
private final ConcurrentSkipListSet<Task> taskSet; // 使用ConcurrentSkipListSet存储任务
public TaskScheduler() {
taskSet = new ConcurrentSkipListSet<>();
}
// 添加任务
public void addTask(Task task) {
taskSet.add(task);
System.out.println("添加任务: " + task);
}
// 取消任务
public void cancelTask(Task task) {
taskSet.remove(task);
System.out.println("取消任务: " + task);
}
// 显示当前所有任务(按优先级排序)
public void showTasks() {
System.out.println("当前任务列表(按优先级排序):");
for (Task task : taskSet) {
System.out.println(task);
}
}
// 执行最高优先级的任务(如果有的话)
public void executeHighestPriorityTask() {
// 由于ConcurrentSkipListSet是有序的,第一个元素就是最高优先级的任务
Task highestPriorityTask = taskSet.first();
if (highestPriorityTask != null) {
System.out.println("执行最高优先级的任务: " + highestPriorityTask);
// 这里只是模拟执行任务,实际上应该有一个线程池来执行任务,并从集合中移除它
// 但由于ConcurrentSkipListSet不支持在遍历过程中直接移除元素,我们需要额外的逻辑来处理这个任务移除的问题。
// 为了简单起见,这里我们只打印任务信息而不实际移除它。
// 在真实场景中,你可能需要使用一个额外的数据结构(如队列)来处理任务执行和移除的逻辑。
} else {
System.out.println("当前没有任务可执行。");
}
}
}
// 测试类
public class TaskSchedulerTest {
public static void main(String[] args) throws InterruptedException {
TaskScheduler scheduler = new TaskScheduler();
// 创建并添加一些任务
scheduler.addTask(new Task(3, "编写文档"));
scheduler.addTask(new Task(5, "修复bug"));
scheduler.addTask(new Task(1, "回复邮件"));
// 显示当前任务列表
scheduler.showTasks();
// 执行最高优先级的任务(应该是“修复bug”)
scheduler.executeHighestPriorityTask();
// 取消一个任务
Task taskToCancel = new Task(3, "编写文档"); // 注意:这里我们创建了一个新的Task对象来尝试取消任务,这实际上是不正确的做法。
// 在真实场景中,你应该保存对原始Task对象的引用,并使用该引用来取消任务。因为Task的equals和hashCode方法没有被重写,所以这里无法正确取消任务。
// 为了演示目的,我们假设这里能够正确取消任务(但在实际代码中这是不会发生的)。
// 正确的做法是在添加任务时保存Task对象的引用,并在需要时使用该引用来取消任务。或者重写Task类的equals和hashCode方法以支持按值比较。
// 但由于这个案例的重点是展示ConcurrentSkipListSet的使用,所以我们不在这里深入讨论这个问题。
scheduler.cancelTask(taskToCancel); // 注意:这里实际上并没有取消任何任务,因为taskToCancel并不是集合中的一个元素(它们是不同的对象实例)。正确的做法是使用原始Task对象的引用来取消任务。
// 再次显示当前任务列表(应该不包含已取消的任务)
scheduler.showTasks(); // 注意:由于上面的取消操作没有成功,所以这里的任务列表仍然包含原始的所有任务。如果取消操作成功的话,这里应该只显示剩下的任务。
}
}
注意:
上面的代码中我们无法正确地取消任务。这是因为我们没有重写Task
类的equals()
和hashCode()
方法,所以我们无法根据任务的内容找到并取消它。在真实的应用中,我们需要重写这些方法以便能够正确地识别和取消任务。
另外,由于ConcurrentSkipListSet
不支持在遍历过程中直接修改集合(例如移除元素),所以在执行和移除任务时我们需要额外的逻辑来处理这个问题。这通常可以通过使用额外的数据结构(如队列或锁)来实现。
总结
ConcurrentSkipListSet
是Java并发编程中的一个强大工具,它提供了高并发访问能力和有序性。然而,由于其内部实现的复杂性,它在某些情况下的性能可能不如其他简单的并发集合实现。因此,在选择并发集合实现时,需要根据具体的使用场景和需求进行权衡。