面试题答案
一键面试高并发环境下LinkedHashSet维护元素插入顺序机制面临的问题
- 线程安全问题:LinkedHashSet本身不是线程安全的。在高并发场景下,多个线程同时进行插入、删除等操作时,可能会导致数据不一致。例如,当一个线程正在遍历LinkedHashSet,另一个线程同时进行插入操作,可能会破坏内部维护顺序的链表结构,导致遍历结果异常。
- 性能问题:LinkedHashSet内部通过链表维护元素插入顺序,在高并发插入时,由于链表节点的创建、连接和调整操作,可能会导致频繁的内存分配和竞争,从而降低性能。
优化方法及源码分析
- 使用线程安全集合类:
- 使用ConcurrentHashMap + 自定义链表:可以借鉴LinkedHashSet的实现原理,使用ConcurrentHashMap存储元素,同时自己维护一个线程安全的双向链表来记录插入顺序。例如:
import java.util.concurrent.ConcurrentHashMap;
public class ThreadSafeLinkedHashSet<E> {
private final ConcurrentHashMap<E, Node<E>> map;
private final Node<E> head;
private final Node<E> tail;
private static class Node<E> {
E value;
Node<E> prev;
Node<E> next;
Node(E value) {
this.value = value;
}
}
public ThreadSafeLinkedHashSet() {
map = new ConcurrentHashMap<>();
head = new Node<>(null);
tail = new Node<>(null);
head.next = tail;
tail.prev = head;
}
public boolean add(E e) {
if (map.containsKey(e)) {
return false;
}
Node<E> newNode = new Node<>(e);
map.put(e, newNode);
Node<E> last = tail.prev;
last.next = newNode;
newNode.prev = last;
newNode.next = tail;
tail.prev = newNode;
return true;
}
// 其他方法类似实现
}
- **使用Collections.synchronizedSet包装**:可以使用`Collections.synchronizedSet(new LinkedHashSet<>())`来创建一个线程安全的LinkedHashSet。这种方式是在LinkedHashSet的基础上,通过同步方法对所有操作进行加锁。例如,`synchronizedSet`的`add`方法源码如下:
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
这里mutex
是一个锁对象,c
是被包装的LinkedHashSet。虽然这种方式简单,但由于锁粒度较大,在高并发下性能较低。
2. 优化链表操作:
- 减少链表调整频率:在高并发插入时,可以批量处理插入操作,减少链表节点的频繁创建和调整。例如,在内部维护一个临时队列,当队列满时,一次性将队列中的元素插入到LinkedHashSet中,并调整链表。这样可以减少链表操作的竞争。
- 使用更高效的链表结构:例如,尝试使用跳表(Skip List)代替普通双向链表。跳表在查询、插入和删除操作上具有接近平衡二叉树的性能,并且可以在一定程度上减少链表维护的开销。但实现相对复杂,需要对数据结构有深入理解。