首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java 多线程与并发编程深度指南:从基础到高级实战

Java 多线程与并发编程深度指南:从基础到高级实战

原创
作者头像
IT大佬 jzit-top
发布2026-07-27 15:54:18
发布2026-07-27 15:54:18
1100
举报

Java 多线程与并发编程深度指南:从基础到高级实战

一、引言:为什么并发如此重要?

在当今多核处理器时代,并发编程已成为提升系统吞吐量和响应性能的核心手段。Java 从诞生之初就内置了强大的多线程支持,经过多个版本的演进(尤其是 Java 5 引入 java.util.concurrent 包、Java 8 的 CompletableFuture、Java 9 的反应式流等),其并发模型日益完善。

然而,并发编程犹如一把双刃剑:正确使用可带来性能飞跃,稍有不慎则容易陷入死锁、数据不一致、性能下降等泥潭。本文将从 Java 内存模型(JMM) 出发,系统讲解线程安全、同步机制、锁优化、并发工具、线程池、异步编程以及常见问题调优,帮助你构建高可靠、高伸缩的并发应用。


二、线程基础:创建与生命周期

2.1 创建线程的三种方式

① 继承 Thread

代码语言:javascript
复制
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
new MyThread().start();

② 实现 Runnable 接口(推荐,因 Java 单继承限制)

代码语言:javascript
复制
Runnable task = () -> System.out.println("Runnable running");
new Thread(task).start();

③ 实现 Callable 接口(可返回结果并抛出异常)

代码语言:javascript
复制
Callable<Integer> callable = () -> {
    Thread.sleep(1000);
    return 123;
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞直到结果返回

2.2 线程生命周期(六种状态)

  • NEW:尚未启动。
  • RUNNABLE:在 JVM 中运行,可能正在执行也可能等待 CPU 时间片。
  • BLOCKED:等待获得监视器锁(进入 synchronized 块)。
  • WAITING:无限期等待另一线程执行特定动作(如 Object.wait() 无超时)。
  • TIMED_WAITING:有超时等待(如 Thread.sleep()wait(timeout))。
  • TERMINATED:已结束。

状态转换如图:NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED


三、线程安全与同步机制

3.1 共享内存与可见性问题

在没有同步的情况下,多个线程对共享变量的修改可能不会立即被其他线程感知,导致脏读。例如:

代码语言:javascript
复制
boolean running = true;
// 线程1
while (running) { }
// 线程2
running = false; // 可能永远不会让线程1停止(可见性问题)

3.2 synchronized 关键字

  • 修饰实例方法:锁当前实例对象。
  • 修饰静态方法:锁当前类的 Class 对象。
  • 修饰代码块:锁指定的对象。

原理:JVM 通过 monitorenter/monitorexit 指令实现,基于进入和退出监视器(Monitor)来保证原子性和可见性。重入锁支持同一线程多次进入。

示例:线程安全的计数器

代码语言:javascript
复制
public class Counter {
    private int count = 0;
    public synchronized void increment() { count++; }
    public synchronized int get() { return count; }
}

3.3 volatile 关键字

  • 保证 可见性:写操作会立即刷新到主存,读操作从主存读取。
  • 禁止 指令重排序(JMM 的 Happens-Before 规则)。
  • 不保证原子性:例如 volatile int i; i++ 非原子操作。

适用场景:布尔状态标志、作为触发器(如 volatile boolean shutdown)。

3.4 原子类与 CAS

java.util.concurrent.atomic 包提供了 AtomicIntegerAtomicLongAtomicReference 等,基于 CAS(Compare-And-Swap) 实现无锁线程安全。

CAS 通过硬件指令保证原子性,三大要素:内存地址 V、期望旧值 A、新值 B,只有当 V 等于 A 时才更新为 B,否则重试(自旋)。

示例:使用 AtomicInteger 替代 synchronized 提升性能

代码语言:javascript
复制
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // 原子+1

ABA 问题:值从 A 变为 B 再变回 A,CAS 误认为未改变。可使用 AtomicStampedReference 添加版本号解决。

3.5 ReentrantLock 与显式锁

相比 synchronizedReentrantLock 提供更灵活的锁机制:

  • 可中断锁(lockInterruptibly())。
  • 超时尝试(tryLock(timeout))。
  • 公平锁(new ReentrantLock(true))。
  • 绑定多个 Condition 实现精细化等待/通知。

示例

代码语言:javascript
复制
class X {
    private final ReentrantLock lock = new ReentrantLock();
    private int value;
    public void set(int v) {
        lock.lock();
        try { value = v; } finally { lock.unlock(); }
    }
}

3.6 读写锁 ReentrantReadWriteLock

允许多个读线程并发,写线程独占,适用于读多写少场景。

代码语言:javascript
复制
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();   // 读锁
rwLock.writeLock().lock();  // 写锁

注意:写锁获取时,所有读锁必须释放,避免写饥饿。


四、Java 内存模型(JMM)核心

JMM 定义了线程与主存之间的抽象关系,保证正确同步的程序在不同硬件平台上有一致的执行结果。

  • 原子性:由 synchronized、锁、java.util.concurrent 包中的原子类保证。
  • 可见性:由 volatilesynchronizedfinal(构造器完成时)保证。
  • 有序性:编译器、处理器可能重排序,但遵循 Happens-Before 规则,包括:
    • 程序次序规则:单线程内,前面的操作先行发生于后面。
    • Volatile 规则:对 volatile 变量的写先行发生于读。
    • 锁规则:解锁先行发生于后续加锁。
    • 传递性。

理解 JMM 有助于排查诡异的并发问题,不要依赖“直觉”猜测重排序。


五、并发工具类(JUC)

5.1 CountDownLatch(倒计时门闩)

允许一个或多个线程等待其他线程完成操作。计数器不可重用。

使用场景:并行处理多个任务,所有任务完成后汇总。

代码语言:javascript
复制
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 工作...
        latch.countDown();
    }).start();
}
latch.await(); // 等待计数器归零

