菜单

Java 并发

相关源文件

Java 并发是指程序同时执行多个代码块的能力。本文档涵盖了在 Java 中编写并发代码的基础概念、机制和最佳实践。它解释了线程、同步原语、线程池、异步编程模型以及 Java 应用程序中常用的并发模式。

有关 Java 集合的信息,请参阅 Java 集合。有关 JVM 架构的信息,请参阅 JVM 架构

线程基础

线程是 Java 中并发执行的基本单元。Java 中的线程代表程序中独立的执行路径。Java 提供了多种与线程交互的方式,包括直接创建线程和使用更高级别的并发工具。

Thread 类是 Java 核心 API 的一部分,代表单个执行线程。每个线程都在单独的调用堆栈中运行,并拥有自己的程序计数器。

线程状态和生命周期

Java 中的线程可以处于以下任一状态:

  1. NEW:线程已被创建但尚未启动
  2. RUNNABLE:线程正在执行或已准备好执行
  3. BLOCKED:线程正在等待监视器锁
  4. WAITING:线程正在无限期地等待另一个线程执行特定操作
  5. TIMED_WAITING:线程正在等待另一个线程,等待时间是指定的
  6. TERMINATED:线程已完成执行

下图说明了 Java 线程的状态转换。

来源:docs/java/concurrent/java-concurrent-questions-01.md132-162

同步机制

synchronized 关键字

Java 中的 synchronized 关键字用于控制多个线程对共享资源的访问。它可以应用于方法或代码块。

当使用 synchronized 时,一次只有一个线程可以访问同步块,从而实现互斥。

有三种使用 synchronized 关键字的方法:

  1. 实例方法同步:锁定当前对象实例

  2. 静态方法同步:锁定 Class 对象

  3. 代码块同步:锁定指定对象

在底层,synchronized 使用监视器(也称为内部锁或监视器锁)来实现互斥。

来源:docs/java/concurrent/java-concurrent-questions-02.md430-563

volatile 关键字

Java 中的 volatile 关键字用于指示变量的值将被不同线程修改。

volatile 关键字保证:

  1. 可见性:当一个线程修改一个 volatile 变量时,所有其他线程会立即看到更新后的值。
  2. 有序性:防止涉及 volatile 变量的指令重排序。

然而,volatile 不为复合操作(如自增 i++)提供原子性。

来源:docs/java/concurrent/java-concurrent-questions-02.md22-162

ThreadLocal

目的和机制

ThreadLocal 提供了一种存储特定于当前线程的数据的方式,确保每个线程都有自己独立初始化的变量副本。

主要特性

  • 每个访问 ThreadLocal 变量的线程都有其独立初始化的变量副本。
  • 一个线程对变量所做的更改不会影响其他线程中的副本。
  • 用于线程局部数据或每个线程的上下文。

ThreadLocal 内存泄漏

ThreadLocal 变量如果使用不当可能导致内存泄漏。这是因为:

  1. 每个 Thread 中的 ThreadLocalMapThreadLocal 作为键(弱引用)和实际值(强引用)持有 Entry 对象。
  2. 如果 ThreadLocal 对象被垃圾回收,但线程仍然存活(例如在线程池中),那么值将无法被垃圾回收。
  3. 这会导致内存泄漏,尤其是在使用线程池的长时间运行的应用程序中。

最佳实践:当您不再需要 ThreadLocal 变量时,请务必调用 threadLocal.remove(),尤其是在线程池环境中。

来源:docs/java/concurrent/java-concurrent-questions-03.md17-225

线程池

线程池管理着一个工作线程池,减少了线程创建的开销。Java 提供了 ThreadPoolExecutor 类来创建和管理线程池。

ThreadPoolExecutor 参数

ThreadPoolExecutor 类提供了一个可配置的线程池,具有几个关键参数:

参数描述
corePoolSize池中要保留的线程数,即使它们是空闲的
maximumPoolSize池中允许的最大线程数
keepAliveTime当线程数大于核心线程数时,这是多余的空闲线程在终止前等待新任务的最大时间
workQueue用于在任务执行前保存任务的队列
threadFactory用于创建新线程的工厂
handler任务执行被阻塞时要使用的处理程序

线程池执行流程

当一个新任务提交到线程池时,会发生以下步骤:

  1. 如果正在运行的线程数少于 corePoolSize,则创建一个新线程来处理该请求。
  2. 如果正在运行的线程数多于 corePoolSize 但少于 maximumPoolSize,则仅当队列已满时才创建一个新线程。
  3. 如果队列已满且所有线程都在活动中,则根据拒绝策略拒绝该任务。

拒绝策略

Java 提供了四种内置拒绝策略:

  1. AbortPolicy:抛出 RejectedExecutionException(默认)。
  2. CallerRunsPolicy:在调用者的线程中执行任务。
  3. DiscardPolicy:默默地丢弃任务。
  4. DiscardOldestPolicy:丢弃最旧的未处理任务并重试。

来源:docs/java/concurrent/java-concurrent-questions-03.md232-621 docs/java/concurrent/java-thread-pool-summary.md14-136

CompletableFuture 和异步编程

Future vs CompletableFuture

Future 接口代表异步计算的结果,但它存在局限性:

  • 它没有提供计算完成时的通知。
  • 它没有提供链接计算的方法。
  • 它没有提供异常处理机制。

CompletableFuture 通过提供全面的异步编程 API 来解决这些局限性。

组合与链式调用

CompletableFuture 允许链接异步操作。

CompletableFuture 最佳实践

  1. 使用自定义线程池:避免将通用的 ForkJoinPool 用于所有异步操作。

  2. 正确处理异常:使用 exceptionallyhandle 方法。

  3. 组合多个 Future:使用 allOfanyOf 来等待多个 Future。

