Java中无锁数组扩容的线程安全实现方案问询
多线程数组扩容的无锁写入优化方案
针对高频写入场景下的数组扩容线程安全问题,以下是几种基于JDK现有工具类的简洁解决方案:
方案1:使用ReentrantReadWriteLock(推荐,最简单高效)
利用读写锁的共享读、独占写特性,既保证扩容时的线程安全,又让高频写入操作几乎无锁开销:
- 定义全局读写锁:
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); - 写入线程逻辑:获取读锁(多个写入线程可同时持有,开销极低)完成写入,无需担心互斥:
rwLock.readLock().lock(); try { // 写入预分配的索引位置 currentArray[assignedIndex] = value; } finally { rwLock.readLock().unlock(); } - 扩容逻辑:获取写锁(会阻塞所有读锁,确保无线程写入旧数组),完成数组复制替换后释放:
rwLock.writeLock().lock(); try { // 双重检查,避免重复扩容 if (currentArray.length < newCapacity) { Object[] newArray = new Object[newCapacity]; System.arraycopy(currentArray, 0, newArray, 0, currentArray.length); currentArray = newArray; } } finally { rwLock.writeLock().unlock(); }
该方案完全依赖JDK原生工具类,代码简洁,读锁的性能损耗几乎可以忽略,完美适配高频写入场景。
方案2:使用Phaser实现动态等待屏障
如果不想使用锁,可以用Phaser替代CyclicBarrier,它支持动态注册线程,无需预先指定数量:
- 定义全局Phaser:
private final Phaser phaser = new Phaser(); - 写入线程逻辑:每次写入前注册到Phaser,完成后注销:
phaser.register(); try { currentArray[assignedIndex] = value; } finally { phaser.arriveAndDeregister(); } - 扩容逻辑:
// 等待所有已注册的写入线程完成当前任务并注销 phaser.arriveAndAwaitAdvance(); try { // 复制并替换数组 Object[] newArray = new Object[newCapacity]; System.arraycopy(currentArray, 0, newArray, 0, currentArray.length); currentArray = newArray; } finally { // 重置Phaser,允许后续写入线程重新注册 phaser.register(); }
注意:如果写入线程是循环执行任务,需要确保每次写入都正确注册/注销Phaser,避免出现屏障永久等待的情况。
方案3:AtomicReference + 原子计数器(无锁极致性能)
追求极致性能时,可以用原子引用持有数组,配合原子计数器跟踪活跃写入线程:
- 定义全局变量:
private final AtomicReference<Object[]> arrayRef = new AtomicReference<>(new Object[initialCapacity]); private final AtomicInteger activeWriters = new AtomicInteger(0); - 写入线程逻辑:
activeWriters.incrementAndGet(); try { Object[] current = arrayRef.get(); // 写入预分配索引 current[assignedIndex] = value; } finally { activeWriters.decrementAndGet(); } - 扩容逻辑:
// 等待所有活跃写入线程完成 while (activeWriters.get() > 0) { Thread.yield(); } // CAS替换数组,避免并发扩容 Object[] oldArray = arrayRef.get(); if (oldArray.length < newCapacity) { Object[] newArray = new Object[newCapacity]; System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); arrayRef.compareAndSet(oldArray, newArray); }
该方案完全无锁,但需要注意:如果扩容过程中有新线程开始写入,可能会写入旧数组,因此需要在写入前再次检查数组引用是否变化(可配合版本号优化),复杂度稍高。
排除方案说明
CopyOnWriteArrayList不适用的原因正如你所说:它每次写入都会复制数组,而你仅需在扩容时复制,会带来不必要的性能开销,不符合需求。
内容的提问来源于stack exchange,提问作者barneypitt
相关产品推荐
相关产品推荐