5.2 CyclicBarrier(循环栅栏)

让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达,然后所有线程继续执行。可重复使用(reset())。

使用场景:多线程分段计算,分段合并。

代码语言:javascript
复制
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All reached"));
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 部分计算
        barrier.await();
        // 后续工作
    }).start();
}

5.3 Semaphore(信号量)

控制同时访问特定资源的线程数量(限流)。

代码语言:javascript
复制
Semaphore semaphore = new Semaphore(5); // 最大并发数 5
semaphore.acquire();  // 获取许可
try { /* 访问资源 */ } finally { semaphore.release(); }

5.4 Exchanger(交换器)

两个线程之间交换数据,用于双线程协作(如生产者-消费者)。

代码语言:javascript
复制
Exchanger<String> exchanger = new Exchanger<>();
// 线程A
String data = exchanger.exchange("A's data");
// 线程B
String data = exchanger.exchange("B's data");

六、阻塞队列与线程池

6.1 阻塞队列(BlockingQueue

用于生产者-消费者模型,支持阻塞的 put()take()。常用实现:

  • ArrayBlockingQueue:有界,基于数组。
  • LinkedBlockingQueue:可选有界,基于链表。
  • SynchronousQueue:不存储元素,每个插入必须等待移除。
  • PriorityBlockingQueue:优先级无界。
  • DelayQueue:延迟队列。

6.2 线程池(ThreadPoolExecutor

不要直接使用 Executors 快捷工厂(如 newFixedThreadPoolnewCachedThreadPool),它们可能导致资源耗尽(如无界队列堆积)。推荐手动构造 ThreadPoolExecutor,明确核心参数:

  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:空闲线程存活时间。
  • workQueue:任务队列(BlockingQueue<Runnable>)。
  • threadFactory:线程工厂。
  • handler:饱和策略(AbortPolicy 抛异常、CallerRunsPolicy 由调用者执行、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢弃最早未处理)。

示例

代码语言:javascript
复制
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(); // 优雅关闭

关键原则

  • 根据任务类型(CPU 密集型 vs IO 密集型)调整线程数。
    • CPU 密集型:corePoolSize = N_CPU + 1
    • IO 密集型:corePoolSize = 2 * N_CPU(经验值)。
  • 使用有界队列,防止 OOM。

6.3 ForkJoinPoolRecursiveTask

专为分治任务设计,工作窃取(Work-Stealing)算法提升效率。

代码语言:javascript
复制
class SumTask extends RecursiveTask<Long> {
    // 实现 compute 方法拆分
}
ForkJoinPool pool = new ForkJoinPool();
Long result = pool.invoke(new SumTask(...));

七、并发集合

7.1 ConcurrentHashMap

原理:Java 8+ 采用 Node 数组 + 链表/红黑树,使用 CAS + synchronized 实现线程安全,锁粒度细化到每个桶(Segment 淘汰)。支持并发扩容(多线程协助)。

使用要点

  • putget 无需外部同步。
  • 迭代器弱一致性(不抛 ConcurrentModificationException)。
  • size()mappingCount() 是估算值。

7.2 CopyOnWriteArrayList

写时复制,读操作无锁,适合读多写少。每次修改创建新数组,代价高,不适用于频繁写入。

7.3 其他并发集合

  • ConcurrentLinkedQueue:无界非阻塞队列,基于 CAS。
  • ConcurrentSkipListMap:并发有序映射,基于跳表。
  • BlockingDeque 实现如 LinkedBlockingDeque

八、异步编程:CompletableFuture

Java 8 引入的 CompletableFuture 是对 Future 的增强,支持链式回调、组合、异常处理,实现函数式异步编程。

基本用法

代码语言:javascript
复制
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。


九、常见并发陷阱与调优

9.1 死锁(Deadlock)

两个或多个线程互相持有对方需要的资源,导致永久阻塞。

预防措施

  • 固定锁顺序(按相同顺序获取锁)。
  • 使用 tryLock 超时机制。
  • 使用开放调用(避免在持有锁时调用外部方法)。

检测:使用 jstack 或 JConsole 分析线程堆栈。

9.2 活锁(Livelock)

线程不断重试但始终失败(例如两个线程相互让步)。解决方案:引入随机退避或优先级。

9.3 饥饿与优先级反转

高优先级线程等待低优先级线程释放资源,解决方案:避免依赖优先级,或使用公平锁。

9.4 线程泄漏

未正确关闭线程池或线程未终止,导致资源耗尽。务必在 finally 中调用 shutdown()shutdownNow()

9.5 伪共享(False Sharing)

多个线程修改同一缓存行(Cache Line)中的不同变量,导致缓存失效,性能下降。通过 @Contended(Java 8+)或填充字节避免。


十、实战场景

10.1 高并发缓存(ConcurrentHashMap + Future

防止缓存击穿:使用 putIfAbsentcomputeIfAbsent 原子性创建,并配合 Future 避免重复计算。

代码语言:javascript
复制
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();
}

10.2 定时任务调度

ScheduledExecutorService 替代 Timer(支持多线程,更可靠)。

代码语言:javascript
复制
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(() -> System.out.println("ping"), 0, 1, TimeUnit.SECONDS);

10.3 并行流(parallelStream

简化集合并行处理,但需注意线程安全,避免共享可变状态。默认使用 ForkJoinPool.commonPool(),可自定义。

代码语言:javascript
复制
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()
  • 分析锁竞争:借助 jvisualvmasync-profiler,观察热点 synchronized 和锁延迟。

十二、总结与展望

Java 并发编程是一项系统工程,需要深入理解 内存模型锁机制JUC 工具 以及 硬件特性。在实践中,我们应优先使用高层并发框架(ExecutorServiceCompletableFutureConcurrentHashMap),而非底层 synchronizedwait/notify。同时,时刻警惕死锁、性能损耗和可见性问题。

未来,Java 并发将持续演进(如虚拟线程 Project Loom 已正式进入 JDK 21+),提供更轻量级的并发模型,简化编码难度。但底层原理依然适用,掌握并发核心思想,才能在变化的 API 中游刃有余。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Java 多线程与并发编程深度指南:从基础到高级实战
    • 一、引言:为什么并发如此重要?
    • 二、线程基础:创建与生命周期
      • 2.1 创建线程的三种方式
      • 2.2 线程生命周期(六种状态)
    • 三、线程安全与同步机制
      • 3.1 共享内存与可见性问题
      • 3.2 synchronized 关键字
      • 3.3 volatile 关键字
      • 3.4 原子类与 CAS
      • 3.5 ReentrantLock 与显式锁
      • 3.6 读写锁 ReentrantReadWriteLock
    • 四、Java 内存模型(JMM)核心
    • 五、并发工具类(JUC)
      • 5.1 CountDownLatch(倒计时门闩)
      • 5.2 CyclicBarrier(循环栅栏)
      • 5.3 Semaphore(信号量)
      • 5.4 Exchanger(交换器)
    • 六、阻塞队列与线程池
      • 6.1 阻塞队列(BlockingQueue)
      • 6.2 线程池(ThreadPoolExecutor)
      • 6.3 ForkJoinPool 与 RecursiveTask
    • 七、并发集合
      • 7.1 ConcurrentHashMap
      • 7.2 CopyOnWriteArrayList
      • 7.3 其他并发集合
    • 八、异步编程:CompletableFuture
    • 九、常见并发陷阱与调优
      • 9.1 死锁(Deadlock)
      • 9.2 活锁(Livelock)
      • 9.3 饥饿与优先级反转
      • 9.4 线程泄漏
      • 9.5 伪共享(False Sharing)
    • 十、实战场景
      • 10.1 高并发缓存(ConcurrentHashMap + Future)
      • 10.2 定时任务调度
      • 10.3 并行流(parallelStream)
    • 十一、性能测试与监控
    • 十二、总结与展望
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档