Java并发编程实战第五章:构建并发模块的技术要点解析
引言:并发编程的基础构建块
在Java并发编程的世界中,第五章"Building Blocks"为我们揭示了构建健壮、高效并发应用的核心技术组件。你是否曾经遇到过这样的困境:在多线程环境下使用集合时遭遇ConcurrentModificationException,或者因为不当的同步策略导致性能瓶颈?本章正是为了解决这些痛点而生,系统地介绍了Java并发包中那些经过实战检验的构建模块。
通过阅读本文,你将掌握:
- 同步集合与并发集合的深度对比与选择策略
- 阻塞队列在生产者-消费者模式中的精妙应用
- 四大同步器(Latches、FutureTask、Semaphores、Barriers)的原理与实践
- 构建高效可伸缩结果缓存的最佳实践方案
同步集合:传统方案的局限性
同步包装器的内在缺陷
Java早期的线程安全集合通过Collections.synchronizedXxx()方法实现,这种方案采用Java监视器模式(Java Monitor Pattern),在每个公共方法上添加同步锁。
// 问题代码示例:看似安全实则存在竞态条件
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1; // 可能被其他线程修改
return list.get(lastIndex); // 可能越界
}
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1; // 可能被其他线程修改
list.remove(lastIndex); // 可能越界
}
复合操作的安全隐患
同步集合最大的问题在于不支持原子性的复合操作。即使是简单的"检查后执行"模式也需要客户端显式加锁:
// 正确的客户端加锁方式
public static Object getLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
迭代器的并发修改异常
迭代操作在多线程环境下尤为脆弱,即使使用现代并发集合,修改期间的迭代仍可能抛出ConcurrentModificationException。解决方案包括:
- 完全锁定集合:保证迭代期间的一致性,但牺牲并发性
- 克隆集合:迭代副本,但克隆操作仍需同步
隐藏迭代器的陷阱
某些看似无害的操作实际上隐含迭代行为:
public class HiddenIterator {
private final Set<Integer> set = new HashSet<>();
public void addTenThings() {
for (int i = 0; i < 10; i++)
set.add(i);
System.out.println("DEBUG: added elements to " + set); // 隐含toString()迭代
}
}
并发集合:现代Java的解决方案
ConcurrentHashMap的革命性设计
ConcurrentHashMap采用分段锁技术,大幅提升并发性能:
原子性Map操作
ConcurrentHashMap通过接口方法提供原子复合操作:
public interface ConcurrentMap<K,V> {
V putIfAbsent(K key, V value); // 不存在时插入
boolean remove(K key, V value); // 精确匹配删除
boolean replace(K key, V oldValue, V newValue); // 条件替换
V replace(K key, V newValue); // 无条件替换
}
CopyOnWriteArrayList的写时复制策略
适用于读多写少的场景,迭代期间保证一致性:
List<String> list = new CopyOnWriteArrayList<>();
// 写操作:复制整个数组,修改副本,替换引用
// 读操作:直接访问当前数组引用,无需同步
阻塞队列与生产者-消费者模式
阻塞队列的核心机制
阻塞队列通过put()和take()方法实现线程间协调:
桌面搜索实例分析
public class FileCrawler implements Runnable {
private final BlockingQueue<File> fileQueue;
private void crawl(File root) throws InterruptedException {
File[] entries = root.listFiles();
for (File entry : entries) {
if (entry.isDirectory()) {
crawl(entry);
} else if (!alreadyIndexed(entry)) {
fileQueue.put(entry); // 阻塞直到有空间
}
}
}
}
public class Indexer implements Runnable {
public void run() {
try {
while (true) {
File file = queue.take(); // 阻塞直到有文件
indexFile(file);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
串行线程限制模式
阻塞队列实现了对象所有权的安全转移:
- 生产者:创建对象并放入队列后不再使用
- 队列:作为中间媒介,保证安全发布
- 消费者:从队列取出后获得独占访问权
工作窃取算法
Java 6引入的Deque支持工作窃取模式:
同步器:高级线程协调机制
CountDownLatch:多线程启动协调
public class TestHarness {
public long timeTasks(int nThreads, Runnable task) throws InterruptedException {
CountDownLatch startGate = new CountDownLatch(1); // 开始门
CountDownLatch endGate = new CountDownLatch(nThreads); // 结束门
for (int i = 0; i < nThreads; i++) {
new Thread(() -> {
try {
startGate.await(); // 等待开始信号
task.run();
} finally {
endGate.countDown(); // 通知任务完成
}
}).start();
}
long start = System.nanoTime();
startGate.countDown(); // 释放所有线程
endGate.await(); // 等待所有线程完成
return System.nanoTime() - start;
}
}
FutureTask:异步结果获取
public class Preloader {
private final FutureTask<ProductInfo> future =
new FutureTask<>(() -> loadProductInfo());
public ProductInfo get() throws Exception {
try {
return future.get(); // 阻塞直到结果就绪
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof DataLoadException) {
throw (DataLoadException) cause;
} else {
throw new RuntimeException(cause);
}
}
}
}
Semaphore:资源访问控制
信号量实现有界集合:
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore semaphore;
public boolean add(T element) throws InterruptedException {
semaphore.acquire(); // 获取许可
boolean added = false;
try {
added = set.add(element);
return added;
} finally {
if (!added) {
semaphore.release(); // 添加失败时释放许可
}
}
}
public boolean remove(Object element) {
boolean removed = set.remove(element);
if (removed) {
semaphore.release(); // 移除成功时释放许可
}
return removed;
}
}
CyclicBarrier:多阶段同步
适用于分阶段并行算法:
public class CellularAutomata {
private final CyclicBarrier barrier;
private final Worker[] workers;
public CellularAutomata() {
int count = Runtime.getRuntime().availableProcessors();
barrier = new CyclicBarrier(count, () -> {
// 所有worker完成后的回调
mainBoard.commitNewValues();
});
workers = new Worker[count];
for (int i = 0; i < count; i++) {
workers[i] = new Worker(mainBoard.getSubBoard(count, i));
}
}
private class Worker implements Runnable {
public void run() {
while (!board.hasConverged()) {
// 计算自己负责的区域
for (int x = 0; x < board.getMaxX(); x++) {
for (int y = 0; y < board.getMaxY(); y++) {
board.setNewValue(x, y, computeValue(x, y));
}
}
try {
barrier.await(); // 等待其他worker
} catch (Exception e) {
return;
}
}
}
}
}
构建高效可伸缩结果缓存
演进历程:从简单到完善
版本1:同步方法实现
public class Memoizer1<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new HashMap<>();
public synchronized V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}
问题:完全串行化,性能极差
版本2:ConcurrentHashMap实现
public class Memoizer2<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new ConcurrentHashMap<>();
public V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg); // 可能重复计算
cache.put(arg, result);
}
return result;
}
}
问题:存在竞态条件,可能重复计算
版本3:FutureTask包装
public class Memoizer3<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
public V compute(A arg) throws InterruptedException {
Future<V> future = cache.get(arg);
if (future == null) {
FutureTask<V> ft = new FutureTask<>(() -> c.compute(arg));
future = ft;
cache.put(arg, ft);
ft.run();
}
return future.get();
}
}
问题:仍存在检查后执行竞态条件
最终版本:原子操作完善
public class Memoizer<A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<>();
public V compute(A arg) throws InterruptedException {
while (true) {
Future<V> future = cache.get(arg);
if (future == null) {
FutureTask<V> ft = new FutureTask<>(() -> c.compute(arg));
future = cache.putIfAbsent(arg, ft); // 原子操作
if (future == null) {
future = ft;
ft.run();
}
}
try {
return future.get();
} catch (CancellationException e) {
cache.remove(arg, future); // 移除已取消的任务
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}
}
性能对比与选择指南
并发集合选择矩阵
| 场景特征 | 推荐方案 | 优点 | 缺点 |
|---|---|---|---|
| 读多写少 | CopyOnWriteArrayList | 迭代安全,无锁读 | 写操作成本高 |
| 高并发读写 | ConcurrentHashMap | 分段锁,高吞吐 | 弱一致性迭代 |
| 顺序处理 | BlockingQueue | 生产者-消费者解耦 | 需要边界管理 |
| 资源池 | Semaphore | 精确控制并发数 | 需要手动释放 |
同步器适用场景总结
| 同步器类型 | 核心用途 | 典型场景 |
|---|---|---|
| CountDownLatch | 一次性事件等待 | 服务启动协调,批量任务完成等待 |
| FutureTask | 异步结果获取 | 预加载,并行计算 |
| Semaphore | 资源访问控制 | 连接池,限流器 |
| CyclicBarrier | 多阶段同步 | 并行算法,多阶段计算 |
最佳实践与陷阱避免
中断处理规范
public class TaskRunnable implements Runnable {
public void run() {
try {
processTask(queue.take());
} catch (InterruptedException e) {
// 恢复中断状态,不要吞没异常
Thread.currentThread().interrupt();
}
}
}
内存一致性保证
所有并发集合都提供happens-before保证:
- 将对象放入并发集合的操作happens-before后续的访问操作
- 从并发集合获取对象的操作happens-before后续的使用操作
性能监控与调优
// 监控缓存命中率
public class MonitoredMemoizer<A, V> extends Memoizer<A, V> {
private final AtomicLong hits = new AtomicLong();
private final AtomicLong misses = new AtomicLong();
@Override
public V compute(A arg) throws InterruptedException {
Future<V> future = cache.get(arg);
if (future != null) {
hits.incrementAndGet();
} else {
misses.incrementAndGet();
}
return super.compute(arg);
}
public double getHitRatio() {
long total = hits.get() + misses.get();
return total == 0 ? 0 : (double) hits.get() / total;
}
}
结语:构建健壮并发系统的艺术
Java并发编程实战第五章为我们提供了一套完整的并发构建模块工具箱。从基础的同步集合到高级的同步器,每一个组件都有其特定的适用场景和优化策略。关键在于根据具体的业务需求、性能要求和一致性需求做出恰当的选择。
记住并发编程的黄金法则:优先使用现有的线程安全组件,而不是自己实现同步逻辑。Java并发包中的这些构建模块经过了无数项目的检验,能够帮助我们在保证线程安全的同时,获得最佳的性能表现。
通过深入理解这些构建模块的工作原理和适用场景,你将能够设计出既正确又高效的并发系统,从容应对多线程环境下的各种挑战。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



