在当今多核处理器时代,并发编程已成为提升系统吞吐量和响应性能的核心手段。Java 从诞生之初就内置了强大的多线程支持,经过多个版本的演进(尤其是 Java 5 引入 java.util.concurrent 包、Java 8 的 CompletableFuture、Java 9 的反应式流等),其并发模型日益完善。
然而,并发编程犹如一把双刃剑:正确使用可带来性能飞跃,稍有不慎则容易陷入死锁、数据不一致、性能下降等泥潭。本文将从 Java 内存模型(JMM) 出发,系统讲解线程安全、同步机制、锁优化、并发工具、线程池、异步编程以及常见问题调优,帮助你构建高可靠、高伸缩的并发应用。
① 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
new MyThread().start();② 实现 Runnable 接口(推荐,因 Java 单继承限制)
Runnable task = () -> System.out.println("Runnable running");
new Thread(task).start();③ 实现 Callable 接口(可返回结果并抛出异常)
Callable<Integer> callable = () -> {
Thread.sleep(1000);
return 123;
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞直到结果返回synchronized 块)。Object.wait() 无超时)。Thread.sleep(),wait(timeout))。状态转换如图:NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED。
在没有同步的情况下,多个线程对共享变量的修改可能不会立即被其他线程感知,导致脏读。例如:
boolean running = true;
// 线程1
while (running) { }
// 线程2
running = false; // 可能永远不会让线程1停止(可见性问题)synchronized 关键字原理:JVM 通过 monitorenter/monitorexit 指令实现,基于进入和退出监视器(Monitor)来保证原子性和可见性。重入锁支持同一线程多次进入。
示例:线程安全的计数器
public class Counter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
}volatile 关键字volatile int i; i++ 非原子操作。适用场景:布尔状态标志、作为触发器(如 volatile boolean shutdown)。
java.util.concurrent.atomic 包提供了 AtomicInteger、AtomicLong、AtomicReference 等,基于 CAS(Compare-And-Swap) 实现无锁线程安全。
CAS 通过硬件指令保证原子性,三大要素:内存地址 V、期望旧值 A、新值 B,只有当 V 等于 A 时才更新为 B,否则重试(自旋)。
示例:使用 AtomicInteger 替代 synchronized 提升性能
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // 原子+1ABA 问题:值从 A 变为 B 再变回 A,CAS 误认为未改变。可使用 AtomicStampedReference 添加版本号解决。
ReentrantLock 与显式锁相比 synchronized,ReentrantLock 提供更灵活的锁机制:
lockInterruptibly())。tryLock(timeout))。new ReentrantLock(true))。Condition 实现精细化等待/通知。示例:
class X {
private final ReentrantLock lock = new ReentrantLock();
private int value;
public void set(int v) {
lock.lock();
try { value = v; } finally { lock.unlock(); }
}
}ReentrantReadWriteLock允许多个读线程并发,写线程独占,适用于读多写少场景。
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // 读锁
rwLock.writeLock().lock(); // 写锁注意:写锁获取时,所有读锁必须释放,避免写饥饿。
JMM 定义了线程与主存之间的抽象关系,保证正确同步的程序在不同硬件平台上有一致的执行结果。
synchronized、锁、java.util.concurrent 包中的原子类保证。volatile、synchronized、final(构造器完成时)保证。理解 JMM 有助于排查诡异的并发问题,不要依赖“直觉”猜测重排序。
CountDownLatch(倒计时门闩)允许一个或多个线程等待其他线程完成操作。计数器不可重用。
使用场景:并行处理多个任务,所有任务完成后汇总。
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 工作...
latch.countDown();
}).start();
}
latch.await(); // 等待计数器归零CyclicBarrier(循环栅栏)让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达,然后所有线程继续执行。可重复使用(reset())。
使用场景:多线程分段计算,分段合并。
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All reached"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 部分计算
barrier.await();
// 后续工作
}).start();
}Semaphore(信号量)控制同时访问特定资源的线程数量(限流)。
Semaphore semaphore = new Semaphore(5); // 最大并发数 5
semaphore.acquire(); // 获取许可
try { /* 访问资源 */ } finally { semaphore.release(); }Exchanger(交换器)两个线程之间交换数据,用于双线程协作(如生产者-消费者)。
Exchanger<String> exchanger = new Exchanger<>();
// 线程A
String data = exchanger.exchange("A's data");
// 线程B
String data = exchanger.exchange("B's data");BlockingQueue)用于生产者-消费者模型,支持阻塞的 put() 和 take()。常用实现:
ArrayBlockingQueue:有界,基于数组。LinkedBlockingQueue:可选有界,基于链表。SynchronousQueue:不存储元素,每个插入必须等待移除。PriorityBlockingQueue:优先级无界。DelayQueue:延迟队列。ThreadPoolExecutor)不要直接使用 Executors 快捷工厂(如 newFixedThreadPool、newCachedThreadPool),它们可能导致资源耗尽(如无界队列堆积)。推荐手动构造 ThreadPoolExecutor,明确核心参数:
corePoolSize:核心线程数。maximumPoolSize:最大线程数。keepAliveTime:空闲线程存活时间。workQueue:任务队列(BlockingQueue<Runnable>)。threadFactory:线程工厂。handler:饱和策略(AbortPolicy 抛异常、CallerRunsPolicy 由调用者执行、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢弃最早未处理)。示例:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
pool.execute(() -> System.out.println("task"));
pool.shutdown(); // 优雅关闭关键原则:
corePoolSize = N_CPU + 1。corePoolSize = 2 * N_CPU(经验值)。ForkJoinPool 与 RecursiveTask专为分治任务设计,工作窃取(Work-Stealing)算法提升效率。
class SumTask extends RecursiveTask<Long> {
// 实现 compute 方法拆分
}
ForkJoinPool pool = new ForkJoinPool();
Long result = pool.invoke(new SumTask(...));ConcurrentHashMap原理:Java 8+ 采用 Node 数组 + 链表/红黑树,使用 CAS + synchronized 实现线程安全,锁粒度细化到每个桶(Segment 淘汰)。支持并发扩容(多线程协助)。
使用要点:
put、get 无需外部同步。ConcurrentModificationException)。size() 和 mappingCount() 是估算值。CopyOnWriteArrayList写时复制,读操作无锁,适合读多写少。每次修改创建新数组,代价高,不适用于频繁写入。
ConcurrentLinkedQueue:无界非阻塞队列,基于 CAS。ConcurrentSkipListMap:并发有序映射,基于跳表。BlockingDeque 实现如 LinkedBlockingDeque。CompletableFutureJava 8 引入的 CompletableFuture 是对 Future 的增强,支持链式回调、组合、异常处理,实现函数式异步编程。
基本用法:
CompletableFuture.supplyAsync(() -> {
// 耗时计算
return 42;
}).thenApply(result -> result * 2)
.thenAccept(System.out::println) // 打印84
.exceptionally(e -> { e.printStackTrace(); return null; });常用方法:
thenApply:同步转换。thenCompose:扁平化异步链。thenCombine:合并两个 Future。allOf / anyOf:等待所有 / 任意完成。自定义线程池:supplyAsync(..., executor) 可指定专用池,避免阻塞公共 ForkJoinPool。
两个或多个线程互相持有对方需要的资源,导致永久阻塞。
预防措施:
tryLock 超时机制。检测:使用 jstack 或 JConsole 分析线程堆栈。
线程不断重试但始终失败(例如两个线程相互让步)。解决方案:引入随机退避或优先级。
高优先级线程等待低优先级线程释放资源,解决方案:避免依赖优先级,或使用公平锁。
未正确关闭线程池或线程未终止,导致资源耗尽。务必在 finally 中调用 shutdown() 或 shutdownNow()。
多个线程修改同一缓存行(Cache Line)中的不同变量,导致缓存失效,性能下降。通过 @Contended(Java 8+)或填充字节避免。
ConcurrentHashMap + Future)防止缓存击穿:使用 putIfAbsent 或 computeIfAbsent 原子性创建,并配合 Future 避免重复计算。
ConcurrentHashMap<String, Future<Data>> cache = new ConcurrentHashMap<>();
public Data getData(String key) {
Future<Data> future = cache.get(key);
if (future == null) {
FutureTask<Data> ft = new FutureTask<>(() -> loadData(key));
Future<Data> existing = cache.putIfAbsent(key, ft);
if (existing == null) { ft.run(); return ft.get(); }
else return existing.get();
}
return future.get();
}ScheduledExecutorService 替代 Timer(支持多线程,更可靠)。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(() -> System.out.println("ping"), 0, 1, TimeUnit.SECONDS);parallelStream)简化集合并行处理,但需注意线程安全,避免共享可变状态。默认使用 ForkJoinPool.commonPool(),可自定义。
List<Integer> list = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
long sum = list.parallelStream().mapToLong(i -> i).sum(); // 线程安全JMH(Java Microbenchmark Harness)进行精确基准测试,避免 JVM 优化干扰。getPoolSize()、getActiveCount()、getQueue().size()。jvisualvm 或 async-profiler,观察热点 synchronized 和锁延迟。Java 并发编程是一项系统工程,需要深入理解 内存模型、锁机制、JUC 工具 以及 硬件特性。在实践中,我们应优先使用高层并发框架(ExecutorService、CompletableFuture、ConcurrentHashMap),而非底层 synchronized 和 wait/notify。同时,时刻警惕死锁、性能损耗和可见性问题。
未来,Java 并发将持续演进(如虚拟线程 Project Loom 已正式进入 JDK 21+),提供更轻量级的并发模型,简化编码难度。但底层原理依然适用,掌握并发核心思想,才能在变化的 API 中游刃有余。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。