来源:docs/java/concurrent/java-concurrent-questions-03.md814-952 docs/java/concurrent/completablefuture-intro.md1-33

AQS (AbstractQueuedSynchronizer)

什么是 AQS?

AbstractQueuedSynchronizer (AQS) 是 Java 并发包中许多同步类的基础。AQS 为实现阻塞锁和相关同步实用程序(如信号量、倒计时锁存器等)提供了一个框架。

AQS 核心原则

  1. 状态管理:AQS 使用一个整数字段 state 来表示同步状态。
  2. 独占/共享模式:支持独占(锁)和共享(信号量)访问。
  3. 队列管理:使用 CLH 锁队列变体维护一个等待线程的 FIFO 队列。
  4. 模板方法模式:通过覆盖 tryAcquiretryRelease 等方法进行自定义。

AQS 中的节点状态

AQS 队列中的节点可以具有以下任一状态:

状态管理描述
CANCELLED1线程已被取消(超时或中断)。
SIGNAL-1当当前节点释放锁时,后继线程需要被唤醒。
CONDITION-2线程正在等待一个条件。
PROPAGATE-3共享锁的传播(确保共享获取的传播)。
(initial)0上述状态之外的任何状态。

AQS 的工作原理

来源:docs/java/concurrent/aqs.md10-102 docs/java/concurrent/java-concurrent-questions-03.md976-1021

Java 中的锁

Java 提供了各种锁实现来实现线程同步,从内部锁(synchronized)到更灵活的显式锁。

悲观锁 vs 乐观锁

Java 支持悲观锁和乐观锁并发控制。

方面悲观锁乐观锁
假设假设会发生冲突。假设冲突很少发生。
实现synchronized, ReentrantLockAtomicInteger, LongAdder
机制阻塞线程。使用 CAS(比较并交换)。
性能较高的冲突开销。较低的冲突开销。
用例高冲突场景,写密集型工作负载。低冲突场景,读密集型工作负载。

ReentrantLock vs Synchronized

ReentrantLocksynchronized 关键字相比提供了更大的灵活性。

功能ReentrantLockSynchronized
锁获取显式 (lock())隐式(进入代码块)
锁释放显式 (unlock())隐式(退出代码块)
公平性支持公平和非公平模式。始终非公平。
可中断性支持 (lockInterruptibly())。不支持
尝试获取锁支持 (tryLock())。不支持
多个条件支持(通过 newCondition())。不支持(每个对象只有一个等待集)。
性能在现代 JVM 中与 synchronized 类似。在现代 JVM 中与 ReentrantLock 类似。

ReentrantLocksynchronized 都是可重入的,这意味着持有锁的线程可以再次获取它而不会阻塞。

来源: docs/java/concurrent/java-concurrent-questions-02.md178-446 docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md1-32

原子类 (Atomic Classes)

java.util.concurrent.atomic 包提供了支持单变量无锁线程安全编程的类。这些类通过 Compare-And-Swap (CAS) 机制使用 CPU 原子指令。

原子类的类型

原子操作如何工作

原子操作使用硬件级别的原子指令,无需锁即可确保线程安全。核心机制是 Compare-And-Swap (CAS)

  1. 读取当前值(期望值)
  2. 基于期望值计算新值
  3. 尝试使用 CAS 操作更新值
  4. 如果成功,则返回;如果失败,则从步骤 1 重试

性能考量

对于高竞争场景,请考虑使用 LongAdder 等累加器类,而不是 AtomicLong。这些类通过维护多个计数器来减少争用,计数器仅在读取总计时进行合并。

来源: docs/java/concurrent/atomic-classes.md1-16 docs/java/concurrent/java-concurrent-questions-02.md284-326

并发最佳实践

线程池最佳实践

  1. 在生产环境中避免使用 Executors 工厂方法:使用 ThreadPoolExecutor 构造函数创建线程池,以避免潜在的资源耗尽问题。
  2. 适当配置线程池大小:
    • CPU密集型任务:threadCount = CPU核数 + 1
    • I/O密集型任务:threadCount = CPU核数 * 2
  3. 命名您的线程:使用自定义 ThreadFactory 以便更好地调试。
  4. 使用有界队列:限制队列大小以避免内存问题。
  5. 监控线程池指标:跟踪活动线程、队列大小和已完成任务。
  6. 为不同类型的任务使用单独的线程池:不要混合 CPU 密集型和 I/O 密集型任务。
  7. 优雅地处理被拒绝的任务:根据您的用例选择合适的拒绝策略。

避免死锁

死锁是指两个或多个线程无限期阻塞,每个线程都在等待由其他线程持有的资源。为防止死锁:

  1. 避免嵌套锁:尽量减少同时持有的锁的数量。
  2. 使用锁顺序:在所有线程中始终以相同的顺序获取锁。
  3. 使用锁超时:使用带超时的 tryLock 以避免无限期等待。
  4. 检测和恢复:实现死锁检测机制。

线程安全策略

  1. 不变性:不可变对象本身就是线程安全的。
  2. 同步:在共享可变状态时,使用 synchronized 或显式锁。
  3. 线程封闭:将状态限制在单个线程内。
  4. 线程局部存储:使用 ThreadLocal 来存储每个线程的状态。
  5. 原子变量:对简单的共享状态使用原子类。
  6. 并发集合:使用 java.util.concurrent 中的线程安全集合。

来源: docs/java/concurrent/java-thread-pool-best-practices.md8-121 docs/java/concurrent/java-concurrent-questions-01.md266-407