并发
并发
多线程
问题集锦-线程
线程的创建方式有哪些?
继承 Thread 类
- 通过继承并重写
run()
方法创建线程,适合简单任务。缺点:单继承限制灵活性(如 Java)。
- 通过继承并重写
实现接口(Runnable/Callable)
- Runnable:解耦任务与线程管理,支持多线程共享同一任务对象。
- Callable:支持返回值和异常处理,需配合
FutureTask
使用,适合异步结果场景。 - C 语言的 pthread:使用
pthread_create
函数创建线程,需链接pthread
库。
函数/任务传递(无需继承)
- 直接传递函数或 Lambda 表达式(如 Python 的
target
参数、Java 的 Lambda)。
- 直接传递函数或 Lambda 表达式(如 Python 的
线程池管理
- Java 的 ExecutorService:复用线程资源,控制线程数量和执行策略。
- Python 的 ThreadPoolExecutor:自动管理线程生命周期,适合高并发场景。
其他高级方式
- 协程:单线程内并发(如 Python asyncio),适合 I/O 密集型任务。
- 守护线程:主线程结束时自动终止,适用于后台任务(如日志监控)。
- 第三方库:如 Java 的 Akka(基于 Actor 模型)提供细粒度并发控制。
选择建议
- 简单任务优先函数传递,需返回值用 Callable,高并发用线程池,跨平台/C 开发用 pthread。
怎么启动线程 ?
通过 Thread.start()
方法启动线程。
如何停止一个线程的运行?
标志位控制退出
- Java 使用
volatile
布尔变量作为退出标志,线程循环检查标志并主动退出。 - Python 通过
threading.Event
或全局变量实现,支持安全退出和资源清理。
- Java 使用
中断机制(Java 推荐)
- 调用
Thread.interrupt()
设置中断标志,通过isInterrupted()
检查状态。 - 阻塞方法(如
sleep()
)触发InterruptedException
,需捕获并恢复中断标志。
- 调用
线程池管理工具
- Java 的
ExecutorService
通过shutdown()
平缓关闭或shutdownNow()
强制终止。 - 使用
Future.cancel(true)
取消正在执行的任务。
- Java 的
Python 特有方法
- 守护线程随主线程结束自动终止,适合无需清理的后台任务。
concurrent.futures
模块支持通过ThreadPoolExecutor
取消任务。
强制终止风险
- Java 的
Thread.stop()
已废弃,可能导致资源泄漏或数据不一致。 - Python 的
_stop()
非官方 API,存在安全隐患。
- Java 的
开发实践建议
- Java 优先选择中断机制或标志位,Python 推荐
threading.Event
。 - 处理阻塞操作时需恢复中断状态(如
try-catch
内调用interrupt()
)。 - 高并发场景使用线程池统一管理,避免直接操作线程。
- Java 优先选择中断机制或标志位,Python 推荐
调用 interrupt 是如何让线程抛出异常的?
中断标志位触发机制
interrupt()
设置线程中断标志位(false
→true
),不会直接中断运行中的线程。- 阻塞方法(如
sleep()
、wait()
)内部自动检查标志位,若为true
则抛出InterruptedException
。
阻塞状态下的异常流程
- 线程在
sleep()
等阻塞状态被中断时,JVM 会:- 清除中断标志位(重置为
false
)。 - 抛出
InterruptedException
使线程退出阻塞。
- 清除中断标志位(重置为
- 线程在
非阻塞状态行为差异
- 线程运行时调用
interrupt()
仅设置标志位,需通过isInterrupted()
主动检查并终止任务。 LockSupport.park()
等操作被中断时不抛异常,但保持标志位为true
,需手动检查。
- 线程运行时调用
中断标志位动态处理
- 抛出
InterruptedException
的方法会重置标志位,捕获异常后需调用interrupt()
重新标记中断状态。
- 抛出
底层实现原理
- JVM 在阻塞方法中隐式插入中断检查逻辑,检测到标志位为
true
时触发本地方法抛异常,依赖线程调度器协作。
- JVM 在阻塞方法中隐式插入中断检查逻辑,检测到标志位为
Java 线程的状态有哪些?
NEW 新建状态
- 线程对象通过
new Thread()
创建后未调用start()
,未关联操作系统底层线程。
- 线程对象通过
RUNNABLE 可运行状态
- 调用
start()
后进入此状态,包含就绪(等待 CPU 分配)和运行中(执行run()
方法)两种子状态。 - 操作系统层面的阻塞 I/O 在 Java 中仍归为 RUNNABLE。
- 调用
BLOCKED 阻塞状态
- 竞争
synchronized
锁失败时进入此状态,进入监视器锁的阻塞队列,等待锁释放后被唤醒。
- 竞争
WAITING 无限等待状态
- 主动调用
Object.wait()
、Thread.join()
或LockSupport.park()
,需其他线程显式唤醒(如notify()
、interrupt()
)。
- 主动调用
TIMED_WAITING 超时等待状态
- 调用带超时参数的方法如
Thread.sleep(long)
、Object.wait(long)
,超时或中断后自动恢复 RUNNABLE。
- 调用带超时参数的方法如
TERMINATED 终止状态
run()
方法执行完毕或抛出未捕获异常后进入此状态,不可重启。
状态转换规则
- NEW → RUNNABLE:调用
start()
。 - RUNNABLE → BLOCKED:竞争
synchronized
锁失败。 - RUNNABLE → WAITING:调用
wait()
/join()
。 - RUNNABLE → TIMED_WAITING:调用
sleep()
/wait(long)
。 - BLOCKED → RUNNABLE:成功获取锁。
- WAITING/TIMED_WAITING → RUNNABLE:被唤醒、中断或超时。
- NEW → RUNNABLE:调用
blocked 和 waiting 有啥区别
触发条件与锁资源行为
- BLOCKED:被动触发(锁竞争失败),不释放已持有的锁。
- WAITING:主动调用
wait()
、join()
或park()
,释放当前对象锁。
唤醒机制
- BLOCKED:锁被释放后自动唤醒,参与锁竞争。
- WAITING:需显式唤醒(如
notify()
、unpark()
或目标线程终止)。
应用场景
- BLOCKED:高并发锁竞争(如多线程访问共享资源)。
- WAITING:线程协作(如生产者-消费者模型)。
状态转换
- BLOCKED → RUNNABLE:成功获取锁后转换。
- WAITING → RUNNABLE:唤醒后需重新获取锁,成功则进入 RUNNABLE,失败则转为 BLOCKED。
JVM 管理队列
- BLOCKED:同步队列(Entry Set)。
- WAITING:等待队列(Wait Set)。
总结对比表
特性 | BLOCKED | WAITING |
---|---|---|
触发方式 | 被动(锁竞争失败) | 主动(调用等待方法) |
锁释放 | 不释放锁(若未持有则不涉及) | 必须释放当前对象锁 |
唤醒机制 | 自动(锁可用时) | 依赖外部显式操作(如 notify() ) |
常见场景 | 高并发锁竞争 | 线程协作等待条件 |
JVM 管理队列 | 同步队列(Entry Set) | 等待队列(Wait Set) |
wait 状态下的线程如何进行恢复到 running 状态?
显式唤醒
- 其他线程在同步块内调用
notify()
或notifyAll()
,唤醒一个或所有等待线程。 - 被唤醒线程转为 BLOCKED 状态尝试重新获取锁,成功则进入 RUNNABLE。
- 其他线程在同步块内调用
中断唤醒
- 外部调用
interrupt()
使等待线程抛出InterruptedException
,需在catch
块处理异常并调用Thread.currentThread().interrupt()
恢复中断标志。
- 外部调用
锁竞争与状态转换
- 被唤醒线程必须重新竞争锁:
- 成功获取锁 → RUNNABLE。
- 锁被占用 → BLOCKED 等待锁释放。
- 被唤醒线程必须重新竞争锁:
注意事项
wait()
/notify()
必须在同步块内调用,否则抛出IllegalMonitorStateException
。- 推荐用
while
循环检查条件,防止虚假唤醒(未被notify
但被唤醒)。 LockSupport.park()
的线程需通过unpark()
唤醒,无锁要求但需管理许可(permit)。
问题集锦-wait/notify 机制
notify 和 notifyAll 的区别?
唤醒线程数量
notify()
随机唤醒一个等待线程。notifyAll()
唤醒所有等待线程。
适用场景
notify()
适合单一任务场景(如生产者仅需唤醒一个消费者)。notifyAll()
适合条件变化需全体响应的场景(如资源释放后通知所有消费者)。
性能与风险
notify()
性能开销小,但可能导致线程饥饿或条件未满足的线程被唤醒。notifyAll()
安全性高,但会增加锁竞争和上下文切换开销。
锁竞争机制
notify()
唤醒的线程直接进入锁池竞争锁。notifyAll()
唤醒的所有线程需先进入锁池竞争,只有获取锁的线程继续执行。
编码要求
- 两者必须在
synchronized
块内调用,否则抛出IllegalMonitorStateException
。 - 被唤醒线程需用
while
循环检查条件,避免虚假唤醒。 - 若无特殊需求,优先使用
notifyAll()
保证安全性。
- 两者必须在
notify 选择哪个线程?
JVM 规范与实现差异
- 官方规范:
notify()
唤醒的线程是任意的(arbitrary),具体选择由 JVM 实现决定。 - HotSpot 实现:按等待队列的 FIFO 顺序唤醒第一个线程(如线程 A → B → C 等待时,先唤醒 A)。
- 官方规范:
实际唤醒机制
- 等待队列结构:
wait()
进入队列的线程按调用顺序排列,notify()
从队列头部选取线程唤醒。 - 被唤醒线程需重新竞争锁,可能因锁被占用再次进入 BLOCKED 状态。
- 等待队列结构:
开发者应对策略
- 避免依赖唤醒顺序:不同 JVM 实现可能不同,代码设计需假设唤醒顺序不可控。
- 使用
while
循环检查条件:确保被唤醒后验证条件,避免虚假唤醒或非目标线程执行。 - 明确唤醒目标:通过条件变量或多监视器对象间接控制唤醒逻辑,而非依赖
notify()
默认行为。
与
notifyAll()
对比notifyAll()
唤醒所有线程,但最终仅一个线程获取锁,适用于条件变化需全体响应的场景。
Java 里面的线程和操作系统的线程一样吗?
本质关系:1:1 映射实现
- 现代 Java 线程(JDK 1.2+)基于操作系统原生线程实现(如 Linux 的
pthread
),每个 Java 线程对应一个内核线程。 - 通过
pthread_create
等系统调用创建内核线程,由操作系统调度器(如 Linux CFS)分配 CPU 时间片。例如new Thread()
会触发 JVM 调用pthread_create
。
- 现代 Java 线程(JDK 1.2+)基于操作系统原生线程实现(如 Linux 的
核心区别:抽象层级与资源管理
- 调度机制:
- Java 线程优先级(如
Thread.MAX_PRIORITY
)会被映射到操作系统优先级(如 Linux 的nice
值),映射规则因平台而异。 - 操作系统可通过
sched_setscheduler()
设置实时调度策略(如SCHED_FIFO
),需 root 权限且影响系统稳定性。
- Java 线程优先级(如
- 状态管理:
- Java 线程状态(
RUNNABLE
、BLOCKED
)是 JVM 逻辑状态,不完全对应内核线程的ready
或waiting
状态。
- Java 线程状态(
- 资源开销:Java 线程需额外 JVM 堆内存,上下文切换成本高于原生线程。
- 调度机制:
历史演变与新型线程
- 绿色线程(JDK 1.2 前):用户态线程,JVM 自行调度,无法利用多核。
- 虚拟线程(Java 19+):轻量级用户态线程,由 JVM 调度,多虚拟线程复用少量内核线程,适合高并发 I/O 场景。
开发注意事项
- 线程数量控制:避免过度创建内核线程(受限于
ulimit -u
),推荐使用ThreadPoolExecutor
。 - 线程类型选择:
- CPU 密集型任务:优先传统线程(1:1 模型)。
- I/O 密集型任务:优先虚拟线程(减少上下文切换)。
- 跨平台差异:Windows 支持 7 级优先级,Linux 优先级范围更窄,需谨慎依赖优先级逻辑。
- 线程数量控制:避免过度创建内核线程(受限于
使用多线程要注意哪些问题?
线程安全问题
- 共享资源竞争:多线程同时修改共享变量(如计数器)会导致数据不一致。
- 解决方案:使用
synchronized
、Lock
或原子类(如AtomicInteger
),或通过线程安全容器(如ConcurrentHashMap
)管理共享数据。
- 解决方案:使用
- 不可变对象:优先设计不可变对象(如
String
),避免同步开销。
- 共享资源竞争:多线程同时修改共享变量(如计数器)会导致数据不一致。
死锁与活锁
- 死锁条件:互斥、持有并等待、不可抢占、循环等待。
- 预防:按固定顺序获取锁,使用
tryLock()
超时机制,避免嵌套锁。
- 预防:按固定顺序获取锁,使用
- 活锁:线程不断重试失败操作(如消息处理冲突),可通过随机退避策略解决。
- 死锁条件:互斥、持有并等待、不可抢占、循环等待。
同步机制的选择
- synchronized:简单但粒度粗,适用于低竞争场景。
- Lock/Condition:更灵活(如可中断、超时),适合高竞争或复杂条件等待。
- CAS 无锁编程:通过
Atomic
类实现,适合高并发但低冲突场景(如计数器)。
资源管理与泄漏
- 线程泄漏:未正确关闭线程池,导致线程持续占用资源。
- 建议:使用
ExecutorService
并调用shutdown()
,避免手动创建线程。
- 建议:使用
- 资源未释放:锁或 I/O 资源未在
finally
块中释放,导致死锁或泄漏。
- 线程泄漏:未正确关闭线程池,导致线程持续占用资源。
性能与开销
- 上下文切换成本:线程过多会导致 CPU 时间浪费在切换而非任务执行。
- 优化:根据任务类型(CPU/I/O 密集)调整线程池大小,或使用协程(如虚拟线程)。
- 伪共享(False Sharing):多个线程修改同一缓存行的不同变量,导致缓存失效。
- 解决:使用
@Contended
注解或填充字节对齐变量。
- 解决:使用
- 上下文切换成本:线程过多会导致 CPU 时间浪费在切换而非任务执行。
内存可见性
- volatile 关键字:确保变量修改对其他线程立即可见,但无法保证原子性。
- happens-before 规则:通过同步操作(如
synchronized
、Lock
)建立内存屏障,避免指令重排序问题。
异常处理
- 未捕获异常:线程内未捕获的异常会导致线程终止且无日志。
- 处理:通过
UncaughtExceptionHandler
统一捕获并记录异常。
- 处理:通过
- 未捕获异常:线程内未捕获的异常会导致线程终止且无日志。
ThreadLocal 的使用与风险
- 适用场景:保存线程私有数据(如数据库连接、用户会话)。
- 内存泄漏:线程池中线程复用可能导致
ThreadLocal
数据长期未被清理。- 解决:使用后调用
remove()
方法清除数据。
- 解决:使用后调用
并发工具的正确使用
- 并发容器:优先使用
ConcurrentHashMap
、CopyOnWriteArrayList
代替同步容器(如Hashtable
)。 - CountDownLatch/CyclicBarrier:协调多线程阶段性任务,避免忙等待。
- 并发容器:优先使用
平台与版本差异
- 虚拟线程(Java 19+):适用于高并发 I/O 场景,但需注意与传统线程的交互。
- 操作系统限制:Linux 默认线程数限制(
ulimit -u
),需调整参数支持大规模线程池。
保证数据的一致性有哪些方案呢?
事务型协议方案
- 两阶段提交 (2PC):协调者与参与者分准备、提交两阶段交互,保证强一致性,但存在单点故障和性能瓶颈。
- 三阶段提交 (3PC):增加预提交阶段,减少阻塞时间,复杂度更高。
- TCC 模式:通过 Try-Confirm-Cancel 实现柔性事务,需手动补偿逻辑,适合复杂业务。
异步最终一致性方案
- 消息队列:通过 Kafka、RabbitMQ 异步传递数据变更事件,结合重试机制实现最终一致性。
- Saga 模式:拆分长事务为子事务,失败时触发补偿操作,适合订单、支付等链式场景。
锁与校验机制
- 分布式锁:使用 Redis/Zookeeper 实现互斥访问,注意锁超时和死锁问题。
- 数据版本控制:基于时间戳或序列号实现乐观锁(CAS),减少锁竞争。
- 哈希校验:计算数据哈希值,用于快速验证一致性,适用于文件传输。
数据同步策略
- 强一致性复制:采用 Paxos、Raft 协议确保多副本实时同步,适合金融等高要求场景。
- 异步复制:通过日志同步降低延迟,容忍短期不一致,适合跨地域系统。
- 双写策略:主备数据库同时写入,结合冲突检测(如 Last Write Wins)保障一致性。
新型技术方案
- 区块链:基于共识算法(如 PoW/PoS)实现不可篡改账本,适合供应链、版权管理。
- 云原生数据库:如 CockroachDB 支持多活架构和自动分片,内置强一致性。
- 数据网格 (Data Mesh):通过领域自治和联邦治理实现跨平台一致性,需标准化接口。
实践建议
- 强一致性选 2PC/TCC,最终一致性优先消息队列或 Saga。
- 高并发场景避免锁竞争,采用无锁设计或异步补偿。
- 结合监控告警和自动修复机制,降低不一致风险。
sleep 和 wait 的区别是什么?
所属类与方法类型
sleep()
是Thread
类的静态方法,可直接通过类名调用。wait()
是Object
类的实例方法,必须在同步代码块或方法中调用。
锁的释放行为
sleep()
不释放持有的锁,其他线程无法进入同步代码块。wait()
会释放当前对象的锁,允许其他线程获取锁执行同步代码。
唤醒机制与状态
sleep()
超时后自动恢复,线程进入 TIMED_WAITING 状态。wait()
需通过notify()
/notifyAll()
或超时唤醒:- 无超时参数时进入 WAITING 状态。
- 有超时参数时进入 TIMED_WAITING 状态。
使用场景与异常处理
sleep()
用于独立延时任务(如定时轮询),需处理InterruptedException
。wait()
用于多线程协作(如生产者-消费者模型),需处理InterruptedException
和IllegalMonitorStateException
(未正确同步时抛出)。
参数与底层实现
sleep()
支持纳秒级精度参数(实际依赖操作系统调度)。wait()
必须通过对象锁调用,支持超时参数(毫秒 + 纳秒)。
核心区别总结
特性 | sleep() | wait() |
---|---|---|
锁释放 | ❌ 不释放 | ✅ 释放 |
调用位置 | 任意位置 | 同步块/方法内 |
唤醒方式 | 自动恢复 | 需 notify() 或超时 |
线程状态 | TIMED_WAITING | WAITING / TIMED_WAITING |
适用场景 | 独立延时任务 | 多线程协作 |
sleep 会释放 cpu 吗?
CPU 释放机制
Thread.sleep()
调用时线程主动让出 CPU 时间片,进入 TIMED_WAITING 状态,操作系统将其移出可运行队列,CPU 资源分配给其他就绪线程。
锁资源的保持
sleep()
不会释放已持有的锁(如synchronized
同步块中的锁),其他线程无法获取同一锁。
性能影响与上下文切换
- 短时间频繁调用
sleep()
(如毫秒级)会导致高频线程切换,增加 CPU 调度开销。 - 建议:高并发场景优先延长休眠时间(如 10ms 替代 1ms)或使用异步等待(如
CompletableFuture.delay()
)。
- 短时间频繁调用
使用场景建议
- 适用场景:独立延时任务(如定时轮询)、模拟耗时操作、调试。
- 不适用场景:线程协作(需用
wait()
/notify()
)、高精度实时任务(受操作系统调度影响)。
异常处理
- 必须捕获
InterruptedException
,并在捕获后通过Thread.currentThread().interrupt()
恢复中断状态。
- 必须捕获
不同的线程之间如何通信?
等待/通知机制(
wait()
/notify()
)- 基于共享对象的监视器锁实现:线程调用
wait()
释放锁并等待,其他线程通过notify()
/notifyAll()
唤醒。 - 适用场景:生产者-消费者模型等需要协调顺序的共享资源操作。
- 注意:必须在
synchronized
代码块内使用,配合while
循环防止虚假唤醒。
- 基于共享对象的监视器锁实现:线程调用
共享变量与
volatile
- 通过共享变量传递数据,
volatile
保证可见性但不保证原子性。 - 适用场景:简单状态标志(如线程启停控制)。
- 通过共享变量传递数据,
Lock
与Condition
ReentrantLock
替代synchronized
,Condition
提供多条件队列支持,可精确控制唤醒顺序。
阻塞队列(
BlockingQueue
)- 线程安全队列自动处理阻塞逻辑(如空队列阻塞消费者,满队列阻塞生产者)。
- 实现类:
ArrayBlockingQueue
(固定容量)、LinkedBlockingQueue
(可选容量)。 - 适用场景:生产者-消费者模式的数据传递与同步。
并发工具类
CountDownLatch
:主线程等待多个子线程完成任务(如初始化)。CyclicBarrier
:多线程相互等待至共同点后继续(可重用)。Semaphore
:控制资源并发访问数(如连接池)。
管道流(
PipedInputStream
/PipedOutputStream
)- 基于字节流的单向数据传输,适用简单数据传递场景,性能较低。
信号量与原子类
Semaphore
控制资源访问权限,支持公平/非公平策略。AtomicInteger
等原子类实现无锁线程安全计数或状态管理。
并发安全
问题集锦-理论
怎么理解可重入锁?
核心定义与特性
- 允许同一线程多次获取同一把锁而不会死锁。
- 重入计数器:每次获取锁时计数器递增,释放时递减,归零时锁完全释放。
- 线程标识绑定:锁内部记录当前持有锁的线程,确保只有持有者能重入。
- 避免递归死锁:线程在递归或嵌套方法中调用同一锁保护的代码时无需重复等待。
与不可重入锁的对比
- 不可重入锁:同一线程重复获取锁时会阻塞,导致死锁(如自旋锁在递归中无限等待)。
- 可重入锁优势:通过计数器机制安全重复进入同步区域,提升代码灵活性。
Java 中的实现与应用
ReentrantLock
- 显式锁,支持公平性选择(公平锁按请求顺序分配,非公平锁允许插队)。
- 支持条件变量(
Condition
)实现精准线程唤醒(如生产者-消费者模型的队列分离)。
synchronized
关键字- 隐式可重入锁,功能有限(不支持中断或超时)。
典型应用场景
- 递归算法:递归函数多次获取同一锁保护共享资源。
- 同步嵌套调用:类中多个方法互斥访问共享资源且存在调用链。
- 复杂线程协作:通过
Condition
实现多等待队列(如任务调度优先级控制)。
注意事项
- 锁释放匹配:每次获取锁必须对应释放,否则计数器无法归零导致死锁。
- 性能权衡:公平锁减少线程饥饿但增加切换开销,非公平锁反之。
- 避免过度嵌套:深层嵌套增加调试复杂度(需追踪计数器状态)。
多线程安全保障的核心策略
同步机制
- 互斥锁
synchronized
:自动管理锁,适用于简单临界区(如计数器操作)。ReentrantLock
:支持可中断锁、公平锁及条件变量,适合复杂同步需求(如超时控制)。
- 读写锁
ReentrantReadWriteLock
:读共享、写独占,提升读多写少场景性能(如缓存系统)。
- 互斥锁
无锁编程
- 原子类
AtomicInteger
/AtomicReference
:基于 CAS 实现无锁线程安全操作(如高并发计数)。
- 可见性控制
volatile
:保证变量修改的可见性,适用于简单状态标志(需结合锁保证原子性)。
- 原子类
线程隔离与不可变设计
- 线程局部存储
ThreadLocal
:为每个线程提供独立变量副本(如会话管理),需调用remove()
防泄漏。
- 不可变对象
- 对象状态不可修改(如
String
、不可变集合),天然线程安全。
- 对象状态不可修改(如
- 线程局部存储
线程安全数据结构
- 并发集合
ConcurrentHashMap
:分段锁优化高并发读写,性能优于HashTable
。CopyOnWriteArrayList
:写时复制机制,适合读多写少场景(如事件监听器列表)。
- 阻塞队列
BlockingQueue
:自动阻塞管理(如LinkedBlockingQueue
),简化生产者-消费者模型。
- 并发集合
并发工具类
- 任务协调工具
CountDownLatch
:主线程等待多个子任务完成(如服务初始化)。CyclicBarrier
:多线程同步至共同阶段后继续执行(如分批次数据处理)。
- 资源控制工具
Semaphore
:控制资源并发访问量(如数据库连接池限流)。
- 任务协调工具
执行框架类
- 线程池管理
ThreadPoolExecutor
:灵活配置线程池参数(核心线程数、队列策略等)。
- 异步任务管理
FutureTask
:封装异步计算结果,支持阻塞获取结果(如并行计算任务)。
- 线程池管理
锁与条件类
- 高级锁机制
StampedLock
:乐观读锁,减少读写锁竞争(读多写少场景优化)。
- 条件变量
Condition
:与ReentrantLock
配合,实现细粒度等待/唤醒(如生产者-消费者模型)。
- 高级锁机制
设计原则与优化
- 锁粒度控制
- 仅同步必要代码块(如共享变量操作),减少锁竞争时间。
- 死锁预防
- 统一锁顺序、设置超时(
tryLock
)、使用检测工具(如jstack
)。
- 统一锁顺序、设置超时(
- 性能优化策略
- 读多写少场景优先用无锁或乐观锁,高并发场景结合线程池管理。
- 锁粒度控制
什么是公平锁和非公平锁?
公平锁
- 获取顺序:严格按线程请求顺序分配锁,遵循“先到先得”原则,释放锁时队列中等待最久的线程优先获取。
- 实现机制
- 维护 FIFO 等待队列,通过
hasQueuedPredecessors()
检查队列状态。 - 示例:Java 的
ReentrantLock(true)
在获取锁前检查队列。
- 维护 FIFO 等待队列,通过
- 特点
- 优点:避免线程饥饿。
- 缺点:性能较低,因维护队列和线程切换开销增加。
非公平锁
- 获取顺序:允许线程直接竞争锁,新请求线程可能插队抢占锁,无需检查队列。
- 实现机制
- 直接尝试 CAS 获取锁,失败后才加入队列。
- 示例:Java 的
ReentrantLock()
默认模式。
- 特点
- 优点:吞吐量高,减少线程调度开销。
- 缺点:可能导致线程饥饿。
核心区别
- 锁分配顺序:公平锁严格按队列顺序,非公平锁允许插队。
- 性能:非公平锁性能更高(减少线程切换),公平锁适合对公平性敏感的场景。
- 实现复杂度:公平锁需维护队列状态,非公平锁实现更简单。
Java 中的实现
- ReentrantLock:通过构造函数参数
true
启用公平锁,默认或false
为非公平锁。 - synchronized:底层固定为非公平锁,不支持配置。
- ReentrantLock:通过构造函数参数
适用场景
- 公平锁:需严格顺序的场景(如银行转账、任务调度)。
- 非公平锁:高并发且锁竞争不激烈(如缓存读取、短任务处理)。
非公平锁吞吐量为什么比公平锁大?
减少上下文切换开销
- 非公平锁允许新线程直接抢占锁,避免公平锁强制唤醒队列头部线程的上下文切换(耗时 1-10 微秒)。
- 刚释放锁的线程可能立即重新获取,减少线程状态切换次数。
利用 CPU 缓存局部性
- 非公平锁允许刚释放锁的线程(缓存未失效)快速重入,避免公平锁切换线程导致的缓存重新加载(延迟 100-300 纳秒)。
避免队列维护成本
- 非公平锁仅在竞争失败时加入队列,公平锁每次获取需检查队列状态(增加 20-50 纳秒/次开销)。
- 实验数据:64 线程时非公平锁吞吐量下降幅度比公平锁低 60%-80%。
减少线程挂起概率
- 锁持有时间短时,非公平锁有 30%-50% 概率让新线程直接获取锁,避免挂起和内核调度介入(耗时 1-5 微秒)。
优化系统调用频率
- 非公平锁通过 CAS 自旋减少
park()
系统调用次数,相同并发量下系统调用比公平锁减少 40%-70%。 - 高并发测试(64 线程)中吞吐量可达公平锁的 5-8 倍。
- 非公平锁通过 CAS 自旋减少
什么情况会产生死锁问题?如何解决?
死锁产生的核心条件
- 互斥条件:资源被独占使用(如数据库写锁)。
- 请求与保持:持有资源的同时请求新资源(如线程A持有锁1后请求锁2)。
- 不可剥夺:资源只能由持有者主动释放。
- 循环等待:形成等待链(如线程A→B→C→A)。
典型死锁场景
- 多线程嵌套锁:线程以不同顺序获取多个锁(如线程1锁A后锁B,线程2锁B后锁A)。
- 数据库事务冲突:事务交叉更新记录(如事务T1更新X后请求Y,T2更新Y后请求X)。
- 资源分配不合理:短任务高频竞争不可剥夺资源(如内存不足时进程互相等待)。
死锁解决方法
- 预防策略
- 统一资源申请顺序:按固定编号顺序获取锁(如先锁表A再锁表B)。
- 一次性申请所有资源:启动前声明所需全部资源。
- 动态避免
- 银行家算法:动态检测资源分配安全性。
- 超时机制:设置锁获取超时(如
tryLock(5, TimeUnit.SECONDS)
)。
- 检测与恢复
- 资源分配图检测:周期性扫描环路并终止部分进程。
- 事务回滚:数据库自动回滚代价最小的事务。
- 工程实践优化
- 减少锁粒度:使用读写锁(如
ReentrantReadWriteLock
)。 - 无锁编程:采用 CAS(如
AtomicInteger
)或线程本地存储。
- 减少锁粒度:使用读写锁(如
- 预防策略
不同场景的解决方案选择
- 高并发服务:非公平锁+超时检测(如电商秒杀系统用
ReentrantLock
)。 - 金融交易系统:统一资源访问顺序+事务隔离级别(按账户ID升序更新)。
- 实时操作系统:资源抢占机制(允许高优先级进程回收资源)。
- 高并发服务:非公平锁+超时检测(如电商秒杀系统用
问题集锦-synchronized
synchronized 及其应用场景?
核心作用
- 通过内置锁机制确保多线程环境下共享资源的原子性和可见性。
- 基于对象监视器(Monitor)控制线程对同步代码的访问,同一时刻仅允许一个线程执行锁定代码。
三种使用方式
- 修饰实例方法
- 锁对象:当前实例(
this
)。 - 场景:保护实例变量的线程安全操作,如多线程操作同一对象的计数器。
- 锁对象:当前实例(
- 修饰静态方法
- 锁对象:类的
Class
对象(ClassName.class
)。 - 场景:保护静态变量的线程安全操作,如全局计数器或单例初始化。
- 锁对象:类的
- 修饰代码块
- 锁对象:可指定任意对象(如
this
、自定义锁)。 - 场景:缩小锁粒度提升性能,仅对关键代码段加锁。
- 锁对象:可指定任意对象(如
- 修饰实例方法
典型应用场景
- 共享资源的原子操作
- 需保证复合操作完整性的场景,如计数器增减、转账操作、库存扣减。
- 单例模式的双重检查锁定
- 延迟初始化且保证线程安全,如全局配置类、数据库连接池。
- 线程安全的数据结构
- 保护集合类的并发修改,如
Collections.synchronizedList
包装的ArrayList
。
- 保护集合类的并发修改,如
- 生产者-消费者模型
- 协调线程间的数据传递,如消息队列的任务生产和消费。
- 共享资源的原子操作
注意事项
- 锁粒度控制
- 避免全方法加锁,优先同步关键代码段以减少竞争。
- 死锁预防
- 统一多锁的获取顺序(如按对象哈希值排序),避免循环等待。
- 性能优化
- 减少锁持有时间,利用 JVM 锁升级机制(偏向锁→轻量级锁→重量级锁)。
- 异常处理
- 在
finally
块中释放锁,防止线程阻塞导致资源泄漏。
- 在
- 锁粒度控制
synchronized 是公平锁吗?
synchronized 的公平性本质
- 底层实现默认且固定为非公平锁,不保证线程按请求顺序获取锁。
- 新请求线程可能直接抢占锁,而非遵循队列顺序。
非公平锁的核心表现
- 随机竞争机制:锁释放时 JVM 不检查队列顺序,新线程与队列线程随机竞争。
- 潜在饥饿风险:高频短任务场景中可能出现线程长期无法获取锁。
设计选择的原因
- 性能优先:减少线程唤醒和上下文切换(每次 1-10 微秒),吞吐量可达公平锁的 5-8 倍。
- 实现简化:锁升级机制(偏向锁→轻量级锁→重量级锁)针对非公平场景优化。
与 ReentrantLock 的对比
- 灵活性差异:ReentrantLock 可配置公平/非公平模式,synchronized 仅支持非公平。
- 适用场景
- synchronized:锁持有时间短、竞争不激烈(如计数器递增)。
- ReentrantLock 公平模式:需严格顺序的场景(如银行转账)。
synchronized 锁静态方法和普通方法区别?
锁对象不同
- 普通方法:锁定当前实例对象(
this
),同一实例的同步普通方法互斥,不同实例方法可并行执行。 - 静态方法:锁定类的
Class
对象,所有实例调用同步静态方法均互斥,全局仅一个线程可执行。
- 普通方法:锁定当前实例对象(
作用范围差异
- 普通方法:仅对同一对象实例的同步方法互斥,不同实例的同名方法不受影响。
- 静态方法:同一时间任何实例的同步静态方法均不可并行执行。
锁的粒度
- 普通方法:锁粒度较细,影响单个实例操作,适合高频实例级资源保护。
- 静态方法:锁粒度较粗,全局锁定类级别资源,可能成为性能瓶颈。
适用场景
- 普通方法:保护实例变量(如对象属性修改),避免多线程数据竞争。
- 静态方法:保护静态变量或实现类级同步逻辑(如单例模式双重检查锁定)。
线程交互影响
- 普通方法:不同线程访问不同实例的同步普通方法可并发执行。
- 静态方法:同一时间仅一个线程能执行静态同步方法(如全局配置更新)。
锁类型独立性
- 互不干扰:类锁与对象锁可同时持有(如线程持有类锁后仍可获取对象锁)。
- 锁升级机制:JVM 对两种锁均可能进行偏向锁、轻量级锁、重量级锁优化升级。
synchronized 支持重入吗?如何实现的?
synchronized 支持重入
- 同一线程可多次获取同一对象的锁而不会阻塞,避免递归或嵌套调用导致的死锁。
实现原理
- 锁计数器与监视器机制
- 对象关联锁计数器(
status
)和持有线程 ID,首次获取计数器置 1,重入时递增,释放时递减,归零后完全释放。
- 对象关联锁计数器(
- 字节码指令支持
- 通过
monitorenter
和monitorexit
指令控制代码块进入/退出时的锁状态检查与计数器更新。
- 通过
- 锁升级机制
- 无竞争时使用偏向锁记录线程 ID,竞争升级为轻量级锁(CAS 自旋),激烈竞争时转为重量级锁(操作系统互斥锁)。
- 锁计数器与监视器机制
优势与局限
- 优势
- 自动管理锁释放,避免死锁风险。
- 支持递归调用和嵌套同步代码块,简化编程逻辑。
- 局限
- 高并发场景下频繁锁升级(如偏向锁→重量级锁)可能带来性能损耗。
- 优势
与显式锁的对比
- synchronized 的可重入性是隐式自动管理,而
ReentrantLock
需手动管理计数器。
- synchronized 的可重入性是隐式自动管理,而
syncronized 锁升级的过程讲一下
无锁状态
- 对象创建时无锁,Mark Word 无锁标记,所有线程可自由访问。
偏向锁阶段
- 触发条件:第一个线程首次获取锁时,JVM 将对象头 Mark Word 设为偏向锁状态(锁标志位 01),记录线程 ID。
- 优化目的:单线程重复获取锁时无需同步操作,直接通过线程 ID 比对进入同步代码块。
- 撤销机制:其他线程尝试获取锁时,JVM 暂停持有线程,若锁已释放则升级为轻量级锁。
轻量级锁阶段
- 触发条件:多线程低竞争时升级(锁标志位 00)。
- 实现方式
- 线程栈帧创建锁记录(Lock Record),拷贝对象头 Mark Word。
- 通过 CAS 替换对象头为指向锁记录的指针,成功则获取锁,失败则自旋等待。
- 自旋优化:自适应策略动态调整自旋次数以减少 CPU 空转。
重量级锁阶段
- 触发条件:自旋次数超限或竞争激烈时升级(锁标志位 10)。
- 实现机制
- 依赖操作系统互斥量(Mutex)实现阻塞与唤醒,未获锁线程进入等待队列。
- 涉及用户态到内核态切换,带来高上下文切换开销。
- 不可逆性:升级后无法降级,即使竞争减弱仍保持该状态。
锁升级的意义
- 性能优化:减少低竞争场景同步开销(如偏向锁单线程优化)。
- 平衡机制:轻量级锁减少阻塞,重量级锁保障高并发有序执行。
- Mark Word 动态变化:对象头存储不同元数据(线程 ID、锁记录指针等)。
注意高版本因为偏向锁性能收益低和实现过于复杂,已将偏向锁移除。
JVM 对 synchornized 的优化?
锁升级机制
- 轻量级锁
- 通过 CAS 操作和线程栈的锁记录实现非阻塞同步,首次获取时拷贝对象头 Mark Word 至锁记录,CAS 替换对象头指针。
- 自适应自旋策略动态调整自旋次数,减少 CPU 空转。
- 重量级锁
- 高竞争时升级为操作系统互斥锁(Mutex),依赖等待队列管理线程阻塞与唤醒。
- 用户态到内核态切换带来高开销,升级后不可逆。
- 轻量级锁
锁消除
- JIT 编译器通过逃逸分析检测同步对象是否仅限当前线程使用,未逃逸时消除锁操作。
- 典型场景:未逃逸的局部变量(如方法内的 StringBuffer)同步被优化为无锁形式。
锁粗化
- 合并相邻同步代码块减少加锁/解锁开销,如循环体内多次同步合并为单次同步。
- 优化原则:临界区执行时间需远小于线程调度时间片(通常控制在 1 毫秒内)。
自适应锁优化
- 动态选择锁策略:低竞争时自旋等待,高竞争时直接阻塞线程。
- 自旋次数阈值基于历史竞争数据动态调整,平衡 CPU 利用率与响应速度。
偏向锁替代方案
- 现代 JVM(JDK 15+)移除偏向锁,优先采用锁消除和轻量级锁优化。
- 遗留支持:通过
-XX:+UseBiasedLocking
强制启用(官方不推荐)。
其他优化策略
- Mark Word 复用:对象头动态存储线程 ID、锁记录指针等元数据。
- 可重入性:通过锁计数器实现同一线程多次获取锁,避免递归死锁。
- 锁粒度控制:低竞争用细粒度锁,高竞争改用显式锁(如 ReentrantLock)。
问题集锦-ReentrantLock
ReentrantLock 及其应用场景?
核心特性
- 可重入性:同一线程可多次获取同一锁,避免递归调用或嵌套同步块导致的死锁。
- 公平性选择:支持公平锁(按请求顺序分配)和非公平锁(默认模式),前者减少线程饥饿但吞吐量较低。
- 条件变量:通过
newCondition()
创建多个等待队列,实现细粒度线程协作(如生产者-消费者模型的不同唤醒条件)。 - 锁超时与中断响应:提供
tryLock(timeout, unit)
和lockInterruptibly()
,支持超时获取锁和中断等待。 - 锁状态监控:通过
getHoldCount()
查询锁重入次数,isLocked()
判断锁是否被占用。
应用场景
- 高竞争环境下的共享资源保护
- 多线程操作计数器、转账逻辑等需要原子性更新的场景,相比
synchronized
在高并发下性能更优。
- 多线程操作计数器、转账逻辑等需要原子性更新的场景,相比
- 需要公平调度的系统
- 任务调度需按提交顺序处理请求的场景(如金融交易系统)。
- 复杂线程协作模型
- 生产者-消费者模型中结合
Condition
实现满队列和空队列的精准唤醒。 - 阻塞队列通过
tryLock
控制入队/出队的超时逻辑。
- 生产者-消费者模型中结合
- 框架级并发控制
- 保护线程池状态(如
ThreadPoolExecutor
的RUNNING
状态)和任务队列的线程安全。 ConcurrentHashMap
的分段锁机制中使用 ReentrantLock 控制哈希桶的并发访问。
- 保护线程池状态(如
- 需要锁中断或超时响应的场景
- 分布式锁实现中通过
tryLock
避免线程因网络问题长期阻塞。 - 实时系统要求任务在指定时间内完成并执行降级策略。
- 分布式锁实现中通过
- 高竞争环境下的共享资源保护
注意事项
- 手动释放锁:必须在
finally
块中调用unlock()
,否则可能导致死锁或资源泄漏。 - 避免锁嵌套:支持可重入但过度嵌套会增加锁竞争复杂度。
- 公平锁的权衡:仅在严格顺序需求时使用,避免增加线程切换开销。
- 性能监控:通过
getQueueLength()
监控等待线程数,优化锁竞争热点。
- 手动释放锁:必须在
ReentrantLock 是怎么实现公平锁的?
公平锁的核心实现机制
- FIFO 队列顺序:通过
FairSync
类实现,线程调用lock()
时必须先检查 AQS 队列是否存在等待线程。- 队列为空或无前驱节点时尝试 CAS 获取锁。
- 队列存在等待线程时,当前线程加入队列尾部排队。
- FIFO 队列顺序:通过
关键方法
hasQueuedPredecessors()
- 在
tryAcquire()
中调用该方法检查队列状态,确保只有队列头部线程能获取锁,阻止新线程插队。
- 在
CLH 队列管理
- AQS 维护 CLH 双向链表队列,公平锁通过
enq()
将竞争失败的线程封装为 Node 节点加入队列尾部。 - 锁释放时唤醒队列头部节点的线程,保证顺序执行。
- AQS 维护 CLH 双向链表队列,公平锁通过
与非公平锁的差异
- 初始抢占逻辑
- 公平锁:必须检查队列状态后才尝试 CAS。
- 非公平锁:直接 CAS 抢占,不检查队列。
- 性能表现
- 公平锁吞吐量较低(约下降 5-8 倍),适合顺序敏感场景。
- 非公平锁适合短任务竞争,吞吐量更高。
- 线程饥饿风险
- 公平锁无饥饿问题,非公平锁可能引发线程长期等待。
- 初始抢占逻辑
适用场景与代价
- 适用场景:支付系统、任务调度等需严格顺序的场景。
- 实现代价
- 每次锁获取需队列检查(增加 20-50 纳秒开销)。
- 频繁线程切换导致性能下降(对比非公平锁吞吐量降低 60%-80%)。
问题集锦-同步工具
CountDownLatch 是做什么的讲一讲?
核心功能
- 允许一个或多个线程等待其他线程完成特定操作后再继续执行。
- 内部维护计数器,初始化时指定等待的线程数量,
countDown()
减少计数,计数器归零时唤醒等待线程。
工作原理
- 初始化计数器:
new CountDownLatch(n)
设置初始值n
(需等待的任务数)。 - 任务完成通知:子线程调用
countDown()
安全减少计数(基于 CAS 实现)。 - 阻塞与唤醒:主线程
await()
阻塞至计数器归零,支持超时await(timeout, unit)
。
- 初始化计数器:
典型应用场景
- 主线程等待子任务完成:服务启动时等待所有组件初始化完毕。
CountDownLatch latch = new CountDownLatch(3); // 启动 3 个子线程初始化服务并调用 latch.countDown() latch.await(); // 阻塞至所有服务就绪
- 多线程结果汇总:并行计算后主线程汇总结果(如分片数据处理)。
- 并发测试同步:确保所有并发任务执行完毕后再验证结果。
- 资源协调:学生全部离场后通知家长,顾客到齐后服务员上菜。
- 主线程等待子任务完成:服务启动时等待所有组件初始化完毕。
注意事项
- 一次性使用:计数器归零后无法重置,需重新创建。
- 异常处理:子线程在
finally
中调用countDown()
避免主线程永久阻塞。 - 超时机制:建议设置
await()
超时防止系统僵死。 - 精确计数:初始值与实际任务数严格匹配(过多导致阻塞,过少提前唤醒)。
问题集锦-AQS
介绍一下 AQS
AQS 的基本概念与核心功能
- 定义:Java 并发包(JUC)的核心框架,用于构建锁和同步器(如 ReentrantLock、Semaphore、CountDownLatch)。
- 核心功能:
- 通过
volatile int state
管理资源状态,支持原子操作(getState()
/setState()
/CAS)。 - 基于 CLH 队列实现线程排队与唤醒,通过 LockSupport 实现线程阻塞(
park()
)与唤醒(unpark()
)。
- 通过
底层实现原理
- 同步队列(CLH 队列)
- 结构:双向链表,节点(Node)包含线程、等待状态(
waitStatus
)及前后指针。 - 作用:存储未获取资源的线程,实现 FIFO 公平调度,头节点(
head
)表示当前持有资源的线程。
- 结构:双向链表,节点(Node)包含线程、等待状态(
- 条件队列
- 单向链表,用于
Condition
的等待/通知机制(如await()
和signal()
)。
- 单向链表,用于
- 同步队列(CLH 队列)
资源共享模式
- 独占模式
- 同一时刻仅一个线程访问资源(如 ReentrantLock),通过
tryAcquire()
/tryRelease()
实现。 - 可重入性:
state
记录同一线程多次获取锁的计数。
- 同一时刻仅一个线程访问资源(如 ReentrantLock),通过
- 共享模式
- 允许多线程同时访问资源(如 Semaphore、CountDownLatch),通过
tryAcquireShared()
/tryReleaseShared()
实现。
- 允许多线程同时访问资源(如 Semaphore、CountDownLatch),通过
- 独占模式
典型应用场景
- 锁实现:ReentrantLock 重写
tryAcquire()
实现公平/非公平锁。 - 同步器:
- CountDownLatch:
state
初始化为计数值,countDown()
递减,归零时唤醒所有等待线程。 - Semaphore:通过
state
控制并发线程数,acquire()
/release()
操作许可。
- CountDownLatch:
- 锁实现:ReentrantLock 重写
优势与设计特点
- 模板方法模式:子类仅需实现
tryAcquire()
/tryRelease()
等模板方法,AQS 自动处理队列管理与调度。 - 性能优化:
- 双向链表支持快速删除节点(如线程超时或中断时)。
- 自旋优化减少上下文切换开销。
- 可扩展性:通过组合 AQS 实现复杂同步策略(如读写锁分离)。
- 模板方法模式:子类仅需实现
如何用 AQS 实现一个可重入的公平锁?
核心实现原理
- 公平性保证
- 通过 AQS 的 CLH 队列实现线程 FIFO 排队,在
tryAcquire()
中调用hasQueuedPredecessors()
检查队列是否有等待线程。
- 通过 AQS 的 CLH 队列实现线程 FIFO 排队,在
- 可重入性
- 通过
state
变量记录锁持有次数,同一线程多次获取时递增state
,释放时递减至归零。
- 通过
- 公平性保证
关键实现步骤
- 定义 Sync 类继承 AQS
- 创建
ReentrantFairLock
类,内部定义Sync
类继承AbstractQueuedSynchronizer
。
- 创建
- 重写 tryAcquire 方法
- 检查
state == 0
:若无等待线程且 CAS 设置state
成功,获取锁并设置独占线程。 - 若当前线程是持有者,
state += acquires
实现可重入。
- 检查
- 重写 tryRelease 方法
- 递减
state
,归零时清空独占线程标识。
- 递减
- 定义 Sync 类继承 AQS
公平锁与非公平锁区别
- 公平锁:强制检查队列状态,仅无等待线程时尝试获取锁。
- 非公平锁:允许直接 CAS 抢占锁,无需检查队列。
锁操作流程
- 加锁
lock()
调用失败时,线程进入 CLH 队列并阻塞,等待前驱节点唤醒。
- 解锁
unlock()
释放锁后,唤醒队列中下一个线程。
- 加锁
性能与注意事项
- 公平性代价:获取锁需遍历队列,吞吐量低于非公平锁。
- 重入限制:
lock()
与unlock()
次数需严格匹配,否则导致死锁或状态异常。
问题集锦-悲观锁、乐观锁
悲观锁和乐观锁的区别?
核心思想差异
- 悲观锁
- 假设并发冲突必然发生,操作数据前必须先加锁(如数据库行锁、表锁),确保独占访问。
- 乐观锁
- 假设并发冲突极少发生,仅在数据更新时检测是否被修改(通过版本号或 CAS),允许无锁并发读取。
- 悲观锁
实现机制对比
- 悲观锁
- 加锁时机:操作数据前加锁(如
SELECT FOR UPDATE
)。 - 典型实现:数据库行锁、Java
synchronized
。 - 冲突处理:通过阻塞其他线程避免冲突。
- 数据一致性:强一致性(全程独占)。
- 加锁时机:操作数据前加锁(如
- 乐观锁
- 加锁时机:仅在更新时检测冲突(无前置锁)。
- 典型实现:版本号机制、CAS 算法(如
AtomicInteger
)。 - 冲突处理:检测到冲突后回滚或重试。
- 数据一致性:最终一致性(允许临时中间状态)。
- 悲观锁
性能与适用场景
- 悲观锁适用场景
- 高并发写操作(如金融转账、库存扣减)。
- 需严格避免脏读、不可重复读的场景。
- 缺点:锁竞争导致线程阻塞,吞吐量低。
- 乐观锁适用场景
- 高并发读操作(如新闻浏览、社交点赞)。
- 冲突概率低的场景(如用户信息更新)。
- 缺点:频繁冲突时重试开销大(需配合重试策略)。
- 悲观锁适用场景
典型问题与解决方案
- 悲观锁死锁
- 解决方案:锁超时机制(如 MySQL 的
innodb_lock_wait_timeout
)、死锁检测算法。
- 解决方案:锁超时机制(如 MySQL 的
- 乐观锁 ABA 问题
- 解决方案:带版本号的 CAS(如
AtomicStampedReference
)。
- 解决方案:带版本号的 CAS(如
- 悲观锁死锁
混合策略实践
- 分段锁(如
ConcurrentHashMap
):将全局锁拆分为多个细粒度锁,平衡性能与一致性。 - 自适应锁(如 Java 锁升级):根据竞争动态切换悲观/乐观策略。
- 分段锁(如
Java 中想实现一个乐观锁,都有哪些方式?
基于版本号机制
- 数据对象增加版本号字段(如
version
),更新时校验当前版本号是否一致,一致则递增版本并更新,否则重试或回滚。 - Java 实现:通过
AtomicInteger
管理版本号字段,结合业务逻辑校验。
- 数据对象增加版本号字段(如
基于时间戳机制
- 使用时间戳字段替代版本号,更新时校验时间戳是否一致。
- 注意点:需处理分布式系统时钟同步问题,避免因时间误差导致冲突。
CAS 原子操作
- 使用
AtomicInteger
、AtomicLong
等原子类,依赖 CPU 的cmpxchg
指令实现原子性比较与交换。 - 扩展方案
- 解决 ABA 问题:使用
AtomicStampedReference
附加版本号或时间戳。
- 解决 ABA 问题:使用
- 使用
JPA/Hibernate 的 @Version 注解
- 在实体类中添加
@Version
注解字段,框架自动管理版本号校验,简化数据库乐观锁实现。
- 在实体类中添加
Redis 分布式乐观锁
- 利用 Redis 的
SETNX(setIfAbsent)
实现跨进程锁,结合超时机制避免死锁。 - 典型场景:分布式系统数据同步、秒杀库存控制。
- 利用 Redis 的
适用场景建议
- 低冲突场景:优先使用版本号或 CAS 减少锁开销。
- 高并发写操作:结合数据库版本号机制确保强一致性。
- 分布式系统:采用 Redis 锁实现跨服务同步。
- ORM 框架集成:直接使用
@Version
简化开发。
注意事项
- ABA 问题:优先使用
AtomicStampedReference
替代基础 CAS。 - 自旋开销:设置最大重试次数,避免 CPU 资源浪费。
- 性能权衡:高冲突场景评估乐观锁重试成本与悲观锁阻塞开销。
CAS 有什么缺点?
ABA 问题
- 问题描述:CAS 仅检测值是否变化,无法感知中间状态变化(如 A→B→A)。
- 解决方案:引入版本号机制(如
AtomicStampedReference
类),同时比对值和版本号。
自旋开销大
- 问题描述:高并发场景下,CAS 失败后循环重试导致 CPU 资源浪费。
- 解决方案
- 设置最大自旋次数或退避策略(如
Thread.yield()
)。 - 高竞争场景改用锁机制(如
synchronized
)。
- 设置最大自旋次数或退避策略(如
仅支持单变量原子性
- 问题描述:无法原子操作多个共享变量,可能导致中间状态不一致。
- 解决方案
- 封装多个变量为对象,通过
AtomicReference
管理。 - 使用锁机制或事务内存(如 JDK9+ 的
VarHandle
)。
- 封装多个变量为对象,通过
高竞争性能下降
- 问题描述:线程竞争激烈时,CAS 成功率低导致吞吐量下降。
- 解决方案:改用分段计数机制(如
LongAdder
替代AtomicLong
)。
其他潜在问题
- 优先级反转:高优先级线程自旋等待低优先级线程释放资源。
- 硬件依赖:部分系统需依赖锁或其他同步机制。
- 代码复杂性:无锁数据结构实现需处理边界条件,开发难度高。
为什么不能所有的锁都用 CAS?
高竞争环境性能瓶颈
- 自旋开销大:CAS 失败后持续重试导致 CPU 资源浪费,高并发场景(如秒杀系统)下传统锁的阻塞机制更高效。
- 适应性不足:CAS 无法动态调整等待策略,传统锁(如
ReentrantLock
)可结合公平锁优化线程调度。
功能局限性
- 仅支持单变量原子操作:无法处理多变量或复合操作(如转账需同时修改两个账户余额)。
- 缺乏高级同步支持:无法实现条件变量(
Condition
)、读写锁分离等功能。
ABA 问题与维护成本
- 值变化不可感知:A→B→A 的变化导致逻辑错误(如无锁栈顶元素多次弹出后重新压入)。
- 解决方案复杂:需引入版本号(
AtomicStampedReference
)或时间戳,增加代码复杂度。
线程公平性与资源分配
- 饥饿风险:CAS 不保证线程排队顺序,公平锁通过 CLH 队列强制 FIFO 避免线程饿死。
- 优先级反转:高优先级线程可能因自旋等待低优先级线程被阻塞。
硬件与场景依赖
- 硬件限制:部分嵌入式系统缺乏 CAS 指令支持,需依赖锁机制。
- 适用场景局限:长耗时操作(如数据库事务)用锁更合适,CAS 适合短临界区、低冲突场景。
替代方案与混合策略
- 优先 CAS 场景:简单原子操作(如计数器自增)、低冲突环境(状态标志位更新)。
- 改用锁机制场景:复杂业务逻辑(多变量事务)、需公平性保障(任务队列调度)。
- 混合策略:分段计数(
LongAdder
)、乐观读模式(StampedLock
)。
CAS 有什么问题,Java 是怎么解决的?
ABA 问题
- 问题:无法感知中间状态变化(如 A→B→A)。
- Java 解决方案:通过
AtomicStampedReference
引入版本号,同时比对值和版本号。
自旋开销大
- 问题:高并发下循环重试导致 CPU 浪费。
- Java 解决方案
- 分段 CAS 机制:如
LongAdder
拆分计数器减少竞争。 - 退避策略:结合
Thread.yield()
或限制自旋次数。
- 分段 CAS 机制:如
仅支持单变量原子操作
- 问题:无法原子操作多变量导致中间状态不一致。
- Java 解决方案
- 封装复合状态:使用
AtomicReference
管理对象。 - 事务内存:JDK9+ 的
VarHandle
支持多变量原子操作。
- 封装复合状态:使用
高竞争性能下降
- 问题:线程竞争激烈时吞吐量降低。
- Java 解决方案
- 切换锁机制:如
synchronized
或ReentrantLock
。 - 混合策略:AQS 框架结合 CAS 与队列机制。
- 切换锁机制:如
硬件依赖与代码复杂性
- 问题:部分系统缺乏 CAS 指令支持,无锁数据结构开发复杂。
- Java 解决方案
- 内置原子类:如
AtomicInteger
屏蔽底层差异。 - 工具类封装:提供
ConcurrentLinkedQueue
等线程安全结构。
- 内置原子类:如
问题集锦-voliatle
voliatle 关键字有什么作用?
保证内存可见性
- 强制每次读写操作直接访问主内存,确保多线程间修改即时可见。
- 示例:线程 A 修改
volatile
变量后,线程 B 能立即读取最新值,避免缓存不一致问题。
禁止指令重排序
- 通过内存屏障防止编译器和处理器对指令进行重排序优化。
- 应用:单例模式中避免获取未完全初始化的对象。
适用场景
- 状态标志:标记线程状态(如任务完成、中断标志),确保状态变更可见。
- 硬件寄存器访问:嵌入式开发中读取可能被硬件修改的寄存器值,防止读取旧值。
- 轻量级同步:读多写少且无需原子性保证的场景(如计数器标记)。
局限性
- 不保证原子性:单次读/写原子性,复合操作(如
i++
)需依赖synchronized
或原子类。 - 性能开销:频繁读写时强制刷新主内存会增加性能损耗。
- 不保证原子性:单次读/写原子性,复合操作(如
语言差异
- Java:通过内存屏障实现可见性和有序性,与
synchronized
和原子类配合使用。 - C/C++:仅禁止编译器优化,需结合锁或原子操作实现线程安全。
- Java:通过内存屏障实现可见性和有序性,与
volatile 可以保证线程安全吗?
volatile 的线程安全保证范围
- 可见性保证:变量修改后立即刷新到主内存,其他线程读取时强制获取最新值(如状态标志变更避免死循环问题)。
- 有序性保证:通过内存屏障禁止指令重排序,确保代码执行顺序与编写顺序一致(如单例模式避免获取未初始化对象)。
volatile 的局限性
- 不保证原子性:复合操作(如
i++
)即使修饰为volatile
仍可能因线程切换导致数据不一致。 - 适用场景限制:仅适用于读多写少且操作简单的场景(如状态标志、硬件寄存器访问)。
- 不保证原子性:复合操作(如
正确使用 volatile 的场景
- 状态标志控制:如
volatile boolean flag
控制任务启停,保证状态变更可见性。 - 双重检查锁定单例模式:结合
volatile
和synchronized
避免指令重排序引发的初始化问题。 - 轻量级同步:读多写少场景下减少锁竞争开销。
- 状态标志控制:如
线程安全的完整实现方案
- 原子类(如
AtomicInteger
):通过CAS + volatile
组合保证原子性和可见性(适用于计数器等场景)。 - synchronized 或显式锁:对复合操作(如转账、库存扣减)提供原子性保证,但引入锁竞争开销。
- 原子类(如
juc 包下你常用的类?
并发集合类
ConcurrentHashMap
:分段锁实现的线程安全哈希表,适用于高并发读写(如缓存)。CopyOnWriteArrayList
:写时复制列表,读多写少场景高效(如监听器列表)。
同步工具类
CountDownLatch
:倒计时门闩,主线程等待多个子任务完成(如系统初始化)。CyclicBarrier
:可重用屏障,多线程同步至同一阶段(如分批次数据处理)。Semaphore
:控制并发资源访问数(如连接池限流)。
执行框架类
ThreadPoolExecutor
:灵活配置线程池参数(核心线程数、队列策略等)。CompletableFuture
:异步编程工具,支持链式调用和组合任务(如并行计算编排)。
原子操作类
AtomicInteger
:基于 CAS 的无锁计数器,高性能并发计数(如请求量统计)。AtomicReference
:原子更新对象引用(如状态标志的无锁更新)。
锁与条件类
ReentrantLock
:可中断、超时的可重入锁,替代synchronized
。StampedLock
:乐观读锁,减少读写锁竞争(如读多写少的场景优化)。Condition
:细粒度条件等待/唤醒,支持多条件队列(如生产者-消费者模型)。
并发队列类
LinkedBlockingQueue
:无界或容量可选阻塞队列(如任务缓冲队列)。SynchronousQueue
:直接传递任务的队列(如线程池的立即匹配策略)。
Java 中有哪些常用的锁,在什么场景下使用?
互斥锁
synchronized
- 特点:JVM 内置锁,自动管理锁的获取与释放,支持可重入。
- 场景:简单临界区保护(如计数器操作)、单例模式的双重检查锁。
ReentrantLock
- 特点:支持可中断锁、公平锁、条件变量,需手动释放锁。
- 场景:复杂同步需求(如超时控制、多条件判断)。
读写锁
ReentrantReadWriteLock
- 特点:读操作共享,写操作独占,提升读多写少场景性能。
- 场景:缓存系统、配置文件读取、共享文档操作。
无锁机制
- 原子类(
AtomicInteger
等)- 特点:基于 CAS 实现无锁线程安全操作。
- 场景:高并发计数(如请求量统计、状态标志更新)。
volatile
- 特点:保证变量可见性,需结合锁保证复合操作原子性。
- 场景:简单状态标志(如线程启停控制)。
- 原子类(
乐观锁与悲观锁
- 乐观锁
- 实现:CAS、版本号机制(如数据库版本字段)。
- 场景:读多写少场景(如商品库存、点赞计数)。
- 悲观锁
- 实现:
synchronized
、ReentrantLock
、数据库SELECT FOR UPDATE
。 - 场景:写操作频繁场景(如金融交易、库存扣减)。
- 实现:
- 乐观锁
公平锁与非公平锁
- 公平锁
- 特点:按请求顺序分配锁,避免线程饥饿。
- 场景:任务调度需严格顺序(如转账操作、数据库连接池)。
- 非公平锁
- 特点:允许线程插队,吞吐量更高。
- 场景:高并发读操作(如缓存读取、秒杀系统)。
- 公平锁
分段锁与锁优化
ConcurrentHashMap
的分段锁- 特点:锁粒度细化到数据段,减少锁竞争。
- 场景:高并发哈希表操作。
- 锁粗化与锁消除
- 特点:JVM 自动优化减少锁开销。
- 场景:循环内重复加锁、明显无竞争的同步代码。
怎么在实践中用锁的?
互斥锁的实践要点
synchronized
关键字- 临界区保护:通过同步方法或代码块保护共享资源(如计数器操作)。
- 锁对象选择:指定特定对象作为锁缩小粒度(如
synchronized (lockObject)
)。 - 单例模式:双重检查锁定需结合
volatile
避免指令重排序。
ReentrantLock
高级控制- 可中断锁:
lockInterruptibly()
避免永久阻塞(如服务端请求处理)。 - 超时机制:
tryLock(1, TimeUnit.SECONDS)
防止死锁(分布式锁场景)。 - 条件变量:
newCondition()
实现线程协作(生产者-消费者模型)。
- 可中断锁:
读写分离场景实践
ReentrantReadWriteLock
- 缓存系统:读锁允许多线程并发读,写锁保证数据更新原子性。
- 配置热更新:读锁读取配置,写锁更新配置实现动态加载。
StampedLock
性能优化- 乐观读模式:
tryOptimisticRead()
提升读多写少场景吞吐量(如金融行情读取)。 - 三维空间计算:保护多个关联变量原子更新(如坐标 x,y,z)。
- 乐观读模式:
线程安全工具类应用
- 原子类与无锁编程
- 计数器场景:
AtomicInteger
替代同步锁提升性能 5-10 倍。 - 状态标志:
volatile
修饰简单状态变量(需注意复合操作加锁)。
- 计数器场景:
- 阻塞队列实践
- 生产者-消费者模型:
BlockingQueue
自动处理阻塞与唤醒(如LinkedBlockingQueue
)。
- 生产者-消费者模型:
- 原子类与无锁编程
锁使用最佳实践
- 死锁预防
- 统一加锁顺序:按锁对象 hashCode 排序。
- 资源限时获取:
tryLock
设置超时(推荐 100ms-1s)。
- 性能优化技巧
- 锁粒度控制:拆分全局锁为分段锁(如
ConcurrentHashMap
桶锁)。 - 锁消除与粗化:依赖 JVM 自动优化循环内同步代码。
- 锁粒度控制:拆分全局锁为分段锁(如
- 资源管理规范
- try-finally 范式:确保任何路径释放锁(如
finally { lock.unlock() }
)。 - AutoCloseable 集成:支持 try-with-resources 自动释放锁。
- try-finally 范式:确保任何路径释放锁(如
- 死锁预防
典型场景锁选型参考
- 简单临界区:
synchronized
(单例模式、计数器)。 - 超时/中断控制:
ReentrantLock
(分布式任务调度)。 - 读多写少(>10:1):
ReentrantReadWriteLock
(文档协作系统)。 - 极高读取并发:
StampedLock
(实时行情推送)。 - 无数据竞争:原子类(请求统计、ID 生成器)。
- 线程协作:
Condition + ReentrantLock
(订单状态机)。
- 简单临界区:
除了用 synchronized,还有什么方法可以实现线程同步?
显式锁(ReentrantLock)
- 手动控制锁的获取与释放(
lock()
/unlock()
),支持公平锁和非公平锁模式。 - 特性:
- 超时获取(
tryLock
)和可中断锁(lockInterruptibly
)。 - 多条件变量(
Condition
)实现精准线程唤醒。
- 超时获取(
- 场景:高并发资源保护(如转账逻辑)、公平调度系统。
- 手动控制锁的获取与释放(
原子操作类(Atomic 类)
- 使用
AtomicInteger
、AtomicReference
等通过 CAS 保证单变量原子性。 - 场景:简单计数器、状态标志(如
volatile + AtomicBoolean
)。
- 使用
信号量(Semaphore)
- 通过计数器控制资源并发访问数(
acquire()
/release()
)。 - 场景:数据库连接池限制、并发下载流量控制。
- 通过计数器控制资源并发访问数(
读写锁(ReentrantReadWriteLock)
- 读锁共享,写锁独占。
- 场景:读多写少的配置管理、缓存系统热更新。
线程本地存储(ThreadLocal)
- 为每个线程创建变量副本,避免共享变量同步问题。
- 场景:数据库连接管理、线程不安全工具类(如日期格式化)。
条件变量(Condition)
- 配合锁实现精准唤醒(
await()
/signal()
)。 - 场景:生产者-消费者模型中的满/空队列分离控制。
- 配合锁实现精准唤醒(
volatile 关键字
- 保证变量可见性和禁止指令重排序。
- 场景:状态标志(任务终止标记)、双重检查锁定单例模式。
阻塞队列(BlockingQueue)
- 内置线程安全机制,自动处理阻塞与唤醒(
put()
/take()
)。 - 实现:
ArrayBlockingQueue
(任务调度)、LinkedBlockingQueue
(日志异步处理)。
- 内置线程安全机制,自动处理阻塞与唤醒(
synchronized 和 Reentrantlock 区别?
实现机制
- synchronized:基于 JVM 的监视器锁(Monitor),通过对象头管理锁状态,自动处理加锁与释放。
- ReentrantLock:基于 JDK 的
Lock
接口,依赖 AQS 队列和 CAS 操作,需显式调用lock()
/unlock()
。
功能特性
- 锁类型
- synchronized:仅非公平锁,线程获取顺序随机。
- ReentrantLock:支持公平锁(按请求顺序)和非公平锁(默认)。
- 中断与超时
- ReentrantLock:支持
lockInterruptibly()
响应中断,tryLock(timeout)
超时机制。 - synchronized:线程阻塞后无法中断,可能导致死锁。
- ReentrantLock:支持
- 条件变量
- ReentrantLock:通过
newCondition()
创建多个Condition
实现精准唤醒(如生产者-消费者模型)。 - synchronized:仅支持
wait()
/notify()
单路通知。
- ReentrantLock:通过
- 锁类型
性能与优化
- 低竞争场景:synchronized 因偏向锁、轻量级锁优化性能接近 ReentrantLock。
- 高并发场景:ReentrantLock 非公平锁通过 CAS 减少线程切换,性能更优;synchronized 升级为重量级锁后开销大。
使用方式
- synchronized:语法简洁,自动释放锁,适用于方法或代码块同步。
- ReentrantLock:需显式调用
lock()
/unlock()
,支持tryLock()
和超时机制,结合try-finally
确保释放。
适用场景
- synchronized:简单同步需求(实例变量保护)、低竞争场景、快速开发。
- ReentrantLock:复杂同步逻辑(公平锁、中断响应)、高并发优化、多条件协作(任务调度优先级控制)。
锁的释放与可重入性
- synchronized:可重入且自动释放锁,JVM 在同步代码结束时解锁。
- ReentrantLock:可重入但需手动释放,未正确
unlock()
可能导致死锁。
注意事项
- 锁粒度:synchronized 静态方法锁定类对象可能影响性能,ReentrantLock 可通过细粒度条件优化。
- 死锁预防:ReentrantLock 的
tryLock()
和超时机制减少死锁概率,synchronized 需避免嵌套锁。 - 锁升级机制:synchronized 从偏向锁逐步升级为重量级锁,ReentrantLock 优化需开发者控制。
CAS 和 AQS 有什么关系?
底层实现的依赖关系
- CAS 是 AQS 的原子操作基础
- AQS 通过
compareAndSetState()
方法实现state
的原子更新(如获取锁时从 0 改为 1)。 - CLH 队列的节点插入与删除(如
enq()
方法)依赖 CAS 保证线程安全。
- AQS 通过
- CAS 是 AQS 的原子操作基础
协同应用的场景
- 锁的公平性与非公平性实现
- 非公平锁:直接通过 CAS 抢占
state
(如ReentrantLock
默认模式)。 - 公平锁:在
tryAcquire()
中先检查队列状态,再通过 CAS 修改state
。
- 非公平锁:直接通过 CAS 抢占
- 资源释放与唤醒机制
- AQS 的
release()
方法通过 CAS 重置state
,并唤醒队列中的下一个线程。
- AQS 的
- 锁的公平性与非公平性实现
功能定位的区别
- CAS 的轻量级特性
- 单变量原子操作(如计数器增减),无需锁机制,适用于低竞争场景。
- AQS 的同步框架特性
- 管理多线程竞争资源的排队、阻塞与唤醒(如
Semaphore
控制并发数)。 - 支持复杂同步逻辑(如读写锁分离、条件变量
Condition
)。
- 管理多线程竞争资源的排队、阻塞与唤醒(如
- CAS 的轻量级特性
性能优化的互补性
- CAS 的局限性
- ABA 问题通过
AtomicStampedReference
解决。 - 高竞争下自旋开销由 AQS 队列阻塞机制缓解。
- ABA 问题通过
- AQS 的扩展性
- 结合 CAS 与队列机制平衡吞吐量(如非公平锁优先 CAS 抢占)。
- 支持可重入性(通过
state
计数器记录锁持有次数)。
- CAS 的局限性
Threadlocal 作用,原理,具体里面存的 key value 是啥,会有什么问题,如何解决?
作用
- 线程数据隔离:为每个线程创建独立变量副本,避免多线程共享数据同步开销。
- 上下文管理:存储线程级上下文信息(如用户会话、数据库连接),简化参数传递。
原理
- ThreadLocalMap 结构:每个线程维护一个
ThreadLocalMap
,以ThreadLocal
对象为键(弱引用),用户数据为值(强引用)。 - 操作机制:通过
set()
/get()
读写当前线程的ThreadLocalMap
,remove()
清理数据。 - 初始化:使用
withInitial()
设置默认值,首次get()
触发初始化。
- ThreadLocalMap 结构:每个线程维护一个
存储的 key-value
- Key:
ThreadLocal
实例(弱引用),通过threadLocalHashCode
唯一标识。 - Value:线程本地数据(强引用),如计数器、会话对象。
- Key:
潜在问题
- 内存泄漏
- 原因:
Entry
的 key 被 GC 回收后,value 仍占用内存(尤其线程池复用线程时)。 - 场景:未调用
remove()
导致残留数据长期存在。
- 原因:
- 数据污染:线程池复用线程时,残留数据被后续任务误用。
- 性能开销:哈希冲突使用线性探测法,高并发效率降低。
- 内存泄漏
解决方案
- 及时清理:在
try-finally
中调用remove()
确保资源释放。 - 弱引用优化:用
static final
修饰ThreadLocal
实例,减少 key 回收风险。 - 避免大对象:优先存储轻量级数据,大型对象改用外部缓存。
- 数据隔离检查:线程池任务执行前显式清理
ThreadLocal
数据。
- 及时清理:在
指令重排序的原理是什么?
编译器优化重排序
- 消除冗余:删除重复计算或无效指令(如未使用的变量赋值)。
- 指令调度:将无数据依赖的指令交错排列以填充 CPU 流水线。
- 循环展开:复制循环体减少分支预测开销。
处理器动态重排序
- 乱序执行(OoOE)
- 使用重排序缓冲区(ROB)存储未提交指令,通过保留站监控操作数可用性。
- 内存系统优化
- 写缓冲区(Write Buffer)延迟内存写入,合并多次写操作。
- 缓存 Bank 划分允许并行访问不同存储单元。
- 乱序执行(OoOE)
重排序的约束条件
- 数据依赖性限制
- 真数据依赖(RAW)不可重排,反依赖(WAR)和输出依赖(WAW)可通过寄存器重命名解除。
- 控制依赖性限制
- 分支预测允许提前执行推测路径指令(预测失败则丢弃结果)。
- 数据依赖性限制
重排序的动机与效果
- 性能提升机制
- 减少流水线停顿,隐藏内存延迟,提高缓存命中率。
- 典型优化效果
- 提升流水线指令吞吐量,通过寄存器重命名实现多指令并行。
- 性能提升机制
重排序引发的问题与解决方案
- 多线程可见性问题
- 写缓冲区延迟导致读取旧值:使用内存屏障指令(如 x86 的
MFENCE
)强制刷新。
- 写缓冲区延迟导致读取旧值:使用内存屏障指令(如 x86 的
- 有序性破坏
- 对象未初始化被使用:通过
volatile
关键字插入 LoadLoad/StoreStore 屏障。
- 对象未初始化被使用:通过
- 多线程可见性问题
volatile 和 sychronized 比较?
可见性保证
- volatile:通过内存屏障强制将修改值刷新到主内存,禁止指令重排序(LoadLoad/StoreStore 屏障)。
- synchronized:锁机制强制同步块内变量从主内存读取/刷新。
原子性差异
- volatile:仅保证单次读/写原子性(如 boolean 赋值),不支持复合操作(如
i++
)。 - synchronized:通过独占锁保证代码块内所有操作的原子性。
- volatile:仅保证单次读/写原子性(如 boolean 赋值),不支持复合操作(如
作用范围与实现机制
- volatile:仅修饰变量,基于 JVM 内存模型实现无锁同步。
- synchronized:可修饰方法或代码块,基于对象监视器锁(Monitor)实现互斥访问。
性能与锁机制
- volatile:无锁机制,无线程阻塞,适合高频读操作。
- synchronized:涉及锁获取/释放,可能触发线程阻塞,但 JVM 优化后性能提升(偏向锁、轻量级锁)。
适用场景
- volatile:单例模式 DCL、状态标志(如
isRunning
)、硬件寄存器访问。 - synchronized:需要原子性的复合操作(如计数器递增)、临界区资源保护(如文件读写)。
- volatile:单例模式 DCL、状态标志(如
有序性处理
- volatile:通过内存屏障禁止指令重排序,保证操作顺序与代码一致。
- synchronized:隐式通过锁保证有序性,无法主动禁止重排序。
可重入性与灵活性
- volatile:不支持重入,仅适用于简单变量同步。
- synchronized:支持可重入锁(如递归调用)。
线程池
线程池工作队列满了有哪些拒接策略?
AbortPolicy (中止策略)
- 行为:直接抛出
RejectedExecutionException
异常中断任务提交。 - 适用场景:对任务完整性要求极高的场景(如金融交易),需配合异常捕获机制。
- 风险:未捕获异常可能导致程序中断。
- 行为:直接抛出
CallerRunsPolicy (调用者运行策略)
- 行为:将任务回退到提交任务的线程(如主线程)直接执行。
- 适用场景:生产速度远高于消费速度时自然限流,允许短暂阻塞主线程的非实时任务。
- 注意事项:避免关键线程(如 UI 主线程)被阻塞。
DiscardPolicy (静默丢弃策略)
- 行为:无提示丢弃新任务,不抛异常也不执行。
- 适用场景:允许任务丢失的非关键场景(如实时监控采样)。
- 风险:需监控丢弃任务量,避免数据不一致。
DiscardOldestPolicy (弃老策略)
- 行为:丢弃队列中最旧任务,重试提交当前新任务。
- 适用场景:新任务优先级高于旧任务(如实时消息覆盖历史数据)。
- 风险:可能丢弃重要旧任务,需评估业务容忍度。
自定义拒绝策略
- 实现方式:实现
RejectedExecutionHandler
接口扩展逻辑。 - 典型方案:
- 持久化存储:任务存入数据库/Redis 后续处理。
- 异步重试:延迟后重新提交或转移至备用线程池。
- 告警通知:触发监控并记录任务详情。
- 适用场景:需保障任务最终执行的业务(如订单支付)。
- 实现方式:实现
策略选择原则
- 任务重要性:关键任务优先用
CallerRunsPolicy
或自定义策略。 - 系统容忍度:允许丢失用
DiscardPolicy
,需时效性用DiscardOldestPolicy
。 - 资源限制:内存敏感场景避免无界队列,明确队列容量。
- 监控配套:静默丢弃策略需加强队列长度和拒绝次数监控。
- 任务重要性:关键任务优先用
核心线程数设置为 0 可不可以?
可以设置但需谨慎权衡
- 技术上允许将核心线程数设为 0,线程池通过非核心线程处理任务。
- 任务提交逻辑:
- 当前工作线程为 0 时创建非核心线程执行任务。
- 已有非核心线程且队列未满时,任务入队等待。
- 适用场景:
- 极低频非关键任务(如日志清理),避免常驻线程占用资源。
- 突发流量后需快速释放资源的临时任务(如活动促销统计)。
潜在风险与限制
- 线程无法复用:每次任务可能触发新建线程,增加创建/销毁开销。
- 队列容量敏感
- 无界队列(如
LinkedBlockingQueue
)可能导致 OOM。 - 有界队列需精确设置容量防止任务堆积。
- 无界队列(如
- 响应延迟:首次任务需等待线程创建,影响实时性要求高的场景。
替代优化方案
- 动态核心线程:通过
allowCoreThreadTimeOut(true)
允许核心线程超时回收,保留复用能力。 - 弹性线程池
- 根据 QPS 动态调整
corePoolSize
(如夜间降为 0,高峰期恢复)。 - 结合监控告警自动扩容/缩容(如 Prometheus + 动态配置中心)。
- 根据 QPS 动态调整
- 动态核心线程:通过
实践建议
- 稳定性要求高的系统(如支付服务)避免核心线程数为 0,优先动态调整策略。
- 测试环境需验证线程创建速率和任务响应时间,防止生产环境性能瓶颈。
线程池中 shutdown (),shutdownNow() 这两个方法有什么作用?
shutdown() 的作用
- 新任务处理:立即拒绝新任务提交,触发拒绝策略(如抛
RejectedExecutionException
)。 - 现有任务处理:等待已提交任务(包括执行中和队列中的)全部完成。
- 状态变化:将线程池状态从
RUNNING
变为SHUTDOWN
,最终进入TERMINATED
。 - 线程中断:仅中断空闲线程,不干扰执行中的任务。
- 新任务处理:立即拒绝新任务提交,触发拒绝策略(如抛
shutdownNow() 的作用
- 新任务处理:立即拒绝新任务提交。
- 现有任务处理
- 尝试中断所有正在执行的任务(通过
interrupt()
)。 - 清空队列并返回未执行任务列表(
List<Runnable>
)。
- 尝试中断所有正在执行的任务(通过
- 状态变化:将状态从
RUNNING
改为STOP
,最终进入TERMINATED
。 - 线程中断:强制中断所有工作线程,无论是否空闲。
关键差异对比
- 新任务处理:均拒绝新任务。
- 现有任务处理
shutdown()
等待任务完成。shutdownNow()
中断任务并丢弃未执行任务。
- 线程中断范围
shutdown()
仅中断空闲线程。shutdownNow()
中断所有线程。
- 返回值
shutdown()
无返回值。shutdownNow()
返回未执行任务列表。
- 适用场景
shutdown()
:优雅停机(如服务正常退出)。shutdownNow()
:紧急终止(如死锁或资源耗尽)。
使用建议
- 组合调用:先调用
shutdown()
等待任务完成,若超时未终止再调用shutdownNow()
,需捕获InterruptedException
并处理。 - 任务容错设计:在任务代码中检查中断状态,确保能响应中断请求。
- 组合调用:先调用
注意事项
- 资源泄漏:未正确关闭线程池可能导致线程和资源(如数据库连接)无法释放。
- 队列影响
- 无界队列使用
shutdown()
可能导致 OOM。 shutdownNow()
清空队列可缓解内存压力。
- 无界队列使用
提交给线程池中的任务可以被撤回吗?
通过
Future
对象取消任务- 提交任务后返回
Future
对象,调用cancel(boolean mayInterruptIfRunning)
可尝试取消任务。 - 参数作用
mayInterruptIfRunning = true
:若任务已执行,触发线程中断(需任务代码响应中断)。mayInterruptIfRunning = false
:仅取消未开始执行的任务。
- 返回值
true
:任务成功取消(未开始或已响应中断)。false
:任务已完成或无法取消。
- 提交任务后返回
不同任务状态下的撤回结果
- 任务未开始执行:
cancel()
可成功移除队列中的任务。 - 任务正在执行:依赖任务代码的中断响应逻辑(如检查
isInterrupted()
或捕获InterruptedException
)。 - 任务已完成:无法撤回,
cancel()
返回false
。
- 任务未开始执行:
线程池实现的影响
ThreadPoolExecutor
的局限性- 无界队列(如
LinkedBlockingQueue
)中的任务无法直接撤回,需通过Future.cancel()
。 remove(Runnable task)
方法可移除队列中未执行的任务,但需明确任务引用。
- 无界队列(如
- 定时任务(
ScheduledThreadPool
)ScheduledFuture.cancel()
可终止周期性任务,已开始的单次任务仍需中断响应逻辑。
实践建议
- 代码容错设计:任务逻辑中定期检查中断状态,确保能响应取消请求。
- 组合关闭策略
- 先调用
shutdown()
等待任务完成,超时后使用shutdownNow()
强制终止。
- 先调用
- 避免无界队列:使用有界队列(如
ArrayBlockingQueue
)减少无法撤回任务的风险。
场景
多线程打印奇偶数,怎么控制打印的顺序
基础同步锁方案
- 同步机制:使用
synchronized
保证原子操作,避免竞态条件。 - 双重检查机制
- 外层
while(num <= 100)
控制整体循环。 - 内层
while(num%2 ==0)
实现条件等待。
- 外层
- 线程协作
wait()
释放锁并暂停当前线程。notify()
唤醒等待线程。
- 同步机制:使用
标志位优化方案
- 显式状态控制:通过
isOddTurn
布尔标志管理奇偶切换。 - 性能优化
- 避免频繁计算奇偶性,提升执行效率。
- 便于扩展多线程交替逻辑(如三线程 ABC 交替)。
- 显式状态控制:通过
高级锁机制方案
- 精细控制:使用
ReentrantLock
替代synchronized
,提供更灵活的锁管理。 - 条件队列
- 通过
Condition
对象(oddCond
和evenCond
)分别管理奇偶线程等待队列。 signal()
精准唤醒目标线程。
- 通过
- 精细控制:使用
实现要点总结
- 同步机制选择:简单场景用
synchronized
,复杂场景用ReentrantLock
。 - 条件判断:必须使用
while
循环检查条件,避免虚假唤醒。 - 数值递增:在同步代码块内完成修改,保证原子性。
- 终止条件:循环外设置退出检测(如
num > 100
),防止数值越界。 - 异常处理:捕获
InterruptedException
并确保锁最终释放。
- 同步机制选择:简单场景用
调试与扩展
- 调试建议
- 通过
Thread.setName()
设置线程名称,便于日志追踪。 - 使用
jstack
检测死锁,VisualVM
监控线程状态。
- 通过
- 扩展应用场景
- 多阶段交替:修改为三个线程交替打印(如 ABCABC 序列)。
- 动态调整上限:通过
volatile
变量实现运行时修改最大值。 - 分布式扩展:结合消息队列(如 Kafka)实现跨进程顺序控制。
- 调试建议
单例模型既然已经用了 synchronized,为什么还要在加 volatile?
解决指令重排序问题
- 对象实例化操作
instance = new Singleton()
在 JVM 中分为三步:- 分配内存空间
- 调用构造函数初始化对象
- 将对象引用赋值给变量
- 无
volatile
时可能发生指令重排序(如顺序变为 ①→③→②),导致其他线程获取未初始化完成的实例。 volatile
通过内存屏障禁止指令重排序,确保初始化顺序为 ①→②→③,避免「半初始化」对象。
- 对象实例化操作
保证可见性
- 当首个线程完成实例化后,
volatile
强制将最新值刷新到主内存,其他线程立即感知instance
非空,避免重复创建。 - 仅用
synchronized
时,其他线程可能因本地缓存未更新而误判instance
为空。
- 当首个线程完成实例化后,
双重检查锁定的完整性
- 第一次判空:减少锁竞争,仅实例未初始化时进入同步块。
- 第二次判空:防止多个线程通过第一次检查后重复初始化。
volatile
配合:确保第二次判空时读取最新值,避免指令重排序或可见性问题导致判空失效。
性能与安全的平衡
synchronized
保证原子性,但无法单独解决指令重排序和可见性问题。volatile
以较低性能代价(内存屏障)补充synchronized
的不足,使 DCL 单例高效且线程安全。
3 个线程并发执行,1 个线程等待这三个线程全部执行完在执行,怎么实现?
使用 CountDownLatch
- 核心机制:创建初始值为 3 的计数器,三个线程执行完毕后调用
countDown()
减少计数。 - 等待线程:通过
await()
阻塞至计数器归零后执行后续任务。
- 核心机制:创建初始值为 3 的计数器,三个线程执行完毕后调用
线程 Join 方法
- 实现方式:在等待线程中依次调用三个并发线程的
join()
,使其阻塞直至所有目标线程结束。 - 注意事项
- 需按顺序先启动并发线程再启动等待线程。
- 适用于简单场景,代码耦合度较高。
- 实现方式:在等待线程中依次调用三个并发线程的
CompletableFuture 组合
- 核心方法:通过
CompletableFuture.allOf()
等待所有异步任务完成。 - 优势:支持异步任务组合,可链式调用后续操作(如
thenRun()
)。
- 核心方法:通过
线程池与 awaitTermination
- 实现流程
- 将三个任务提交至线程池。
- 调用
shutdown()
关闭线程池后,使用awaitTermination()
等待任务完成。
- 优势:适合批量任务管理,可设置超时时间防止无限等待。
- 实现流程
信号量 (Semaphore)
- 控制逻辑
- 初始化三个许可,等待线程需调用
acquire(3)
获取全部许可后执行。 - 每个并发线程执行后调用
release()
释放许可。
- 初始化三个许可,等待线程需调用
- 适用场景:支持动态调整等待线程数量,灵活性较高。
- 控制逻辑
方案选择建议
- 轻量级场景:优先选择
CountDownLatch
或CompletableFuture
,代码简洁且控制精准。 - 批量任务管理:推荐线程池的
awaitTermination
,便于扩展和资源管理。 - 动态调整需求:使用
Semaphore
可灵活控制许可数量。 - 异常处理:所有方案需注意线程泄漏和死锁风险,确保资源释放。
- 轻量级场景:优先选择
假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?
可能的取值范围
- 最低结果:0(极端情况下所有加操作被覆盖)。
- 最高结果:100(理想线程串行执行)。
- 实际常见值:50~100 之间波动,可能更小(如交替执行多次覆盖修改)。
原因分析
- 非原子操作:
n += 1
包含读取、修改、写入三步,可能被线程切换打断。- 示例:线程 A 读
n=0
→ 线程 B 读n=0
→ 均写入n=1
,导致两次操作仅生效一次。
- 示例:线程 A 读
- 缓存可见性问题:多核 CPU 缓存未同步,线程可能读取旧值(如线程 A 修改后未刷回主存,线程 B 仍用
n=0
计算)。 - 指令重排序:编译器或 CPU 调整指令顺序,延迟写入操作加剧数据不一致。
- 非原子操作:
解决方案
- 同步锁
- 使用
synchronized
或ReentrantLock
保证临界区代码原子性。
- 使用
- 原子变量
- 用
AtomicInteger
替代普通整型变量,通过incrementAndGet()
的 CAS 机制保证原子性,性能优于锁。
- 用
- 线程安全容器
- 如
ConcurrentHashMap
提供细粒度并发控制,但本场景更适用原子变量。
- 如
- 同步锁
高优先级
线程池种类有哪些?
FixedThreadPool (固定大小线程池)
- 核心参数:
corePoolSize = maximumPoolSize
,无界队列LinkedBlockingQueue
。 - 特点:线程数固定,适用于任务量稳定场景(如 Web 服务器请求处理)。
- 风险:无界队列可能导致 OOM。
- 核心参数:
CachedThreadPool (可缓存线程池)
- 核心参数:核心线程数 0,最大线程数
Integer.MAX_VALUE
,同步队列SynchronousQueue
。 - 特点:动态扩容线程,空闲线程 60 秒回收(适合短期异步任务)。
- 风险:线程过多可能耗尽资源。
- 核心参数:核心线程数 0,最大线程数
SingleThreadExecutor (单线程池)
- 核心参数:核心线程数 1,无界队列
LinkedBlockingQueue
。 - 特点:保证任务顺序执行(如日志处理、数据库操作)。
- 核心参数:核心线程数 1,无界队列
ScheduledThreadPool (定时任务线程池)
- 核心参数:固定核心线程数,延迟队列
DelayedWorkQueue
。 - 特点:支持延时/周期性任务(如定时备份、心跳检测)。
- 核心参数:固定核心线程数,延迟队列
WorkStealingPool (工作窃取线程池)
- 核心参数:基于
ForkJoinPool
,默认线程数等于 CPU 核数。 - 特点:任务窃取机制,适合并行计算(如分治算法、图像处理)。
- 核心参数:基于
自定义线程池
- 核心参数:通过
ThreadPoolExecutor
定制核心线程数、队列类型及拒绝策略。 - 适用场景:高并发系统灵活平衡资源与效率。
- 核心参数:通过
其他补充
- ForkJoinPool:专为分治任务设计(如递归计算、大规模数据处理)。
- 拒绝策略:支持
AbortPolicy
(抛异常)或DiscardOldestPolicy
(丢弃旧任务)等。
线程池的参数有哪些?
corePoolSize (核心线程数)
- 线程池保持活跃的最小线程数,空闲时不会被销毁(除非设置
allowCoreThreadTimeOut=true
)。 - 新任务提交时优先创建核心线程处理,直到达到阈值。
- 线程池保持活跃的最小线程数,空闲时不会被销毁(除非设置
maximumPoolSize (最大线程数)
- 允许创建的最大线程总数(核心线程 + 非核心线程)。
- 当队列已满且线程数小于该值时,创建非核心线程。
keepAliveTime (空闲线程存活时间)
- 非核心线程空闲时的最大存活时间,超时后销毁。
- 与
unit
配合使用,默认不影响核心线程。
unit (时间单位)
- 定义
keepAliveTime
的时间单位(如秒、毫秒)。
- 定义
workQueue (任务队列)
- 存储待执行任务的阻塞队列,影响任务调度逻辑。
- 常见类型:
LinkedBlockingQueue
(无界队列,可能 OOM)。ArrayBlockingQueue
(有界队列,推荐使用)。SynchronousQueue
(直接传递任务)。
threadFactory (线程工厂)
- 自定义线程创建方式(命名规则、优先级、守护线程属性)。
- 默认使用
Executors.defaultThreadFactory()
。
handler (拒绝策略)
- 线程和队列均满时处理新任务的策略。
- 常用策略:
AbortPolicy
(默认,抛异常)。CallerRunsPolicy
(提交线程直接执行)。DiscardPolicy
(静默丢弃)。DiscardOldestPolicy
(丢弃最旧任务)。
注意事项
- 队列选择原则:
- CPU 密集型任务推荐有界队列(如
ArrayBlockingQueue
)。 - I/O 密集型任务可考虑无界队列(需防范 OOM)。
- CPU 密集型任务推荐有界队列(如
- 参数联动规则:
- 仅当
workQueue
已满且线程数 <maximumPoolSize
时创建非核心线程。 - 使用无界队列时
maximumPoolSize
无效。
- 仅当
- 推荐实践:手动创建
ThreadPoolExecutor
控制队列边界,避免使用Executors
默认实现。
- 队列选择原则:
线程池设计
任务类型决定核心线程数
- CPU 密集型任务(如加密计算):
corePoolSize = CPU 核数 + 1
(如 8 核设为 9 线程)。 - I/O 密集型任务(如网络请求):
corePoolSize ≈ 2 * CPU 核数
(如 8 核设为 16)。 - 混合型任务:
corePoolSize = Ncpu * Ucpu * (1 + W/C)
,其中W
为任务等待时间,C
为计算时间。
- CPU 密集型任务(如加密计算):
队列选择与容量设计
- 无界队列(
LinkedBlockingQueue
):适用短耗时任务,明确设置容量防止 OOM(如LinkedBlockingQueue(2000)
)。 - 有界队列(
ArrayBlockingQueue
):容量计算为(corePoolSize / 任务平均耗时) * 最大容忍响应时间
。 - 同步队列(
SynchronousQueue
):需高实时性,搭配maximumPoolSize
设为较大值(如 50-100)。
- 无界队列(
线程池扩容与收缩规则
- 最大线程数:通常设为核心数的 1.5-3 倍(如电商场景核心 85,最大 150)。
- 空闲线程回收:
keepAliveTime
设为 30-60 秒,突发流量可延长至 2-5 分钟。 - 动态调整:根据 QPS 监控自动扩容(
新核心数 = 当前 QPS * 平均耗时 / 1000 * 0.8
)。
拒绝策略与容错机制
- CallerRunsPolicy:允许主线程降级处理(如日志记录)。
- DiscardOldestPolicy:新任务优先级高(如实时消息覆盖)。
- 自定义策略:
- 持久化存储:任务存数据库/Redis 后续处理。
- 异步重试:延迟后提交或转移至备用线程池。
监控与调优实践
- 监控指标:活跃线程数、队列堆积量、任务平均耗时(推荐 Prometheus 实时监控)。
- 压测验证:某电商优化后 QPS 从 4200 提升至 8500,错误率从 15% 降至 0.2%。
- 弹性设计:容器水平扩容(如 K8s)与线程池参数调优结合应对突发流量。
避坑指南
- 避免
corePoolSize = maximumPoolSize
(丧失突发能力)。 - 明确队列容量(禁用默认无界队列)。
- CPU 密集型任务勿用
CallerRunsPolicy
(可能阻塞主线程)。
- 避免
介绍一下线程池的工作原理
核心组成与初始化
- 线程池管理器:负责创建、销毁线程及任务调度,维护线程池生命周期。
- 任务队列:存储待执行任务,常用阻塞队列(如
LinkedBlockingQueue
),队列满时触发拒绝策略。 - 工作线程:核心线程常驻处理任务,非核心线程队列满时动态创建,空闲超时后销毁。
- 线程工厂与拒绝策略:自定义线程属性(如命名规则),定义队列满时的处理逻辑(如抛异常)。
任务处理流程
- 任务提交:通过
submit()
或execute()
提交Runnable
/Callable
任务。 - 资源分配
- 线程数 < 核心线程数:直接创建新线程执行。
- 核心线程已满:任务入队等待。
- 队列满且线程数 < 最大线程数:创建救急线程处理。
- 队列和线程均满:触发拒绝策略(如
AbortPolicy
)。
- 任务执行:工作线程从队列取任务执行,完成后等待新任务。
- 任务提交:通过
线程池状态管理
- 状态标识:通过
ctl
变量(高3位状态,低29位线程数)维护:- RUNNING:正常接收并执行任务。
- SHUTDOWN:不再接收新任务,处理存量任务。
- STOP:中断所有任务,丢弃未处理任务。
- TERMINATED:线程池完全终止。
- 状态转换:
shutdown()
进入 SHUTDOWN,shutdownNow()
进入 STOP,完成后转为 TERMINATED。
- 状态标识:通过
性能优化机制
- 线程复用:避免频繁创建/销毁,减少上下文切换(每次 1-10 微秒)。
- 动态伸缩:根据任务量调整救急线程数量,空闲超时后回收。
- 队列选择
- CPU 密集型:有界队列(如
ArrayBlockingQueue
)。 - I/O 密集型:无界队列(如
LinkedBlockingQueue
)。
- CPU 密集型:有界队列(如
- 拒绝策略适配:电商系统采用
CallerRunsPolicy
由提交线程执行任务。
典型应用场景
- Web 服务器:Tomcat 用固定核心线程数(如200)处理 HTTP 请求。
- 异步任务处理:日志写入用缓存线程池(
CachedThreadPool
)动态扩容。 - 定时任务调度:
ScheduledThreadPool
支持延迟/周期性任务(如数据同步)。 - 数据库连接池:复用连接减少建立开销(类比线程池设计)。
为啥 Java 线程池核心线程满后,入队列等待,Tomcat 核心满后不入队列等待的原因
设计目标差异
- Java 线程池:面向 CPU 密集型任务,优先用队列缓冲任务减少线程创建开销。核心线程满后任务入队,队列满时才创建非核心线程。
- Tomcat 线程池:针对 I/O 密集型场景优化,优先创建新线程降低延迟,核心满后直接扩容线程至最大值,队列仅作最后缓冲。
队列行为实现机制
- Java 默认队列:使用
LinkedBlockingQueue
,核心线程满后直接入队,无界队列可能导致 OOM。 - Tomcat 自定义队列
- 重写
offer()
方法:当已提交任务数超过活跃线程数时返回false
触发创建新线程。 - 仅在线程达最大值或队列满时才真正入队。
- 重写
- Java 默认队列:使用
线程创建策略对比
- Java 线程池:严格遵循「核心线程→队列→非核心线程」流程,队列未满不扩容。
- Tomcat 线程池:核心线程满后,若提交任务数大于当前线程数,立即创建新线程直至最大值。
性能与资源权衡
- Java 队列优先:适合耗时长的 CPU 运算任务,减少线程切换损耗。
- Tomcat 线程优先:避免队列延迟,快速响应突发流量(如秒杀请求),默认限制队列长度(
acceptCount=100
)防 OOM。
参数配置体现差异
- Java 参数:
corePoolSize
与maximumPoolSize
分离,依赖队列容量控制资源。 - Tomcat 参数
maxThreads
限制线程总数,acceptCount
限制队列容量。minSpareThreads
预创建核心线程,减少首次请求延迟。
- Java 参数:
为什么不推荐用内置线程池
资源耗尽风险
- 无界队列导致 OOM
FixedThreadPool
和SingleThreadExecutor
使用LinkedBlockingQueue
,任务提交速度过快时队列无限堆积可能引发内存溢出。ScheduledThreadPool
的延迟队列容量为Integer.MAX_VALUE
,周期性任务堆积存在 OOM 隐患。
- 无界线程池问题
CachedThreadPool
允许创建Integer.MAX_VALUE
线程,高并发下可能耗尽 CPU/内存资源。
- 无界队列导致 OOM
配置灵活性与容错能力不足
- 参数固定化
- 内置线程池的队列类型、核心线程数等参数无法动态调整,难以适配突发流量或资源敏感型任务。
- 默认拒绝策略单一
- 默认使用
AbortPolicy
(抛异常),缺乏降级、重试等容错机制,可能引发服务雪崩。
- 默认使用
- 参数固定化
可维护性与监控缺陷
- 线程命名模糊
- 内置线程池生成的线程名称(如
pool-1-thread-n
)无业务含义,增加排查难度。
- 内置线程池生成的线程名称(如
- 缺乏监控支持
- 无法直接获取活跃线程数、队列堆积量等关键指标,需额外开发监控逻辑。
- 线程命名模糊
生产环境最佳实践
- 推荐替代方案
- 使用
ThreadPoolExecutor
手动配置:- 有界队列:如
ArrayBlockingQueue
控制内存风险。 - 合理线程数:根据 CPU 核数和任务类型(CPU/I/O 密集型)设置
corePoolSize
和maximumPoolSize
。 - 自定义拒绝策略:结合
CallerRunsPolicy
或降级逻辑(如任务存入外部队列)。
- 有界队列:如
- 使用
- 线程工厂与命名
- 自定义
ThreadFactory
为线程添加业务标识,便于日志追踪。
- 自定义
- 生命周期管理
- 通过
shutdown()
和shutdownNow()
组合实现优雅关闭,避免线程泄漏。
- 通过
- 推荐替代方案