Java 集合类里,大部分日常用的(ArrayList、HashMap、HashSet)都是无锁的,多线程直接共享它们等于自找麻烦。真正"上了锁"的线程安全集合,其实是一小撮。
本文按锁的类型把它们分门别类,一个一个过原理、看代码。
一、synchronized 派:老牌选手,整表一把锁
1. Vector —— 数组列表的"初代安全版"
概念:Vector 就是 ArrayList 的同步祖先。它的实现方式和 ArrayList 几乎一样,区别只有一个:所有方法都用 synchronized 修饰。
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 扩容方法:整个方法加锁
protected synchronized void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 默认扩容为原来的 2 倍(ArrayList 是 1.5 倍)
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
public synchronized boolean add(E e) {
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
// 检查越界后直接取,加锁保证读到完整数据
if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
}
原理讲解:get、add 这种最简单的操作也加锁,意味着读读之间也要互斥——两个线程只是各自读元素,却互相阻塞。这就是"整表一把锁"的代价。
2. Hashtable —— 古老的哈希表
概念:Hashtable 之于 HashMap,正如 Vector 之于 ArrayList。它的所有方法也都是 synchronized,且不允许 null 键值。
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
public synchronized V put(K key, V value) {
// 不允许 null,这是和 HashMap 的一个差异点
if (value == null) throw new NullPointerException();
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % table.length;
// 遍历拉链上的节点
for (Entry<?,?> e = table[index]; e != null ; e = e.next) {
if (e.hash == hash && e.key.equals(key)) {
V old = (V)e.value;
e.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
public synchronized V get(Object key) {
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % table.length;
for (Entry<?,?> e = table[index]; e != null ; e = e.next) {
if (e.hash == hash && e.key.equals(key)) return (V)e.value;
}
return null;
}
}
原理讲解:put 和 get 加同一把对象锁(即 Hashtable 实例本身),所以任何一个线程的写,都会阻塞所有线程的读写。并发度再高,也只有一个线程能碰到这张表。
3. Stack —— 随 Vector 一起陪葬的栈
概念:Stack 继承自 Vector,所有方法都是 synchronized。栈语义只有 push、pop、peek 几个操作,完全可以用 ArrayDeque(无锁)替代。
public class Stack<E> extends Vector<E> {
public synchronized E push(E item) {
addElement(item); // 底层就是 Vector 的加锁 addElement
return item;
}
public synchronized E pop() {
E obj = peek(); // peek 内部也加锁,可重入锁允许这么写
int len = size();
removeElementAt(len – 1); // Vector 的加锁删除
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0) throw new EmptyStackException();
return elementAt(len – 1);
}
}
原理讲解:synchronized 是可重入锁——pop 里调用 peek,锁已经被本线程持有,可以直接进来,不会自己锁死自己。不过 Stack 现在基本只出现在面试题和教科书里了。
4. Collections.synchronizedXxx —— 换皮的同步包装
概念:Collections 提供一组静态工厂,把任何普通集合包装成线程安全的。原理和 Vector 类似:每个方法外包一层 synchronized。
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 实际返回的是内部类 SynchronizedList
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
final List<E> list;
public void add(int index, E element) {
synchronized (mutex) { // 对 mutex 加锁,而不是对 list 本身
list.add(index, element);
}
}
public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}
}
原理讲解:mutex 默认就是包装类自身。但这层包装有个著名陷阱——复合操作要自己加锁:
synchronized (list) { // 必须手动同步,否则下面的复合操作是两次调用
if (!list.contains(x)) { // 第一次调用
list.add(x); // 第二次调用
}
}
contains 和 add 各自加锁、各自返回,之间可能插进别的线程。这点即使不用 synchronizedXxx,在面试中也经常被追问。
二、ReentrantLock 派:精细化锁控
5. CopyOnWriteArrayList —— 写时复制,读永远不锁
概念:写操作加锁(ReentrantLock),复制出新数组再修改;读操作完全无锁,读的是修改前的旧快照。适合读极多、写极少的场景。
public class CopyOnWriteArrayList<E> implements List<E> {
private final transient ReentrantLock lock = new ReentrantLock();
private volatile Object[] array; // 用 volatile 保证替换后读线程立即可见
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 只有写才加锁
try {
Object[] elements = array;
Object[] newElements = Arrays.copyOf(elements, elements.length + 1); // 复制新数组
newElements[elements.length] = e;
array = newElements; // 一次性替换引用
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
return elementAt(array, index); // 无锁读,返回此刻的快照内容
}
}
原理讲解:写一次就复制整个数组,数据量大了代价很高;但读线程拿到的数组引用是永远不变的,所以 get 无需任何同步。volatile 保证数组引用替换后,读线程下一次 get 能看到新引用。这正是"读多写少"设计哲学的极致体现。
6. ArrayBlockingQueue —— 有界队列:一把锁,两个哨兵
概念:一个有固定容量的队列,一把 ReentrantLock 管读写,配两个 Condition:notEmpty(空时让消费者等待)、notFull(满时让生产者等待)。
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E> {
private final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length) return false; // 队列满,直接失败
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
items[putIndex] = x;
putIndex++;
count++;
notEmpty.signal(); // 唤醒一个等待取数据的消费者
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 可响应中断地加锁
try {
while (count == 0) notEmpty.await(); // 队列空则等待
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
E x = items[takeIndex];
items[takeIndex] = null;
takeIndex++;
count—;
notFull.signal(); // 腾出一个位置,唤醒一个生产者
return x;
}
}
原理讲解:核心是 Condition 的等待/唤醒机制。await() 会释放锁进入等待,signal() 唤醒一个等待线程后再竞争锁。这就是为什么队列可以同时实现"满时阻塞、空时阻塞",而不用调用者自己写 wait/notify。
7. LinkedBlockingQueue —— 无界队列:读写分锁
概念:和 ArrayBlockingQueue 类似,但内部是链表。它把读锁和写锁分开了:putLock 只管入队,takeLock 只管出队,两个条件变量 notFull 和 notEmpty 各归各管。
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E> {
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
private final AtomicInteger count = new AtomicInteger(); // 计数用原子类,跨两把锁
public void put(E e) throws InterruptedException {
int c = –1;
Node<E> node = new Node<>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) notFull.await(); // 满则等待
enqueue(node);
c = count.getAndIncrement(); // 先自增前计数加1
if (c + 1 < capacity) notFull.signal(); // 还有空位,唤醒下一个生产者
} finally {
putLock.unlock();
}
if (c == 0) signalNotEmpty(); // 队列从空变非空,唤醒等待的消费者
}
public E take() throws InterruptedException {
E x;
int c = –1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) notEmpty.await(); // 空则等待
x = dequeue();
c = count.getAndDecrement(); // 先自增后计数减1
if (c > 1) notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity) signalNotFull(); // 队列从满变不满,唤醒等待的生产者
return x;
}
}
原理讲解:读读、写写、读写的锁竞争都大幅减小——入队只和入队者抢锁,出队只和出队者抢锁。代价是 count 得用 AtomicInteger 来跨两把锁计数,因为两把锁没法安全地共享一个普通 int。
8. PriorityBlockingQueue —— 优先级队列:一把锁加无锁扩容
概念:一个基于数组堆结构的优先级阻塞队列,只有一把 ReentrantLock,内部还做了一个"扩容时先释放锁、扩完再锁"的优化。
public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E> {
private final ReentrantLock lock;
private final Condition notEmpty;
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length)) {
// 队列已满:扩容需要较长时间,先释放锁再扩
tryGrow(array, cap);
}
try {
siftUp(n, e, array); // 堆上浮操作
size = n + 1;
notEmpty.signal(); // 唤醒等待的消费者
} finally {
lock.unlock();
}
return true;
}
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 释放锁再扩容,让其他线程继续干活
try {
int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : (oldCap >> 1));
queue = Arrays.copyOf(array, newCap);
} finally {
lock.lock(); // 扩完重新加锁
}
}
}
原理讲解:扩容是个纯分配内存的操作,不影响数据本身,所以暂时放掉锁让别的线程继续存取,扩完再回来。这是"把长耗时操作排除在临界区之外"的经典示范。注意 PriorityBlockingQueue 是无界的,所以只有 notEmpty 一个 Condition。
三、synchronized + CAS 派:并发 HashMap 的混搭
9. ConcurrentHashMap —— 锁桶头,读无锁
概念:JDK 1.8 的 ConcurrentHashMap 放弃了 1.7 的 Segment 分段锁,改为:桶为空时用 CAS 抢槽位,桶非空时 synchronized 锁住桶的头节点,读操作全程无锁。
final V putVal(K key, V value, boolean onlyIfAbsent) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) {
tab = initTable(); // 延迟初始化:第一次 put 时才分配数组
} else if ((f = tabAt(tab, i = (n – 1) & hash)) == null) {
// 槽位是空的:CAS 直接放,成功就完事,不加锁
if (casTabAt(tab, i, null, new Node<>(hash, key, value, null)))
break;
} else if ((fh = f.hash) == MOVED) {
tab = helpTransfer(tab, f); // 遇到正在扩容的桶,先帮它迁完
} else {
V oldVal = null;
synchronized (f) { // 槽位非空:只锁这个桶的头节点
if (tabAt(tab, i) == f) { // 再确认一次头节点没变(防止拿到过时数据)
if (fh >= 0) { // 普通链表节点
for (Node<K,V> e = f;;) {
if (e.hash == hash && e.key.equals(key)) {
oldVal = e.val;
e.val = value;
break;
}
if (e.next == null) { e.next = new Node<>(...); break; }
e = e.next;
}
} else if (f instanceof TreeBin) {
oldVal = ((TreeBin<K,V>)f).putTreeVal(...); // 红黑树分支
}
}
}
if (oldVal != null) return oldVal;
addCount(1L, binCount); // 计数,可能触发扩容
break;
}
}
return null;
}
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
// 用 Unsafe.getObjectVolatile 保证读取到最新值
return (Node<K,V>) U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
原理讲解:
- 写分散到一个个桶头锁上,只有操作同一个桶的线程才互斥,并行度 ≈ 桶数;
- 读完全不拿锁,靠 tabAt 的 volatile 语义读,get 基本等同 HashMap;
- 扩容由多个线程协作迁移(helpTransfer),每个线程每次负责几个桶,速度比 HashMap 1.7 的单线程扩容快得多;
- size() 用 baseCount + CounterCell[](类似 LongAdder)分散计数,多线程计数也几乎无竞争。
四、无锁但值得一提:CAS 的极端派
10. ConcurrentSkipListMap —— 跳表并发,无锁有序
概念:一个并发有序 Map,用 CAS 实现无锁并发,完全没有锁。数据结构是跳表,有序性和 TreeMap 相同,但并发性远好于 synchronized 包装的 TreeMap。
public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V>
implements ConcurrentNavigableMap<K,V>, Cloneable, java.io.Serializable {
private boolean doPut(K key, V value, boolean onlyIfAbsent) {
Node<K,V> z; // 新增节点
for (Index<K,V> q = head, r = q.right, t = z;;) {
if (r == null || r.node.key.compareTo(key) > 0) {
// 需要跳到下一层,或直接落到底层插入
if (q.down != null) { q = q.down; r = q.right; }
else {
// 找到插入位置,CAS 插入
if (t.key.compareTo(key) > 0) {
if (t.casNext(key, value)) { /* 插入成功 */ }
return false; // 或重来
}
}
}
}
}
}
原理讲解:跳表通过多层索引实现 O(log n) 查找。所有修改靠 CAS 乐观并发——遇到竞争就重试,而不是挂起等待。在高并发写场景下它的无冲突 CAS 操作几乎不占 CPU 锁时间。
五、总结速查
| Vector | synchronized 方法 | 加锁 | 加锁 |
| Hashtable | synchronized 方法 | 加锁 | 加锁 |
| Stack | synchronized 方法(继承Vector) | 加锁 | 加锁 |
| Collections.synchronizedXxx | synchronized 包装 | 加锁 | 加锁 |
| CopyOnWriteArrayList | ReentrantLock(写时复制) | 无锁 | 加锁 |
| ArrayBlockingQueue | 一把 ReentrantLock + 两个 Condition | 加锁 | 加锁 |
| LinkedBlockingQueue | 读写分锁(2 把 ReentrantLock) | 加锁 | 加锁 |
| PriorityBlockingQueue | 一把 ReentrantLock(扩容时短暂释放) | 加锁 | 加锁 |
| ConcurrentHashMap | synchronized(桶头)+ CAS | 无锁 | 锁单个桶 |
| ConcurrentSkipListMap | 无锁(CAS + 跳表) | 无锁 | 无锁 |
选型建议:
- 需要 Map 并发:无脑 ConcurrentHashMap,别碰 Hashtable
- 需要 List 并发、读多写少:CopyOnWriteArrayList;否则 Collections.synchronizedList
- 生产者-消费者:LinkedBlockingQueue(吞吐高)或 ArrayBlockingQueue(防 OOM)
- 需要排序且并发:ConcurrentSkipListMap
网硕互联帮助中心





评论前必须登录!
注册