揭秘 Java ConcurrentSkipListMap:从源码洞悉其精妙原理
一、引言
在 Java 编程的世界里,高效且线程安全的数据结构是处理复杂并发场景的关键。ConcurrentSkipListMap 作为 Java 并发包(java.util.concurrent)中的一颗璀璨明星,为开发者提供了一种在多线程环境下进行有序键值对存储和操作的强大工具。与传统的 HashMap 不同,ConcurrentSkipListMap 不仅能保证线程安全,还能根据键的自然顺序或指定的比较器对键值对进行排序。这使得它在需要有序存储和并发访问的场景中表现卓越,例如实现排行榜、范围查询等功能。
本文将深入剖析 ConcurrentSkipListMap 的源码,详细解读其内部结构、核心操作原理、性能特点以及使用场景等方面的内容。通过对源码的细致分析,你将全面了解 ConcurrentSkipListMap 的工作机制,从而在实际开发中更加得心应手地运用它。
二、ConcurrentSkipListMap 概述
2.1 什么是 ConcurrentSkipListMap
ConcurrentSkipListMap 是 Java 并发包中的一个类,它实现了 NavigableMap 接口,继承自 AbstractMap 类。ConcurrentSkipListMap 基于跳表(Skip List)数据结构实现,跳表是一种随机化的数据结构,它通过在每个节点中维护多个指向其他节点的指针,从而实现了快速的查找、插入和删除操作。
ConcurrentSkipListMap 与 TreeMap 类似,都能保证键的有序性,但 TreeMap 是非线程安全的,而 ConcurrentSkipListMap 是线程安全的,适用于多线程环境下的并发操作。
2.2 特点与优势
- 线程安全:
ConcurrentSkipListMap是线程安全的,多个线程可以同时对其进行插入、删除和查找操作,而不需要额外的同步机制。这使得它在多线程环境下能够高效地工作,避免了数据不一致的问题。 - 有序性:
ConcurrentSkipListMap会根据键的自然顺序或者指定的比较器对键值对进行排序,保证键按照顺序存储和访问。这在需要有序键值对的场景中非常有用,例如实现排行榜、范围查询等功能。 - 高效的操作:由于采用了跳表数据结构,
ConcurrentSkipListMap在插入、删除和查找操作上的平均时间复杂度为 O(log n),其中 n 是映射中键值对的数量。这使得它在处理大量数据时也能保持较好的性能。
2.3 基本使用示例
import java.util.concurrent.ConcurrentSkipListMap;
public class ConcurrentSkipListMapExample {
public static void main(String[] args) {
// 创建一个 ConcurrentSkipListMap 实例,使用键的自然顺序进行排序
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
// 向映射中添加键值对
map.put(3, "Three");
map.put(1, "One");
map.put(2, "Two");
// 遍历映射,键将按升序输出
for (Integer key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 检查映射中是否包含某个键
boolean containsKey = map.containsKey(2);
System.out.println("Map contains key 2: " + containsKey);
// 移除映射中的某个键值对
map.remove(1);
System.out.println("Map after removing key 1: " + map);
}
}
在上述示例中,我们创建了一个 ConcurrentSkipListMap 实例,并向其中添加了一些键值对。然后,我们遍历映射,可以看到键是按照升序输出的。接着,我们检查映射中是否包含某个键,并移除了一个键值对。
三、ConcurrentSkipListMap 源码结构分析
3.1 类的定义与继承关系
// ConcurrentSkipListMap 类继承自 AbstractMap 类,并实现了 NavigableMap、Cloneable 和 Serializable 接口
public class ConcurrentSkipListMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
// 比较器
final Comparator<? super K> comparator;
// 头节点
transient volatile HeadIndex<K,V> head;
// 构造函数,使用键的自然顺序进行排序
public ConcurrentSkipListMap() {
// 初始化比较器为 null,表示使用键的自然顺序
this.comparator = null;
// 初始化头节点
initialize();
}
// 构造函数,使用指定的比较器进行排序
public ConcurrentSkipListMap(Comparator<? super K> comparator) {
// 初始化比较器
this.comparator = comparator;
// 初始化头节点
initialize();
}
// 其他构造函数和方法的定义...
}
从上述源码可以看出,ConcurrentSkipListMap 继承自 AbstractMap 类,并实现了 NavigableMap、Cloneable 和 Serializable 接口。它包含一个比较器 comparator 和一个头节点 head。比较器用于确定键的顺序,头节点是跳表的起始节点。
3.2 重要成员变量和方法概述
- comparator:比较器,用于确定键的顺序。如果为
null,则使用键的自然顺序。 - head:头节点,是跳表的起始节点。
- put(K key, V value):向映射中插入一个键值对。
- get(Object key):根据键获取对应的值。
- remove(Object key):从映射中移除指定键的键值对。
- containsKey(Object key):检查映射中是否包含某个键。
- firstKey():返回映射中的第一个键。
- lastKey():返回映射中的最后一个键。
- subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive):返回一个指定范围的子映射。
下面我们将详细分析这些方法的实现原理。
四、ConcurrentSkipListMap 核心操作原理分析
4.1 插入元素操作
4.1.1 put 方法
// 向映射中插入一个键值对
public V put(K key, V value) {
// 检查键是否为 null,如果为 null,抛出 NullPointerException 异常
if (key == null)
throw new NullPointerException();
// 调用 doPut 方法进行插入操作
return doPut(key, value, false);
}
// 插入键值对的核心方法
private V doPut(K key, V value, boolean onlyIfAbsent) {
// 键值对节点
Node<K,V> z; // added node
// 检查键是否为 null,如果为 null,抛出 NullPointerException 异常
if (key == null)
throw new NullPointerException();
// 比较器
Comparator<? super K> cmp = comparator;
outer: for (;;) {
// 从最顶层的头节点开始查找插入位置
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 如果找到的节点不为 null
if (n != null) {
// 另一个节点
Object v; int c;
// 另一个节点的后继节点
Node<K,V> f = n.next;
// 如果当前节点已经被删除,或者后继节点不等于当前节点的后继节点,说明节点状态已改变,重新查找
if (n != b.next) // inconsistent read
break;
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果当前节点的前一个节点已被删除,重新查找
if (b.value == null || v == n) // b is deleted
break;
// 比较键的大小
if ((c = cpr(cmp, key, n.key)) > 0) {
// 如果键大于当前节点的键,继续向后查找
b = n;
n = f;
continue;
}
// 如果键等于当前节点的键
if (c == 0) {
// 如果 onlyIfAbsent 为 true,且当前节点的值不为 null,直接返回当前节点的值
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
// 否则,继续尝试更新值
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
// 创建一个新的节点
z = new Node<K,V>(key, value, n);
// 尝试将新节点插入到当前节点之前
if (!b.casNext(n, z))
break; // restart if lost race to append to b
// 插入成功,跳出外层循环
break outer;
}
}
// 随机决定是否需要增加节点的层数
int rnd = ThreadLocalRandom.nextSecondarySeed();
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
int level = 1, max;
// 随机增加层数
while (((rnd >>>= 1) & 1) != 0)
++level;
HeadIndex<K,V> h = head;
// 如果随机层数超过当前最大层数
if (level > (max = h.level)) {
// 最多增加一层
level = max + 1; // hold in array and later pick the highest valid
// 创建一个新的头节点索引
HeadIndex<K,V> newh = new HeadIndex<K,V>(h.node, h.next, level);
// 尝试更新头节点
if (casHead(h, newh)) {
h = newh;
// 为新的层创建索引节点
for (int i = h.level - 1; i >= max; --i) {
newh.indexes[i] = new Index<K,V>(h.node, null, null);
}
} else {
// 更新失败,重新获取头节点
level = max;
}
}
// 创建索引节点
Index<K,V> idx = null;
for (int i = 1; i <= level; ++i) {
idx = new Index<K,V>(z, idx, null);
}
// 插入索引节点
splice(z, idx, level);
}
// 返回 null,表示插入成功
return null;
}
// 比较两个键的大小
private static <K> int cpr(Comparator<? super K> cmp, K k1, K k2) {
// 如果比较器不为 null,使用比较器比较
return (cmp != null)? cmp.compare(k1, k2)
// 否则,使用键的自然顺序比较
: ((Comparable<? super K>)k1).compareTo(k2);
}
put 方法首先调用 doPut 方法进行插入操作。doPut 方法的核心逻辑如下:
- 从最顶层的头节点开始查找插入位置,通过比较键的大小,找到合适的插入位置。
- 如果找到的节点已经存在相同的键,根据
onlyIfAbsent参数决定是否更新值。 - 创建一个新的节点,并尝试将其插入到合适的位置。
- 随机决定是否需要增加节点的层数,如果需要,创建新的头节点和索引节点。
- 插入索引节点,完成插入操作。
4.2 查找元素操作
4.2.1 get 方法
// 根据键获取对应的值
public V get(Object key) {
// 调用 getNode 方法查找节点
Node<K,V> p = getNode(key);
// 返回节点的值,如果节点为 null,返回 null
return (p == null)? null : p.value;
}
// 根据键查找节点
private Node<K,V> getNode(Object key) {
// 检查键是否为 null,如果为 null,抛出 NullPointerException 异常
if (key == null)
throw new NullPointerException();
// 比较器
Comparator<? super K> cmp = comparator;
// 从最顶层的头节点开始查找
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 如果找到的节点不为 null
if (n != null) {
// 另一个节点
Object v; int c;
// 另一个节点的后继节点
Node<K,V> f = n.next;
// 如果当前节点已经被删除,或者后继节点不等于当前节点的后继节点,说明节点状态已改变,重新查找
if (n != b.next) // inconsistent read
break;
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果当前节点的前一个节点已被删除,重新查找
if (b.value == null || v == n) // b is deleted
break;
// 比较键的大小
if ((c = cpr(cmp, key, n.key)) > 0) {
// 如果键大于当前节点的键,继续向后查找
b = n;
n = f;
continue;
}
// 如果键等于当前节点的键,返回该节点
if (c == 0)
return n;
// else c < 0; key not found
break;
}
// 未找到节点,返回 null
return null;
}
// 重新查找
return getNode(key);
}
get 方法通过调用 getNode 方法查找节点。getNode 方法的核心逻辑如下:
- 从最顶层的头节点开始查找,通过比较键的大小,找到合适的节点。
- 如果找到的节点已经被删除,帮助删除该节点并重新查找。
- 如果找到相同的键,返回该节点;否则返回
null。
4.3 删除元素操作
4.3.1 remove 方法
// 移除指定键的键值对
public V remove(Object key) {
// 调用 doRemove 方法进行删除操作
return doRemove(key, null);
}
// 删除键值对的核心方法
private V doRemove(Object key, Object value) {
// 如果键为 null,抛出 NullPointerException 异常
if (key == null)
throw new NullPointerException();
// 比较器
Comparator<? super K> cmp = comparator;
outer: for (;;) {
// 从最顶层的头节点开始查找删除位置
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 如果找到的节点不为 null
if (n != null) {
// 另一个节点
Object v; int c;
// 另一个节点的后继节点
Node<K,V> f = n.next;
// 如果当前节点已经被删除,或者后继节点不等于当前节点的后继节点,说明节点状态已改变,重新查找
if (n != b.next) // inconsistent read
break;
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果当前节点的前一个节点已被删除,重新查找
if (b.value == null || v == n) // b is deleted
break;
// 比较键的大小
if ((c = cpr(cmp, key, n.key)) > 0) {
// 如果键大于当前节点的键,继续向后查找
b = n;
n = f;
continue;
}
// 如果键不等于当前节点的键,说明未找到要删除的节点,跳出外层循环
if (c < 0)
break outer;
// 如果值不匹配,跳出外层循环
if (value != null && !value.equals(v))
break outer;
// 尝试将当前节点的值置为 null
if (!n.casValue(v, null))
break;
// 尝试将当前节点从链表中移除
if (!n.appendMarker(f) || !b.casNext(n, f))
// 移除失败,重新查找
findNode(key); // Retry via findNode
else {
// 移除成功,清理索引节点
findPredecessor(key, cmp); // Clean index
// 如果头节点的后继节点为空,尝试降低头节点的层数
if (head.level > 1 && head.next == null)
tryReduceLevel();
}
// 返回被删除节点的值
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
// 未找到要删除的节点,跳出外层循环
break outer;
}
}
// 未找到要删除的节点,返回 null
return null;
}
remove 方法通过调用 doRemove 方法进行删除操作。doRemove 方法的核心逻辑如下:
- 从最顶层的头节点开始查找删除位置,通过比较键的大小,找到要删除的节点。
- 如果找到的节点已经被删除,帮助删除该节点并重新查找。
- 如果找到相同的键,尝试将节点的值置为
null,并将节点从链表中移除。 - 移除成功后,清理索引节点,并尝试降低头节点的层数。
- 返回被删除节点的值;如果未找到要删除的节点,返回
null。
4.4 遍历操作
4.4.1 迭代器的实现
ConcurrentSkipListMap 提供了迭代器来遍历映射中的键值对。以下是迭代器的部分源码:
// 键值对迭代器类
private final class EntryIterator extends AscendingIterator
implements Iterator<Map.Entry<K,V>> {
// 获取下一个键值对
public Map.Entry<K,V> next() {
// 调用父类的 nextNode 方法获取下一个节点
Node<K,V> p = nextNode();
// 返回节点的键值对
return new AbstractMap.SimpleImmutableEntry<K,V>(p.key, p.value);
}
}
// 升序迭代器类
abstract class AscendingIterator {
// 下一个要访问的节点
Node<K,V> next;
// 最后访问的节点
Node<K,V> lastReturned;
// 构造函数,初始化迭代器
AscendingIterator() {
// 从最底层的头节点开始查找第一个有效节点
advance();
}
// 查找下一个有效节点
final void advance() {
// 从当前节点的后继节点开始查找
Node<K,V> e = next;
// 如果当前节点为空,从最底层的头节点开始查找
if (e == null)
e = findFirst();
else
e = e.next;
// 查找下一个有效节点
for (;;) {
// 如果节点为空,说明没有更多节点了,将 next 置为 null
if (e == null) {
next = null;
break;
}
// 获取节点的值
Object v = e.value;
// 如果节点的值不为 null,说明是有效节点,将 next 置为该节点,跳出循环
if (v != null) {
next = e;
break;
}
// 如果节点的值为 null,说明节点已被删除,继续向后查找
e = e.next;
}
}
// 获取下一个节点
final Node<K,V> nextNode() {
// 获取下一个要访问的节点
Node<K,V> e = next;
// 如果节点为空,抛出 NoSuchElementException 异常
if (e == null)
throw new NoSuchElementException();
// 记录最后访问的节点
lastReturned = e;
// 查找下一个有效节点
advance();
// 返回当前节点
return e;
}
// 判断是否还有下一个元素
public final boolean hasNext() {
// 如果下一个节点不为 null,说明还有下一个元素
return next != null;
}
// 移除当前元素
public void remove() {
// 获取最后访问的节点
Node<K,V> p = lastReturned;
// 如果最后访问的节点为空,抛出 IllegalStateException 异常
if (p == null)
throw new IllegalStateException();
// 删除最后访问的节点
remove(p.key);
// 将最后访问的节点置为 null
lastReturned = null;
}
}
// 查找第一个有效节点
private Node<K,V> findFirst() {
// 从最底层的头节点开始查找
for (;;) {
// 获取最底层的头节点
Node<K,V> b = head.node;
// 获取头节点的后继节点
Node<K,V> n = b.next;
// 如果后继节点为空,说明没有元素了,返回 null
if (n == null)
return null;
// 获取后继节点的值
Object v = n.value;
// 如果后继节点的值不为 null,说明是有效节点,返回该节点
if (v != null)
return n;
// 如果后继节点的值为 null,说明节点已被删除,帮助删除该节点
n.helpDelete(b, n.next);
}
}
EntryIterator 继承自 AscendingIterator,用于遍历映射中的键值对。AscendingIterator 是一个抽象类,实现了基本的迭代逻辑。advance 方法用于查找下一个有效节点,nextNode 方法用于获取下一个节点,hasNext 方法用于判断是否还有下一个元素,remove 方法用于移除当前元素。findFirst 方法用于查找第一个有效节点。
4.5 范围查询操作
4.5.1 subMap 方法
// 获取指定范围的子映射
public NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive) {
// 检查键是否为 null,如果为 null,抛出 NullPointerException 异常
if (fromKey == null || toKey == null)
throw new NullPointerException();
// 调用 subMap 方法创建子映射
return new SubMap<K,V>(this, false, fromKey, fromInclusive,
toKey, toInclusive);
}
// 子映射类
static final class SubMap<K,V> extends AbstractNavigableMap<K,V> {
// 父映射
final ConcurrentSkipListMap<K,V> m;
// 是否为逆序
final boolean descending;
// 起始键
final K fromKey;
// 起始键是否包含
final boolean fromInclusive;
// 结束键
final K toKey;
// 结束键是否包含
final boolean toInclusive;
// 构造函数,初始化子映射
SubMap(ConcurrentSkipListMap<K,V> m, boolean descending,
K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive) {
// 初始化父映射
this.m = m;
// 初始化是否为逆序
this.descending = descending;
// 初始化起始键
this.fromKey = fromKey;
// 初始化起始键是否包含
this.fromInclusive = fromInclusive;
// 初始化结束键
this.toKey = toKey;
// 初始化结束键是否包含
this.toInclusive = toInclusive;
}
// 获取子映射的大小
public int size() {
// 调用父映射的 size 方法
return m.size();
}
// 判断子映射是否为空
public boolean isEmpty() {
// 调用父映射的 isEmpty 方法
return m.isEmpty();
}
// 判断子映射是否包含某个键
public boolean containsKey(Object key) {
// 检查键是否在范围内
if (!inRange(key))
return false;
// 调用父映射的 containsKey 方法
return m.containsKey(key);
}
// 向子映射中插入一个键值对
public V put(K key, V value) {
// 检查键是否在范围内
if (!inRange(key))
throw new IllegalArgumentException();
// 调用父映射的 put 方法
return m.put(key, value);
}
// 从子映射中移除指定键的键值对
public V remove(Object key) {
// 检查键是否在范围内
if (!inRange(key))
return null;
// 调用父映射的 remove 方法
return m.remove(key);
}
// 清空子映射
public void clear() {
// 调用父映射的 clear 方法
m.clear();
}
// 获取子映射的键集
public Set<K> keySet() {
// 返回键集
return navigableKeySet();
}
// 获取子映射的逆序键集
public NavigableSet<K> descendingKeySet() {
// 返回逆序子映射的键集
return descendingMap().navigableKeySet();
}
// 获取子映射的迭代器
public Iterator<Map.Entry<K,V>> iterator() {
// 如果是升序,返回升序迭代器
if (!descending)
return new EntryIterator(m, getFirst(), toKey, toInclusive);
// 否则,返回逆序迭代器
else
return new DescendingEntryIterator(m, getLast(), fromKey, fromInclusive);
}
// 获取子映射的逆序迭代器
public Iterator<Map.Entry<K,V>> descendingIterator() {
// 如果是升序,返回逆序迭代器
if (!descending)
return new DescendingEntryIterator(m, getLast(), fromKey, fromInclusive);
// 否则,返回升序迭代器
else
return new EntryIterator(m, getFirst(), toKey, toInclusive);
}
// 获取子映射的第一个键值对
public Map.Entry<K,V> firstEntry() {
// 获取第一个键值对
Node<K,V> p = getFirst();
// 如果键值对为空,返回 null
return (p == null)? null : new AbstractMap.SimpleImmutableEntry<K,V>(p.key, p.value);
}
// 获取子映射的最后一个键值对
public Map.Entry<K,V> lastEntry() {
// 获取最后一个键值对
Node<K,V> p = getLast();
// 如果键值对为空,返回 null
return (p == null)? null : new AbstractMap.SimpleImmutableEntry<K,V>(p.key, p.value);
}
// 获取子映射的比较器
public Comparator<? super K> comparator() {
// 调用父映射的 comparator 方法
return m.comparator();
}
// 获取指定范围的子子映射
public NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive) {
// 检查键是否在范围内
if (!inBounds(fromKey, fromInclusive) ||
!inBounds(toKey, toInclusive))
throw new IllegalArgumentException();
// 调用 subMap 方法创建子子映射
return new SubMap<K,V>(m, descending, fromKey, fromInclusive,
toKey, toInclusive);
}
// 获取小于指定键的子映射
public NavigableMap<K,V> headMap(K toKey, boolean inclusive) {
// 检查键是否在范围内
if (!inBounds(toKey, inclusive))
throw new IllegalArgumentException();
// 调用 headMap 方法创建子映射
return new SubMap<K,V>(m, descending,
descending? toKey : fromKey,
descending? inclusive : fromInclusive,
descending? fromKey : toKey,
descending? fromInclusive : inclusive);
}
// 获取大于等于指定键的子映射
public NavigableMap<K,V> tailMap(K fromKey, boolean inclusive) {
// 检查键是否在范围内
if (!inBounds(fromKey, inclusive))
throw new IllegalArgumentException();
// 调用 tailMap 方法创建子映射
return new SubMap<K,V>(m, descending,
descending? toKey : fromKey,
descending? inclusive : fromInclusive,
descending? fromKey : toKey,
descending? fromInclusive : inclusive);
}
// 检查键是否在范围内
private boolean inRange(Object key) {
// 比较器
Comparator<? super K> cmp = m.comparator;
// 比较起始键
int c1 = (fromKey == null)? -1 : cpr(cmp, key, fromKey);
// 如果起始键不包含,且键等于起始键,返回 false
if ((c1 < 0) || (!fromInclusive && c1 == 0))
return false;
// 比较结束键
int c2 = (toKey == null)? 1 : cpr(cmp, key, toKey);
// 如果结束键不包含,且键等于结束键,返回 false
if ((c2 > 0) || (!toInclusive && c2 == 0))
return false;
// 键在范围内,返回 true
return true;
}
// 检查键是否在边界内
private boolean inBounds(K key, boolean inclusive) {
// 比较器
Comparator<? super K> cmp = m.comparator;
// 比较起始键
int c1 = (fromKey == null)? -1 : cpr(cmp, key, fromKey);
// 如果起始键不包含,且键等于起始键,返回 false
if ((c1 < 0) || (!fromInclusive && c1 == 0))
return false;
// 比较结束键
int c2 = (toKey == null)? 1 : cpr(cmp, key, toKey);
// 如果结束键不包含,且键等于结束键,返回 false
if ((c2 > 0) || (!toInclusive && c2 == 0))
return false;
// 键在边界内,返回 true
return true;
}
// 获取第一个键值对
private Node<K,V> getFirst() {
// 如果是升序
if (!descending) {
// 从起始键开始查找
Node<K,V> p = m.getCeilingNode(fromKey);
// 如果键值对在范围内,返回该键值对
if (p != null && inRange(p.key))
return p;
} else {
// 从结束键开始查找
Node<K,V> p = m.getFloorNode(toKey);
// 如果键值对在范围内,返回该键值对
if (p != null && inRange(p.key))
return p;
}
// 未找到符合条件的键值对,返回 null
return null;
}
// 获取最后一个键值对
private Node<K,V> getLast() {
// 如果是升序
if (!descending) {
// 从结束键开始查找
Node<K,V> p = m.getFloorNode(toKey);
// 如果键值对在范围内,返回该键值对
if (p != null && inRange(p.key))
return p;
} else {
// 从起始键开始查找
Node<K,V> p = m.getCeilingNode(fromKey);
// 如果键值对在范围内,返回该键值对
if (p != null && inRange(p.key))
return p;
}
// 未找到符合条件的键值对,返回 null
return null;
}
}
subMap 方法用于获取指定范围的子映射。它首先检查键是否为 null,然后创建一个 SubMap 对象。SubMap 类继承自 AbstractNavigableMap,实现了子映射的基本操作。inRange 方法用于检查键是否在范围内,getFirst 方法用于获取子映射的第一个键值对,getLast 方法用于获取子映射的最后一个键值对。
4.5.2 headMap 方法
// 获取小于指定键的子映射
public NavigableMap<K,V> headMap(K toKey) {
// 调用 headMap 方法创建子映射,结束键不包含
return headMap(toKey, false);
}
// 获取小于指定键的子映射
public NavigableMap<K,V> headMap(K toKey, boolean inclusive) {
// 检查键是否为 null,如果为 null,抛出 NullPointerException 异常
if (toKey == null)
throw new NullPointerException();
// 调用 subMap 方法创建子映射
return new SubMap<K,V>(this, false, null, false,
toKey, inclusive);
}
headMap 方法用于获取小于指定键的子映射。它调用 subMap 方法创建子映射,起始键为 null,结束键为指定的键。
4.5.3 tailMap 方法
// 获取大于等于指定键的子映射
public NavigableMap<K,V> tailMap(K fromKey) {
// 调用 tailMap 方法创建子映射,起始键包含
return tailMap(fromKey, true);
}
// 获取大于等于指定键的子映射
public NavigableMap<K,V> tailMap(K fromKey, boolean inclusive) {
// 检查键是否为 null,如果为 null,抛出 NullPointerException 异常
if (fromKey == null)
throw new NullPointerException();
// 调用 subMap 方法创建子映射
return new SubMap<K,V>(this, false, fromKey, inclusive,
null, false);
}
tailMap 方法用于获取大于等于指定键的子映射。它调用
五、跳表数据结构在 ConcurrentSkipListMap 中的应用
5.1 跳表的基本概念
跳表(Skip List)是一种随机化的数据结构,它通过在每个节点中维护多个指向其他节点的指针,从而实现了快速的查找、插入和删除操作。跳表的基本思想是在普通链表的基础上,为每个节点随机地增加一些层次,使得在查找元素时可以跳过一些不必要的节点,从而提高查找效率。
跳表的每个节点包含多个指针,这些指针指向不同层次的后继节点。最底层的链表包含所有的元素,而更高层次的链表则是对底层链表的索引。通过在不同层次的链表中进行查找,可以快速定位到目标元素。
5.2 跳表在 ConcurrentSkipListMap 中的实现
在 ConcurrentSkipListMap 中,跳表的实现主要涉及以下几个类:
- Node:表示跳表中的节点,包含键、值和指向下一个节点的指针。
// 跳表节点类
static final class Node<K,V> {
// 键
final K key;
// 值
volatile Object value;
// 指向下一个节点的指针
volatile Node<K,V> next;
// 构造函数,初始化节点
Node(K key, Object value, Node<K,V> next) {
// 初始化键
this.key = key;
// 初始化值
this.value = value;
// 初始化指向下一个节点的指针
this.next = next;
}
// 尝试将当前节点的值置为 null
boolean casValue(Object cmp, Object val) {
// 使用 CAS 操作更新值
return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
}
// 尝试将当前节点的后继节点置为指定节点
boolean casNext(Node<K,V> cmp, Node<K,V> val) {
// 使用 CAS 操作更新后继节点
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// 标记节点为已删除
void appendMarker(Node<K,V> f) {
// 创建一个标记节点
Node<K,V> m = new Node<K,V>(null, null, f);
// 尝试将标记节点插入到当前节点之后
if (!casNext(f, m))
return;
// 获取当前节点的后继节点
Node<K,V> p = next;
// 如果后继节点不等于标记节点,说明节点状态已改变,帮助删除该节点
if (p != m)
helpDelete(p, f);
}
// 帮助删除节点
void helpDelete(Node<K,V> b, Node<K,V> f) {
// 如果当前节点的后继节点等于标记节点,且前一个节点的后继节点等于当前节点
if (f == next && this == b.next) {
// 尝试将前一个节点的后继节点置为标记节点的后继节点
if (f == null || casNext(f, new Node<K,V>(null, null, f))) {
// 尝试将前一个节点的后继节点置为标记节点的后继节点
b.casNext(this, f);
}
}
}
// 检查节点是否被标记为已删除
boolean isMarker() {
// 如果值为 null,说明节点被标记为已删除
return value == null;
}
// 检查节点是否为头节点
boolean isHead() {
// 如果键为 null,说明节点为头节点
return key == null;
}
// 偏移量
private static final sun.misc.Unsafe UNSAFE;
// 值的偏移量
private static final long valueOffset;
// 后继节点的偏移量
private static final long nextOffset;
static {
try {
// 获取 Unsafe 实例
UNSAFE = sun.misc.Unsafe.getUnsafe();
// 获取值的偏移量
Class<?> k = Node.class;
valueOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("value"));
// 获取后继节点的偏移量
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
// 抛出异常
throw new Error(e);
}
}
}
- Index:表示跳表中的索引节点,包含指向节点的指针、指向下一个索引节点的指针和指向下一层索引节点的指针。
// 跳表索引节点类
static class Index<K,V> {
// 指向节点的指针
final Node<K,V> node;
// 指向下一个索引节点的指针
volatile Index<K,V> right;
// 指向下一层索引节点的指针
volatile Index<K,V> down;
// 构造函数,初始化索引节点
Index(Node<K,V> node, Index<K,V> right, Index<K,V> down) {
// 初始化指向节点的指针
this.node = node;
// 初始化指向下一个索引节点的指针
this.right = right;
// 初始化指向下一层索引节点的指针
this.down = down;
}
// 尝试将当前索引节点的右指针置为指定索引节点
final boolean casRight(Index<K,V> cmp, Index<K,V> val) {
// 使用 CAS 操作更新右指针
return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);
}
// 偏移量
private static final sun.misc.Unsafe UNSAFE;
// 右指针的偏移量
private static final long rightOffset;
static {
try {
// 获取 Unsafe 实例
UNSAFE = sun.misc.Unsafe.getUnsafe();
// 获取右指针的偏移量
Class<?> k = Index.class;
rightOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("right"));
} catch (Exception e) {
// 抛出异常
throw new Error(e);
}
}
}
- HeadIndex:表示跳表的头索引节点,继承自
Index类,包含当前跳表的层数。
// 跳表头索引节点类
static final class HeadIndex<K,V> extends Index<K,V> {
// 当前跳表的层数
final int level;
// 构造函数,初始化头索引节点
HeadIndex(Node<K,V> node, Index<K,V> right, Index<K,V> down, int level) {
// 调用父类构造函数,初始化指向节点的指针、右指针和下指针
super(node, right, down);
// 初始化当前跳表的层数
this.level = level;
}
}
5.3 跳表的插入操作在 ConcurrentSkipListMap 中的实现
在 ConcurrentSkipListMap 中,插入操作的核心逻辑在 doPut 方法中,其中涉及到跳表的插入操作。具体步骤如下:
- 查找插入位置:从最顶层的头节点开始,通过比较键的大小,找到合适的插入位置。
// 从最顶层的头节点开始查找插入位置
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 如果找到的节点不为 null
if (n != null) {
// 另一个节点
Object v; int c;
// 另一个节点的后继节点
Node<K,V> f = n.next;
// 如果当前节点已经被删除,或者后继节点不等于当前节点的后继节点,说明节点状态已改变,重新查找
if (n != b.next) // inconsistent read
break;
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果当前节点的前一个节点已被删除,重新查找
if (b.value == null || v == n) // b is deleted
break;
// 比较键的大小
if ((c = cpr(cmp, key, n.key)) > 0) {
// 如果键大于当前节点的键,继续向后查找
b = n;
n = f;
continue;
}
// 如果键等于当前节点的键
if (c == 0) {
// 如果 onlyIfAbsent 为 true,且当前节点的值不为 null,直接返回当前节点的值
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
// 否则,继续尝试更新值
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
// 创建一个新的节点
z = new Node<K,V>(key, value, n);
// 尝试将新节点插入到当前节点之前
if (!b.casNext(n, z))
break; // restart if lost race to append to b
// 插入成功,跳出外层循环
break outer;
}
- 随机决定是否增加节点的层数:插入节点后,随机决定是否需要增加节点的层数。如果需要增加层数,创建新的头节点和索引节点。
// 随机决定是否需要增加节点的层数
int rnd = ThreadLocalRandom.nextSecondarySeed();
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
int level = 1, max;
// 随机增加层数
while (((rnd >>>= 1) & 1) != 0)
++level;
HeadIndex<K,V> h = head;
// 如果随机层数超过当前最大层数
if (level > (max = h.level)) {
// 最多增加一层
level = max + 1; // hold in array and later pick the highest valid
// 创建一个新的头节点索引
HeadIndex<K,V> newh = new HeadIndex<K,V>(h.node, h.next, level);
// 尝试更新头节点
if (casHead(h, newh)) {
h = newh;
// 为新的层创建索引节点
for (int i = h.level - 1; i >= max; --i) {
newh.indexes[i] = new Index<K,V>(h.node, null, null);
}
} else {
// 更新失败,重新获取头节点
level = max;
}
}
// 创建索引节点
Index<K,V> idx = null;
for (int i = 1; i <= level; ++i) {
idx = new Index<K,V>(z, idx, null);
}
// 插入索引节点
splice(z, idx, level);
}
- 插入索引节点:将新创建的索引节点插入到跳表中。
// 插入索引节点
private void splice(Node<K,V> z, Index<K,V> idx, int level) {
// 最顶层的头索引节点
HeadIndex<K,V> h = head;
// 最大层数
int max = h.level;
// 索引数组
Index<K,V>[] preds = (Index<K,V>[])new Index[max + 1];
// 索引数组
Index<K,V>[] succs = (Index<K,V>[])new Index[max + 1];
// 查找插入位置
for (int i = 1; i <= max; ++i) {
preds[i] = null;
succs[i] = null;
}
// 从最顶层的头索引节点开始查找
for (Index<K,V> q = h, r = q.right, t = idx; q != null; q = q.down, r = q.right) {
for (;;) {
// 如果右指针不为 null
if (r != null) {
// 另一个节点
Node<K,V> n = r.node;
// 比较键的大小
int c = cpr(comparator, z.key, n.key);
// 如果键大于当前节点的键,继续向右查找
if (c > 0) {
q = r;
r = r.right;
continue;
}
// 如果键小于当前节点的键,跳出内层循环
if (c == 0) {
if (r.right == null)
break;
q = r;
r = r.right;
continue;
}
}
// 记录前驱和后继索引节点
if (t != null) {
t.down = q.down;
q.down = t;
t = t.down;
}
preds[i] = q;
succs[i] = r;
break;
}
}
// 插入索引节点
for (int i = 1; i <= level; ++i) {
Index<K,V> p = preds[i], s = succs[i], newi = idx;
if (p != null) {
newi.right = s;
if (!p.casRight(s, newi))
break;
}
idx = idx.down;
}
}
5.4 跳表的查找操作在 ConcurrentSkipListMap 中的实现
在 ConcurrentSkipListMap 中,查找操作的核心逻辑在 getNode 方法中,其中涉及到跳表的查找操作。具体步骤如下:
- 从最顶层的头节点开始查找:通过比较键的大小,在不同层次的链表中进行查找,逐步缩小查找范围。
// 从最顶层的头节点开始查找
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 如果找到的节点不为 null
if (n != null) {
// 另一个节点
Object v; int c;
// 另一个节点的后继节点
Node<K,V> f = n.next;
// 如果当前节点已经被删除,或者后继节点不等于当前节点的后继节点,说明节点状态已改变,重新查找
if (n != b.next) // inconsistent read
break;
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果当前节点的前一个节点已被删除,重新查找
if (b.value == null || v == n) // b is deleted
break;
// 比较键的大小
if ((c = cpr(cmp, key, n.key)) > 0) {
// 如果键大于当前节点的键,继续向后查找
b = n;
n = f;
continue;
}
// 如果键等于当前节点的键,返回该节点
if (c == 0)
return n;
// else c < 0; key not found
break;
}
// 未找到节点,返回 null
return null;
}
- 检查节点状态:在查找过程中,需要检查节点的状态,如节点是否已被删除等。如果节点已被删除,需要帮助删除该节点并重新查找。
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
5.5 跳表的删除操作在 ConcurrentSkipListMap 中的实现
在 ConcurrentSkipListMap 中,删除操作的核心逻辑在 doRemove 方法中,其中涉及到跳表的删除操作。具体步骤如下:
- 查找删除位置:从最顶层的头节点开始,通过比较键的大小,找到要删除的节点。
// 从最顶层的头节点开始查找删除位置
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 如果找到的节点不为 null
if (n != null) {
// 另一个节点
Object v; int c;
// 另一个节点的后继节点
Node<K,V> f = n.next;
// 如果当前节点已经被删除,或者后继节点不等于当前节点的后继节点,说明节点状态已改变,重新查找
if (n != b.next) // inconsistent read
break;
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果当前节点的前一个节点已被删除,重新查找
if (b.value == null || v == n) // b is deleted
break;
// 比较键的大小
if ((c = cpr(cmp, key, n.key)) > 0) {
// 如果键大于当前节点的键,继续向后查找
b = n;
n = f;
continue;
}
// 如果键不等于当前节点的键,说明未找到要删除的节点,跳出外层循环
if (c < 0)
break outer;
// 如果值不匹配,跳出外层循环
if (value != null && !value.equals(v))
break outer;
// 尝试将当前节点的值置为 null
if (!n.casValue(v, null))
break;
// 尝试将当前节点从链表中移除
if (!n.appendMarker(f) || !b.casNext(n, f))
// 移除失败,重新查找
findNode(key); // Retry via findNode
else {
// 移除成功,清理索引节点
findPredecessor(key, cmp); // Clean index
// 如果头节点的后继节点为空,尝试降低头节点的层数
if (head.level > 1 && head.next == null)
tryReduceLevel();
}
// 返回被删除节点的值
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
// 未找到要删除的节点,跳出外层循环
break outer;
}
- 标记节点为已删除:找到要删除的节点后,尝试将节点的值置为
null,并标记节点为已删除。
// 尝试将当前节点的值置为 null
if (!n.casValue(v, null))
break;
// 尝试将当前节点从链表中移除
if (!n.appendMarker(f) || !b.casNext(n, f))
// 移除失败,重新查找
findNode(key); // Retry via findNode
- 移除节点并清理索引节点:将节点从链表中移除,并清理跳表中的索引节点。
// 移除成功,清理索引节点
findPredecessor(key, cmp); // Clean index
// 如果头节点的后继节点为空,尝试降低头节点的层数
if (head.level > 1 && head.next == null)
tryReduceLevel();
六、ConcurrentSkipListMap 的线程安全性分析
6.1 无锁算法的应用
ConcurrentSkipListMap 采用了无锁算法来保证线程安全,主要通过 CAS(Compare-And-Swap)操作来实现。CAS 是一种原子操作,它会比较内存中的值与预期值是否相等,如果相等,则将内存中的值更新为新值。ConcurrentSkipListMap 中的很多操作都使用了 CAS 操作,例如插入、删除和查找操作。
6.1.1 CAS 操作在插入操作中的应用
在插入操作中,ConcurrentSkipListMap 使用 CAS 操作来更新节点的指针和值。例如,在插入新节点时,会使用 CAS 操作将新节点插入到合适的位置。
// 创建一个新的节点
z = new Node<K,V>(key, value, n);
// 尝试将新节点插入到当前节点之前
if (!b.casNext(n, z))
break; // restart if lost race to append to b
6.1.2 CAS 操作在删除操作中的应用
在删除操作中,ConcurrentSkipListMap 使用 CAS 操作来标记节点为已删除,并将节点从链表中移除。例如,在删除节点时,会使用 CAS 操作将节点的值置为 null,并将节点的后继节点置为标记节点。
// 尝试将当前节点的值置为 null
if (!n.casValue(v, null))
break;
// 尝试将当前节点从链表中移除
if (!n.appendMarker(f) || !b.casNext(n, f))
// 移除失败,重新查找
findNode(key); // Retry via findNode
6.2 并发控制机制
除了 CAS 操作,ConcurrentSkipListMap 还采用了一些并发控制机制来保证线程安全,例如节点的状态检查和帮助删除机制。
6.2.1 节点状态检查
在进行插入、删除和查找操作时,ConcurrentSkipListMap 会检查节点的状态,如节点是否已被删除等。如果节点已被删除,会帮助删除该节点并重新查找。
// 如果当前节点的值为 null,说明节点已被删除,帮助删除该节点并重新查找
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
6.2.2 帮助删除机制
当一个线程发现某个节点已被删除时,会帮助其他线程删除该节点。例如,在 helpDelete 方法中,会尝试将前一个节点的后继节点置为标记节点的后继节点。
// 帮助删除节点
void helpDelete(Node<K,V> b, Node<K,V> f) {
// 如果当前节点的后继节点等于标记节点,且前一个节点的后继节点等于当前节点
if (f == next && this == b.next) {
// 尝试将前一个节点的后继节点置为标记节点的后继节点
if (f == null || casNext(f, new Node<K,V>(null, null, f))) {
// 尝试将前一个节点的后继节点置为标记节点的后继节点
b.casNext(this, f);
}
}
}
6.3 并发性能分析
ConcurrentSkipListMap 在并发场景下具有较好的性能,主要得益于其无锁算法和跳表数据结构。无锁算法避免了传统锁机制带来的线程阻塞和上下文切换开销,使得多个线程可以同时进行插入、删除和查找操作。跳表数据结构的平均时间复杂度为 O(log n),在处理大量数据时也能保持较好的性能。
然而,在高并发场景下,ConcurrentSkipListMap 仍然可能会出现性能瓶颈,例如多个线程同时对同一个节点进行操作时,可能会导致 CAS 操作失败,需要进行重试。因此,在实际应用中,需要根据具体的场景选择合适的数据结构和并发控制策略。
七、ConcurrentSkipListMap 的序列化机制
7.1 序列化过程
ConcurrentSkipListMap 实现了 Serializable 接口,因此可以进行序列化和反序列化操作。在序列化过程中,会将 ConcurrentSkipListMap 的状态保存到字节流中,包括键值对、比较器和跳表的结构等。
// 序列化方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 写入默认的序列化数据
s.defaultWriteObject();
// 写入键值对的数量
s.writeInt(size());
// 遍历键值对
for (Map.Entry<K,V> e : entrySet()) {
// 写入键
s.writeObject(e.getKey());
// 写入值
s.writeObject(e.getValue());
}
}
在 writeObject 方法中,首先调用 s.defaultWriteObject() 写入默认的序列化数据,然后写入键值对的数量,最后遍历键值对,将键和值依次写入字节流中。
7.2 反序列化过程
在反序列化过程中,会从字节流中读取数据,并重建 ConcurrentSkipListMap 的状态。
// 反序列化方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 读取默认的序列化数据
s.defaultReadObject();
// 初始化跳表
initialize();
// 读取键值对的数量
int n = s.readInt();
// 检查键值对的数量是否合法
if (n < 0)
throw new java.io.InvalidObjectException("Illegal size: " + n);
// 读取键值对
for (int i = 0; i < n; ++i) {
@SuppressWarnings("unchecked") K key = (K) s.readObject();
@SuppressWarnings("unchecked") V value = (V) s.readObject();
// 插入键值对
put(key, value);
}
}
在 readObject 方法中,首先调用 s.defaultReadObject() 读取默认的序列化数据,然后初始化跳表,接着读取键值对的数量,最后依次读取键和值,并将键值对插入到 ConcurrentSkipListMap 中。
7.3 注意事项
- 版本兼容性:在进行序列化和反序列化时,要确保
ConcurrentSkipListMap所在的 Java 版本一致。不同版本的 Java 可能会对ConcurrentSkipListMap的内部实现进行修改,从而导致反序列化失败。 - 可序列化性:
ConcurrentSkipListMap中的键和值必须实现Serializable接口。因为在序列化过程中,会尝试对键和值进行序列化。如果键或值没有实现该接口,会抛出NotSerializableException异常。 - 安全性:反序列化操作可能存在安全风险。如果反序列化的数据来自不可信的源,可能会导致恶意代码执行。因此,在进行反序列化操作时,要确保数据来源的安全性。
八、ConcurrentSkipListMap 的使用场景
8.1 并发有序集合需求
在多线程环境下,如果需要一个有序的集合来存储元素,并且多个线程可能同时对集合进行读写操作,ConcurrentSkipListMap 是一个很好的选择。例如,在一个在线游戏中,需要实时记录玩家的分数排名。多个线程可能同时更新玩家的分数,并且随时需要查询排名信息。使用 ConcurrentSkipListMap 可以保证玩家分数的有序存储,并且多个线程可以安全地进行分数的更新和查询操作。
import java.util.concurrent.ConcurrentSkipListMap;
// 玩家类,实现 Comparable 接口以支持排序
class Player implements Comparable<Player> {
// 玩家姓名
private String name;
// 玩家分数
private int score;
// 构造函数,初始化玩家姓名和分数
public Player(String name, int score) {
// 初始化玩家姓名
this.name = name;
// 初始化玩家分数
this.score = score;
}
// 获取玩家姓名
public String getName() {
return name;
}
// 获取玩家分数
public int getScore() {
return score;
}
// 实现 compareTo 方法,用于比较玩家分数
@Override
public int compareTo(Player other) {
// 按分数降序排序
return Integer.compare(other.score, this.score);
}
// 重写 toString 方法,方便输出玩家信息
@Override
public String toString() {
return "Player{name='" + name + "', score=" + score + "}";
}
}
public class PlayerRankingExample {
public static void main(String[] args) {
// 创建一个 ConcurrentSkipListMap 实例,用于存储玩家信息
ConcurrentSkipListMap<Player, Integer> playerRanking = new ConcurrentSkipListMap<>();
// 模拟多个线程更新玩家分数
Thread thread1 = new Thread(() -> {
// 创建一个新玩家
Player player1 = new Player("Alice", 100);
// 将玩家添加到排名集合中
playerRanking.put(player1, player1.getScore());
System.out.println("Thread 1 added player: " + player1);
});
Thread thread2 = new Thread(() -> {
// 创建一个新玩家
Player player2 = new Player("Bob", 200);
// 将玩家添加到排名集合中
playerRanking.put(player2, player2.getScore());
System.out.println("Thread 2 added player: " + player2);
});
// 启动线程
thread1.start();
thread2.start();
try {
// 等待线程执行完毕
thread1.join();
thread2.join();
} catch (InterruptedException e) {
// 捕获并处理线程中断异常
e.printStackTrace();
}
// 输出玩家排名
System.out.println("Player Ranking:");
for (Player player : playerRanking.keySet()) {
System.out.println(player);
}
}
}
在这个示例中,我们定义了一个 Player 类,实现了 Comparable 接口,按照分数降序排序。然后创建了一个 ConcurrentSkipListMap 来存储玩家信息。多个线程可以同时向集合中添加玩家,并且集合会自动维护玩家的排名顺序。
8.2 范围查询场景
ConcurrentSkipListMap 提供了范围查询的功能,如 subMap、headMap 和 tailMap 方法。在需要根据元素的范围进行查询的场景中非常有用。例如,在一个电商系统中,需要查询价格在某个区间内的商品列表。
import java.util.concurrent.ConcurrentSkipListMap;
// 商品类,实现 Comparable 接口以支持排序
class Product implements Comparable<Product> {
// 商品名称
private String name;
// 商品价格
private double price;
// 构造函数,初始化商品名称和价格
public Product(String name, double price) {
// 初始化商品名称
this.name = name;
// 初始化商品价格
this.price = price;
}
// 获取商品名称
public String getName() {
return name;
}
// 获取商品价格
public double getPrice() {
return price;
}
// 实现 compareTo 方法,用于比较商品价格
@Override
public int compareTo(Product other) {
// 按价格升序排序
return Double.compare(this.price, other.price);
}
// 重写 toString 方法,方便输出商品信息
@Override
public String toString() {
return "Product{name='" + name + "', price=" + price + "}";
}
}
public class ProductRangeQueryExample {
public static void main(String[] args) {
// 创建一个 ConcurrentSkipListMap 实例,用于存储商品信息
ConcurrentSkipListMap<Product, Integer> productMap = new ConcurrentSkipListMap<>();
// 向集合中添加商品
productMap.put(new Product("Product A", 10.0), 1);
productMap.put(new Product("Product B", 20.0), 2);
productMap.put(new Product("Product C", 30.0), 3);
productMap.put(new Product("Product D", 40.0), 4);
// 定义价格范围
Product minPriceProduct = new Product("", 15.0);
Product maxPriceProduct = new Product("", 35.0);
// 查询价格在 15 到 35 之间的商品
ConcurrentSkipListMap<Product, Integer> subMap = (ConcurrentSkipListMap<Product, Integer>) productMap.subMap(minPriceProduct, true, maxPriceProduct, true);
// 输出查询结果
System.out.println("Products in price range [15, 35]:");
for (Product product : subMap.keySet()) {
System.out.println(product);
}
}
}
在这个示例中,我们定义了一个 Product 类,按照价格升序排序。然后创建了一个 ConcurrentSkipListMap 来存储商品信息。使用 subMap 方法可以方便地查询价格在指定范围内的商品列表。
8.3 高性能排行榜系统
由于 ConcurrentSkipListMap 具有良好的并发性能和有序性,非常适合用于构建高性能的排行榜系统。例如,在一个社交媒体平台中,需要实时更新用户的点赞数排行榜。多个线程可能同时更新用户的点赞数,并且用户可以随时查询排行榜信息。
import java.util.concurrent.ConcurrentSkipListMap;
// 用户类,实现 Comparable 接口以支持排序
class User implements Comparable<User> {
// 用户姓名
private String username;
// 用户点赞数
private int likes;
// 构造函数,初始化用户姓名和点赞数
public User(String username, int likes) {
// 初始化用户姓名
this.username = username;
// 初始化用户点赞数
this.likes = likes;
}
// 获取用户姓名
public String getUsername() {
return username;
}
// 获取用户点赞数
public int getLikes() {
return likes;
}
// 增加点赞数
public void addLike() {
this.likes++;
}
// 实现 compareTo 方法,用于比较用户点赞数
@Override
public int compareTo(User other) {
// 按点赞数降序排序
return Integer.compare(other.likes, this.likes);
}
// 重写 toString 方法,方便输出用户信息
@Override
public String toString() {
return "User{username='" + username + "', likes=" + likes + "}";
}
}
public class SocialMediaRankingExample {
public static void main(String[] args) {
// 创建一个 ConcurrentSkipListMap 实例,用于存储用户信息
ConcurrentSkipListMap<User, Integer> userRanking = new ConcurrentSkipListMap<>();
// 初始化一些用户
User user1 = new User("User1", 10);
User user2 = new User("User
九、ConcurrentSkipListMap 与其他并发 Map 的比较
9.1 与 ConcurrentHashMap 的比较
9.1.1 数据结构差异
- ConcurrentHashMap:它基于哈希表实现,采用分段锁(在 Java 8 之前)或 CAS 和 synchronized 结合(Java 8 及以后)的方式保证并发操作的线程安全性。哈希表通过哈希函数将键映射到不同的桶中,查找、插入和删除操作的平均时间复杂度为 O(1)。但它不保证键的有序性。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建 ConcurrentHashMap 实例
ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
// 插入键值对
concurrentHashMap.put(1, "One");
concurrentHashMap.put(2, "Two");
concurrentHashMap.put(3, "Three");
// 遍历键值对,不保证顺序
for (Integer key : concurrentHashMap.keySet()) {
System.out.println(key + ": " + concurrentHashMap.get(key));
}
}
}
- ConcurrentSkipListMap:基于跳表数据结构,跳表是一种有序的数据结构,通过在每个节点中维护多个指向其他节点的指针,实现快速的查找、插入和删除操作,平均时间复杂度为 O(log n)。它能保证键按照自然顺序或指定的比较器排序。
import java.util.concurrent.ConcurrentSkipListMap;
public class ConcurrentSkipListMapExample {
public static void main(String[] args) {
// 创建 ConcurrentSkipListMap 实例
ConcurrentSkipListMap<Integer, String> concurrentSkipListMap = new ConcurrentSkipListMap<>();
// 插入键值对
concurrentSkipListMap.put(3, "Three");
concurrentSkipListMap.put(1, "One");
concurrentSkipListMap.put(2, "Two");
// 遍历键值对,键按升序排列
for (Integer key : concurrentSkipListMap.keySet()) {
System.out.println(key + ": " + concurrentSkipListMap.get(key));
}
}
}
9.1.2 并发性能差异
- ConcurrentHashMap:在高并发场景下,由于其哈希表的结构和高效的并发控制机制(如 Java 8 中的 CAS 和 synchronized 结合),对于随机的插入、删除和查找操作,性能非常高。但在进行范围查询时,由于其无序性,需要额外的操作来实现。
- ConcurrentSkipListMap:在并发插入、删除和查找操作上的性能相对较低,因为跳表的操作涉及到多层链表的遍历和节点的插入、删除。但它在范围查询方面具有明显优势,能够直接利用跳表的有序性进行高效的范围查询。
9.1.3 使用场景差异
- ConcurrentHashMap:适用于不需要键有序,且对随机插入、删除和查找操作性能要求较高的场景,如缓存系统、分布式系统中的数据存储等。
- ConcurrentSkipListMap:适用于需要键有序,并且可能需要进行范围查询的场景,如排行榜系统、时间序列数据存储等。
9.2 与 TreeMap 的比较
9.2.1 线程安全性差异
- TreeMap:是一个非线程安全的有序映射,它基于红黑树实现,保证键按照自然顺序或指定的比较器排序。在多线程环境下,如果多个线程同时对 TreeMap 进行读写操作,可能会导致数据不一致的问题。
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
// 创建 TreeMap 实例
TreeMap<Integer, String> treeMap = new TreeMap<>();
// 插入键值对
treeMap.put(3, "Three");
treeMap.put(1, "One");
treeMap.put(2, "Two");
// 遍历键值对,键按升序排列
for (Integer key : treeMap.keySet()) {
System.out.println(key + ": " + treeMap.get(key));
}
}
}
- ConcurrentSkipListMap:是线程安全的有序映射,多个线程可以同时对其进行插入、删除和查找操作,不需要额外的同步机制。它通过无锁算法(如 CAS 操作)和并发控制机制来保证线程安全。
9.2.2 性能差异
- TreeMap:在单线程环境下,由于红黑树的平衡特性,插入、删除和查找操作的时间复杂度为 O(log n),性能相对稳定。但在多线程环境下,需要额外的同步机制来保证线程安全,这会带来一定的性能开销。
- ConcurrentSkipListMap:在多线程环境下,由于采用了无锁算法,避免了传统锁机制带来的线程阻塞和上下文切换开销,性能相对较高。但在单线程环境下,由于跳表的随机化特性,其性能可能不如 TreeMap。
9.2.3 使用场景差异
- TreeMap:适用于单线程环境下需要键有序的场景,如对数据进行排序和范围查询等。
- ConcurrentSkipListMap:适用于多线程环境下需要键有序,并且需要保证线程安全的场景,如多线程的排行榜系统、并发的范围查询等。
十、ConcurrentSkipListMap 的性能优化建议
10.1 合理选择比较器
比较器的性能会直接影响 ConcurrentSkipListMap 的插入、删除和查找操作的性能。在选择比较器时,应尽量选择简单、高效的比较逻辑。例如,对于整数类型的键,可以直接使用 Integer.compare 方法进行比较。
import java.util.Comparator;
import java.util.concurrent.ConcurrentSkipListMap;
// 自定义比较器,比较整数键
class IntegerComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
// 使用 Integer.compare 方法进行比较
return Integer.compare(o1, o2);
}
}
public class ComparatorExample {
public static void main(String[] args) {
// 创建 ConcurrentSkipListMap 实例,使用自定义比较器
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>(new IntegerComparator());
// 插入键值对
map.put(3, "Three");
map.put(1, "One");
map.put(2, "Two");
// 遍历键值对
for (Integer key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
}
}
10.2 避免频繁的扩容操作
在 ConcurrentSkipListMap 中,当插入节点时,可能会随机增加节点的层数,从而导致跳表的扩容。频繁的扩容操作会影响性能。为了避免频繁的扩容操作,可以根据实际需求预估数据量,在初始化 ConcurrentSkipListMap 时,尽量选择合适的初始容量。
10.3 批量操作的使用
如果需要进行大量的插入、删除或查找操作,可以考虑使用批量操作来提高性能。例如,可以使用 putAll 方法一次性插入多个键值对,而不是逐个插入。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
public class BatchOperationExample {
public static void main(String[] args) {
// 创建 ConcurrentSkipListMap 实例
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
// 创建一个 HashMap 用于存储批量数据
Map<Integer, String> batchData = new HashMap<>();
batchData.put(1, "One");
batchData.put(2, "Two");
batchData.put(3, "Three");
// 使用 putAll 方法一次性插入多个键值对
map.putAll(batchData);
// 遍历键值对
for (Integer key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
}
}
10.4 减少锁竞争
虽然 ConcurrentSkipListMap 采用了无锁算法,但在某些情况下仍然可能存在锁竞争。例如,多个线程同时对同一个节点进行操作时,可能会导致 CAS 操作失败,需要进行重试。为了减少锁竞争,可以尽量避免多个线程同时对同一个节点进行操作,或者采用分区的方式将数据分散到不同的 ConcurrentSkipListMap 实例中。
十一、总结与展望
11.1 总结
ConcurrentSkipListMap 是 Java 并发包中一个非常强大的有序映射数据结构,它基于跳表数据结构实现,具有线程安全、有序性和高效的范围查询等特点。通过无锁算法(如 CAS 操作)和并发控制机制,ConcurrentSkipListMap 能够在多线程环境下保证数据的一致性和操作的高效性。
在插入、删除和查找操作方面,ConcurrentSkipListMap 的平均时间复杂度为 O(log n),在处理大量数据时也能保持较好的性能。同时,它提供了丰富的范围查询方法,如 subMap、headMap 和 tailMap,方便用户根据键的范围进行查询。
与其他并发 Map 相比,ConcurrentSkipListMap 在需要键有序和范围查询的场景中具有明显优势。但在随机插入、删除和查找操作的性能上,相对 ConcurrentHashMap 较低。与 TreeMap 相比,ConcurrentSkipListMap 具有线程安全的特性,更适合多线程环境下的使用。
11.2 展望
11.2.1 性能优化
未来可以进一步研究和优化 ConcurrentSkipListMap 的性能。例如,通过改进跳表的结构和算法,减少节点的层数和节点的插入、删除操作的复杂度,从而提高其在高并发场景下的性能。同时,可以探索更高效的并发控制机制,减少锁竞争和 CAS 操作的失败率。
11.2.2 功能扩展
可以考虑为 ConcurrentSkipListMap 增加更多的功能。例如,支持更复杂的范围查询,如区间的动态更新和合并;提供更丰富的统计功能,如统计某个范围内的元素数量、元素的总和等。
11.2.3 应用场景拓展
随着计算机技术的不断发展,ConcurrentSkipListMap 的应用场景也将不断拓展。例如,在大数据、人工智能等领域,需要处理大量的有序数据,并且可能需要进行并发操作。ConcurrentSkipListMap 可以作为一种高效的数据存储和处理结构,为这些领域的应用提供支持。
总之,ConcurrentSkipListMap 作为一种优秀的并发数据结构,在未来的软件开发中将会发挥越来越重要的作用。通过不断的研究和优化,它将能够更好地满足各种复杂的应用需求。