一、线程的基础概念
1.1 基本概念
1.1.1 进程与线程
进程: 进程是指运行中的程序。 比如我们使用钉钉、浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源(占用内存资源);
所谓进程就是线程的容器,需要线程利用进程中的一些资源,处理一个代码、指令。最终实现进程锁预期的结果。
线程: 线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段;
进程 vs 线程:
- 根本不同: 进程是操作系统 分配的资源,而线程是CPU调度的基本单位;
- 资源方面: 同一个进程下的线程共享进程中的一些资源,线程同时拥有自身的独立存储空间,进程之间的资源通常是独立的;
- 数量不同: 进程一般指的是一个进程,而线程是依附于某个进程的,而且一个进程中至少会有一个或多个线程;
- 开销不同: 毕竟进程和线程不是一个级别的内容,线程的创建和终止的时间是比较短的,而且线程之间的切换比进程之间的切换速度要快很多,而且进程之间的通讯很麻烦,一般要借助内核才可以实现,而线程之间通讯,相当方便;
1.1.2 多线程
多线程是指单个进程中同时运行多个线程;
- 多线程是为了提高CPU的利用率;
- 可通过避免一些网络IO或磁盘IO等需要等待的操作,让CPU去调度其他线程;
- 可大幅度的提升程序的效率,提高用户的体验;
多线程的局限:
- 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗;
- 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好;
- 线程安全问题: 虽然多线程带来了一定的性能提升,但是再做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题;
1.1.3 串行、并行、并发
| 概念 | 核心含义 | 硬件要求 | 进程层面举例 | 线程层面举例 |
|---|---|---|---|---|
| 串行 | 一个接一个执行 | 无 | 单核 CPU 依次运行 A、B、C 三个进程 | 单线程代码,按顺序调用方法 1、2、3 |
| 并发 | 逻辑同时,微观交替 | 单核即可 | 单核 CPU 上,三个进程轮流使用时间片 | 单核 CPU 上,多线程通过锁/切换交替运行 |
| 并行 | 物理同时 | 必须多核/多 CPU | 4 核 CPU 同时运行 4 个进程 | 4 核 CPU 上,同一个进程内的 4 个线程分别跑在 4 个核上 |
总结 :串行、并行、并发描述的是任务的执行方式,既可以指进程,也可以指线程。并发是逻辑上的同时,并行是物理上的同时。
1.1.4 同步异步、阻塞非阻塞
核心概念对比
| 概念 | 关注维度 | 定义 | 关键词 |
|---|---|---|---|
| 同步 | 消息通知机制 | 调用者主动等待结果返回 | 等待、主动获取 |
| 异步 | 消息通知机制 | 被调用者主动通知结果 | 回调、被动通知 |
| 阻塞 | 线程状态 | 线程被挂起,无法执行其他任务 | 挂起、等待 |
| 非阻塞 | 线程状态 | 线程立即返回,可执行其他任务 | 立即返回、轮询 |
四种组合对比
| 组合模式 | 调用者状态 | 结果获取方式 | 效率 | 典型应用 |
|---|---|---|---|---|
| 同步阻塞 | 线程挂起等待 | 主动等待返回 | 低 | 传统BIO、简单应用 |
| 同步非阻塞 | 立即返回(轮询) | 主动轮询获取 | 中(CPU占用高) | 极少单独使用 |
| 异步阻塞 | 线程挂起等待 | 被动通知 | 极低(逻辑矛盾) | 几乎不用 |
| 异步非阻塞 | 立即返回 | 回调/事件通知 | 高 | Netty、Node.js、AIO |
| 模式 | 过程描述 | 对应技术概念 |
|---|---|---|
| 同步阻塞 | 打电话点餐后,拿着电话干等40分钟,直到饭做好 | 线程挂起,等待IO完成 |
| 同步非阻塞 | 挂掉电话,每隔5分钟主动打电话询问饭好了没 | 轮询检查状态 |
| 异步阻塞 | 餐厅说做好通知你,你拿着电话干等通知 | 设计不合理,实际少见 |
| 异步非阻塞 | 留下电话,去玩游戏,收到短信通知后取餐 | 回调机制,事件驱动 |
| 特性 | 同步阻塞 | 同步非阻塞 | 异步阻塞 | 异步非阻塞 |
|---|---|---|---|---|
| 线程是否挂起 | ✅ 是 | ❌ 否 | ✅ 是 | ❌ 否 |
| 是否立即返回 | ❌ 否 | ✅ 是 | ❌ 否 | ✅ 是 |
| 是否需要轮询 | ❌ 否 | ✅ 是 | ❌ 否 | ❌ 否 |
| 数据拷贝谁做 | 用户线程 | 用户线程 | 用户线程 | 内核线程 |
| 结果如何获取 | 主动等待 | 主动轮询 | 被动等待 | 被动回调 |
| 编程复杂度 | 低 | 中 | 中 | 高 |
| CPU利用率 | 低 | 高(轮询) | 低 | 高 |
| 吞吐量 | 低 | 中 | 低 | 高 |
| 适用并发量 | 低 | 中 | 低 | 高 |
IO模型对比
| IO模型 | 同步/异步 | 阻塞/非阻塞 | 数据准备阶段 | 数据拷贝阶段 | 代表技术 |
|---|---|---|---|---|---|
| 阻塞IO | 同步 | 阻塞 | 线程阻塞等待 | 线程参与拷贝 | 传统BIO |
| 非阻塞IO | 同步 | 非阻塞 | 轮询检查 | 线程参与拷贝 | 极少单独使用 |
| IO多路复用 | 同步 | 阻塞(select) | 内核通知就绪 | 线程参与拷贝 | epoll、Java NIO |
| 信号驱动IO | 同步 | 非阻塞 | 信号通知就绪 | 线程参与拷贝 | 较少使用 |
| 异步IO | 异步 | 非阻塞 | 内核自动处理 | 内核完成拷贝 | IOCP、io_uring、AIO |
数据流程
| 阶段 | 同步阻塞 | 同步非阻塞 | IO多路复用 | 异步非阻塞 |
|---|---|---|---|---|
| 发起请求 | 调用read | 调用read | 调用select | 调用aio_read |
| 数据未就绪 | 线程阻塞 | 立即返回错误 | select阻塞 | 立即返回 |
| 数据就绪通知 | 无(直接返回) | 轮询发现 | select返回 | 回调/信号 |
| 数据拷贝 | 线程完成 | 线程完成 | 线程完成 | 内核完成 |
| 线程参与度 | 全程参与 | 轮询+拷贝 | 等待+拷贝 | 仅发起请求 |
误区
| 误区 | 错误认知 | 正确理解 |
|---|---|---|
| epoll是异步IO | epoll是异步的 | epoll是同步IO多路复用,数据拷贝仍需用户线程完成 |
| 非阻塞就是异步 | 调用立即返回=异步 | 非阻塞只表示立即返回,但仍需主动轮询,是同步模式 |
| 同步一定效率低 | 同步性能差 | 低并发下同步阻塞代码简单易维护,效率并不低 |
| 异步一定性能高 | 异步总是更好 | 异步编程复杂度高,不适合所有场景 |
| 阻塞=同步 | 阻塞和同步是一回事 | 阻塞是线程状态,同步是通知机制,两者不同维度 |
同步 vs 异步
| 对比维度 | 同步 | 异步 |
|---|---|---|
| 通知方式 | 调用者主动等待 | 被调用者主动通知 |
| 结果获取 | 调用者自己获取 | 被调用者推送结果 |
| 数据拷贝 | 用户线程参与 | 内核自动完成 |
| 编程复杂度 | 简单 | 复杂 |
| CPU利用率 | 低(阻塞时) | 高 |
| 并发能力 | 低(受线程数限制) | 高 |
| 调试难度 | 容易 | 困难 |
| 代表场景 | 简单CRUD应用 | 高并发网络服务 |
阻塞 vs 非阻塞
| 对比维度 | 阻塞 | 非阻塞 |
|---|---|---|
| 线程状态 | 挂起(Waiting/Blocked) | 就绪/运行(Runnable/Running) |
| CPU占用 | 释放CPU | 占用CPU(轮询时) |
| 响应延迟 | 较低(立即可用) | 较高(轮询间隔) |
| 资源消耗 | 线程栈内存 | CPU时间片 |
| 并发能力 | 一个线程一个连接 | 一个线程多个连接 |
| 编程模型 | 线性顺序执行 | 状态机/事件驱动 |
面试高频问答
| 问题 | 简洁答案 | 详细说明 |
|---|---|---|
| 同步和异步的区别? | 如何通知 | 同步主动等待,异步被动通知 |
| 阻塞和非阻塞的区别? | 线程状态 | 阻塞挂起,非阻塞立即返回 |
| epoll是同步还是异步? | 同步 | 数据拷贝由用户线程完成 |
| NIO是阻塞还是非阻塞? | 非阻塞 | 但select可以阻塞等待 |
| 异步非阻塞的优势? | 高并发 | 线程不等待,数据拷贝不参与 |
| 什么时候用同步阻塞? | 低并发场景 | 代码简单,易于维护 |
| 什么时候用异步非阻塞? | 高并发场景 | CPU利用率高,吞吐量大 |
1.2 线程的创建
1.2.1 继承Thread类 重写run方法
启动线程是调用start方法 ,这样会创建一个新的线程,并执行线程的任务,如果直接调用run方法 ,会让当前线程执行run方法中的业务逻辑;
特点:
- 简单直接,但Java是单继承,继承了Thread就不能继承其他类;
- 线程执行的任务与线程对象绑定,耦合度高;
java
/**
* 线程启动顺序不等于执行顺序;
* 线程执行速度受CPU调度影响;
* 每次运行结果可能不同(非确定性);
* 这是多线程并发执行的表现;
*/
public class MyThread01 extends Thread {
@Override
public void run() {
// 线程执行的具体任务
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建并启动线程
MyThread01 thread01 = new MyThread01();
MyThread01 thread02 = new MyThread01();
MyThread01 thread03 = new MyThread01();
thread01.start(); //启动线程,自动调用run方法
thread02.start();
thread03.start();
}
}
1.2.2 实现Runnable接口 重写run方法
实现Runnable接口,重写run方法,将任务与线程分离;
特点:
- 避免了单继承的局限,可以继承其他类;
- 任务与线程解耦,便于复用和测试;
- 更符合面向对象的设计思想;
java
public class MyThread02 implements Runnable {
private String taskName;
public MyThread02(String taskName) {
this.taskName = taskName;
}
public MyThread02() {
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(taskName + "-" + Thread.currentThread().getName() + "执行:" + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建任务对象
MyThread02 task01 = new MyThread02("任务A");
MyThread02 task02 = new MyThread02("任务B");
// MyThread02 task04 = new MyThread02();
// 将任务交给线程执行
Thread thread01 = new Thread(task01, "线程1");
Thread thread02 = new Thread(task02, "线程2");
// Thread thread04 = new Thread(task04);
thread01.start();
thread02.start();
// thread04.start();
// 使用lambda表达式简化
Thread thread03 = new Thread(() -> {
System.out.println("使用Lambda创建线程");
});
thread03.start();
}
}
1.2.3 实现Callable 重写call方法,配合FutrueTask
实现Callable接口,重写call方法,支持返回结果和抛出异常;
特点:
- 可以有返回值;
- 可以抛出异常;
- 支持泛型,返回指定类型的结果;
- 可以通过Future对象获取结果、取消任务、检查状态;
java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class MyThread03 implements Callable<String> {
private int taskId;
public MyThread03(int taskId) {
this.taskId = taskId;
}
@Override
public String call() throws Exception {
System.out.println("任务" + taskId + "开始执行,线程:" + Thread.currentThread().getName());
Thread.sleep(1000); //模拟耗时操作
// 可以抛出异常
if (taskId == 3) {
throw new RuntimeException("任务3执行失败");
}
return "任务" + taskId + "执行完成,返回结果:" + System.currentTimeMillis();
}
public static void main(String[] args) {
// 方式1:使用FutureTask包装Callable任务
FutureTask<String> futureTask1 = new FutureTask<>(new MyThread03(1));
Thread thread01 = new Thread(futureTask1);
thread01.start();
try {
// 获取返回结果(阻塞知道任务完成)
String result01 = futureTask1.get();
System.out.println("获取到结果:" + result01);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 方式2:配合线程池使用
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> future2 = executor.submit(new MyThread03(2));
try {
// 设置超时时间
String result2 = future2.get(2, TimeUnit.SECONDS);
System.out.println("结果:" + result2);
} catch (TimeoutException e) {
System.out.println("任务超时");
future2.cancel(true); //取消任务
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
// 批量提交多个Callable任务
ExecutorService executor2 = Executors.newFixedThreadPool(3);
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
futures.add(executor2.submit(new MyThread03(i)));
}
// 等待所有任务完成
for (Future<String> future : futures) {
try {
System.out.println(future.get());
} catch (Exception e) {
System.out.println("任务执行异常:" + e.getMessage());
}
}
executor2.shutdown();
}
}
1.2.4 基于线程池构建线程
使用线程池管理线程,避免频繁创建销毁线程的开销。
线程池核心参数说明:
- corePoolSize: 核心线程数,即使空闲也会保留;
- maximumPoolSize: 最大线程数;
- keepAliveTime: 空闲线程存活时间;
- unit: 时间单位;
- workQueue: 任务队列(阻塞队列);
- threadFactory: 线程工厂(用于创建线程);
- handler: 拒绝策略(队列满且线程数达到最大时的处理策略);
常用的拒绝策略:
- AbortPolicy: 直接抛出异常(默认);
- CallerRunsPolicy: 在调用者线程中执行任务;
- DiscardPolicy: 静默丢弃任务;
- DiscardOldestPolicy: 丢弃队列中最旧的任务;
java
package com.ruoyi.web.controller.test;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class MyThread04 {
public static void main(String[] args) {
// 1. 创建固定大小的线程池
fixedThreadPoolExample();
// 2. 创建缓存线程池
cachedThreadPoolExample();
// 3. 创建单线程池
singleThreadExecutorExample();
// 4. 创建定时任务线程池
scheduledThreadPoolExample();
// 5. 自定义线程池(推荐方式)
customThreadPoolExample();
}
// 固定大小的线程池:每次最多有3个线程
private static void fixedThreadPoolExample() {
System.out.println("=== 固定大小线程池示例 ===");
ExecutorService fixedPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int taskId = i;
fixedPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + taskId);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
fixedPool.shutdown(); // 关闭线程池
}
// 缓存线程池(根据需要创建新线程,空闲线程会被回收)
private static void cachedThreadPoolExample() {
System.out.println("=== 缓存线程池示例 ===");
ExecutorService cachedPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int taskId = i;
cachedPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + taskId);
return "完成";
});
}
cachedPool.shutdown();
}
// 单线程池(保证任务顺序执行)
private static void singleThreadExecutorExample() {
System.out.println("=== 单线程池示例 ===");
ExecutorService singlePool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
final int taskId = i;
singlePool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + taskId);
});
}
singlePool.shutdown();
}
// 定时任务线程池
private static void scheduledThreadPoolExample() {
System.out.println("=== 定时任务线程池示例 ===");
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// 延迟执行
scheduledPool.schedule(() -> {
System.out.println("延迟3秒执行的任务");
}, 3, TimeUnit.SECONDS);
// 周期性执行(首次延迟2秒,之后每5秒执行一次)
scheduledPool.scheduleAtFixedRate(() -> {
System.out.println("周期性任务: " + System.currentTimeMillis());
}, 2, 5, TimeUnit.SECONDS);
// 10秒后关闭线程池(示例用,实际项目中需要合理管理)
scheduledPool.schedule(() -> scheduledPool.shutdown(), 10, TimeUnit.SECONDS);
}
// 自定义线程池(最灵活、最推荐的方式)
private static void customThreadPoolExample() {
System.out.println("=== 自定义线程池示例 ===");
// 创建自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(10), // 任务队列
new ThreadFactory() { // 自定义线程工厂
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("自定义线程-" + threadNumber.getAndIncrement());
thread.setDaemon(false);
System.out.println("创建新线程: " + thread.getName());
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交任务
for (int i = 0; i < 20; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + taskId);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 监控线程池状态
new Thread(() -> {
while (!executor.isTerminated()) {
System.out.println("线程池状态 - 活跃线程数: " + executor.getActiveCount() +
", 任务队列大小: " + executor.getQueue().size() +
", 已完成任务数: " + executor.getCompletedTaskCount());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
break;
}
}
}).start();
// 关闭线程池
executor.shutdown();
try {
// 等待所有任务完成
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 强制关闭
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
System.out.println("所有任务执行完毕");
}
}
1.3 线程的使用
1.3.1 线程的状态
线程在生命周期中有多种状态,这些状态之间可以相互转换。以下是线程的六种核心状态:

| 状态 | 说明 |
|---|---|
| 新建 (New) | 线程对象创建但未调用 start() 方法 |
| 可运行 (Runnable) | 调用 start() 后,等待CPU调度执行 |
| 运行 (Running) | 获得CPU时间片,正在执行 run() 方法 |
| 阻塞 (Blocked) | 等待获取监视器锁(synchronized) |
| 等待 (Waiting) | 无限期等待其他线程唤醒(wait()、join()无超时) |
| 超时等待 (Timed Waiting) | 有限时间内等待(sleep()、wait(timeout)、join(timeout)) |
| 终止 (Terminated) | 线程执行完毕或异常退出 |
1.3.2 线程的常用方法
| 方法 | 作用 | 使用场景 |
|---|---|---|
start() |
启动线程,进入就绪状态 | 线程创建后调用 |
run() |
线程执行体 | 子类重写此方法 |
sleep(long millis) |
让当前线程休眠指定时间 | 模拟延时、让出CPU |
yield() |
让出CPU时间片,进入就绪状态 | 线程礼让,不释放锁 |
join() |
等待目标线程执行完毕 | 线程间同步 |
interrupt() |
中断线程 | 优雅停止线程 |
优先级设置
// 优先级范围 1-10
thread.setPriority(Thread.MAX_PRIORITY); // 10
thread.setPriority(Thread.NORM_PRIORITY); // 5
thread.setPriority(Thread.MIN_PRIORITY); // 1
等待与通知(配合 synchronized)
| 方法 | 作用 |
|---|---|
wait() |
当前线程进入等待状态,释放锁 |
wait(long timeout) |
超时等待,释放锁 |
notify() |
唤醒一个等待该锁的线程 |
notifyAll() |
唤醒所有等待该锁的线程 |
注意:
- wait()、notify()、notifyAll() 必须在 synchronized 代码块中调用;
- 锁对象必须是调用这些方法的对象;
1.3.3 线程的结束方式
- 自然结束(推荐方式)
线程的run()方法执行完毕,自动终止。
java
public class NaturalEnd extends Thread {
@Override
public void run() {
// 执行完业务逻辑后自动结束
for (int i = 0; i < 10; i++) {
System.out.println("执行第 " + i + " 次");
}
// 方法结束,线程自然终止
}
}
- 标志位退出(推荐方式)
使用 volatile 标志位控制线程结束。
java
public class FlagStop implements Runnable {
// volatile 保证内存可见性
private volatile boolean running = true;
@Override
public void run() {
while (running) {
try {
// 执行业务逻辑
Thread.sleep(1000);
System.out.println("线程运行中...");
} catch (InterruptedException e) {
// 处理中断异常
Thread.currentThread().interrupt();
break;
}
}
System.out.println("线程已停止");
}
public void stop() {
this.running = false;
}
// 使用示例
public static void main(String[] args) throws InterruptedException {
FlagStop task = new FlagStop();
Thread thread = new Thread(task);
thread.start();
Thread.sleep(5000);
task.stop(); // 优雅停止
}
}
- 中断机制退出(推荐方式)
使用 interrupt() 方法配合中断状态检查。
java
public class InterruptStop implements Runnable {
@Override
public void run() {
// 检查中断状态
while (!Thread.currentThread().isInterrupted()) {
try {
// 执行业务逻辑
Thread.sleep(1000); // sleep会自动响应中断
System.out.println("线程运行中...");
} catch (InterruptedException e) {
// 捕获中断异常,重置中断标志位
Thread.currentThread().interrupt(); // 重新设置中断标志
System.out.println("检测到中断请求,准备退出");
break;
}
}
System.out.println("线程已通过中断停止");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptStop());
thread.start();
Thread.sleep(5000);
thread.interrupt(); // 发送中断信号
}
}
- 废弃的 stop() 方法(不推荐)
java
// 以下方法已被标记为 @Deprecated,存在安全隐患
thread.stop(); // 可能导致数据不一致
thread.suspend(); // 容易造成死锁
thread.resume(); // 与suspend配合使用
废弃原因:
- stop(): 直接终止线程,不会释放持有的锁,可能导致数据不一致
- suspend()/resume(): 挂起线程时不释放锁,容易造成死锁;
结束方式对比
| 结束方式 | 推荐度 | 优点 | 缺点 |
|---|---|---|---|
| 自然结束 | ⭐⭐⭐⭐⭐ | 最简单,安全可靠 | 需要代码逻辑自然执行完 |
| 标志位 | ⭐⭐⭐⭐⭐ | 可控性强,主动结束 | 需要正确处理阻塞状态 |
| 中断机制 | ⭐⭐⭐⭐⭐ | Java官方推荐,响应阻塞操作 | 需要正确处理InterruptedException |
| stop() | ❌ 废弃 | 无 | 不安全,可能破坏数据一致性 |
二、并发编程的三大特性
并发编程的三大特性是保证线程安全 的核心基础,理解这三大特性有助于编写正确的多线程程序:

2.1 原子性
定义: 原子性是指一个或多个操作在CPU执行过程中不被中断的特性,即这些操作要么全部执行完成,要么全部不执行。
java
public class AtomicityExample {
private int count = 0;
// 非原子操作:count++ 实际上包含三个步骤
public void increment() {
count++; // 1. 读取count值 2. 加1 3. 写回count
}
// 多线程环境下会出现线程安全问题
}
| 方式 | 说明 | 示例 |
|---|---|---|
| synchronized | 同步锁,保证代码块原子性 | synchronized(this) { count++; } |
| Lock | 显式锁,提供更灵活的锁机制 | lock.lock(); try { count++; } finally { lock.unlock(); } |
| AtomicXXX | CAS机制实现的原子类 | AtomicInteger count = new AtomicInteger(); count.incrementAndGet(); |
| volatile | 不能保证原子性 | 只能保证可见性,不能保证复合操作的原子性 |
java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
public int getCount() {
return count.get();
}
}
2.2 可见性
定义: 可见性是指当一个线程修改了共享变量 的值,其他线程能够立即感知 到这个修改。
问题原因: Java内存模型(JMM)规定,每个线程都有自己的工作内存(本地缓存),线程对变量的操作必须在工作内存中进行,不能直接操作主内存。这导致一个线程修改了变量后,其他线程可能无法立即看到修改。

保证可见性的方式
| 方式 | 原理 | 示例 |
|---|---|---|
| volatile | 强制将修改立即写回主内存,并通知其他线程缓存失效 | private volatile boolean flag = true; |
| synchronized | 进入同步块时清空工作内存,退出时刷新到主内存 | synchronized(this) { count++; } |
| Lock | 与synchronized类似,保证可见性 | lock.lock(); try { count++; } finally { lock.unlock(); } |
| final | 一旦初始化完成,其他线程可见(不发生重排序) | private final int value; |
java
public class VisibilityExample {
// volatile 保证可见性
private volatile boolean running = true;
public void stop() {
running = false; // 修改立即可见
}
public void run() {
while (running) {
// 能够及时感知到 running 的变化
System.out.println("运行中...");
}
}
}
2.3 有序性
定义: 有序性是指程序执行的顺序按照代码的先后顺序执行。由于指令重排序优化,实际执行顺序可能与代码顺序不一致。
指令重排序: 编译器和处理器为了优化性能,可能会对指令进行重排序,但在单线程环境下保证最终结果一致。
// 代码顺序
int a = 1; // 语句1
int b = 2; // 语句2
int c = a + b; // 语句3
// 可能的重排序执行顺序
// 语句2 -> 语句1 -> 语句3
// 但不会出现 语句3 在语句1、2之前执行
重排序的三种类型
| 重排序类型 | 说明 |
|---|---|
| 编译器重排序 | 编译器在不改变单线程语义的前提下,重新调整执行顺序 |
| 处理器重排序 | 处理器采用乱序执行技术,对指令进行重排序 |
| 内存系统重排序 | 由于缓存和写缓冲区的存在,导致内存操作看起来被重排序 |
保证有序性的方式
| 方式 | 原理 | 说明 |
|---|---|---|
| volatile | 插入内存屏障,禁止指令重排序 | 保证volatile变量的读写有序 |
| synchronized | 同一时刻只有一个线程执行,天然有序 | 同步块内的代码不会与其他线程交叉执行 |
| Lock | 与synchronized类似 | 保证临界区的有序性 |
| happens-before规则 | JMM定义的一组规则 | 保证特定操作之间的可见性和有序性 |
java
public class Singleton {
// volatile 防止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
// 问题:new操作不是原子操作
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将引用指向内存地址
// 重排序可能导致 1->3->2
// volatile 禁止重排序
instance = new Singleton();
}
}
}
return instance;
}
}
as-if-serial: 在单线程环境下,重排序后的执行结果必须与按代码顺序执行的结果完全一致。
不论指定如何重排序,需要保证单线程的程序执行结果是不变的。而且如果存在依赖的关系,那么也不可以做指令重排。
java
// 这种情况肯定不能做指令重排序
int i = 0;
i++;
// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100
happens-before: 是 Java 内存模型(JMM)中定义的一个核心概念,用于保证多线程环境下操作的可见性和有序性。
具体规则:
| 规则 | 说明 |
|---|---|
| 程序次序规则 | 同一个线程中,前面的操作happens-before后面的操作 |
| 监视器锁规则 | unlock操作happens-before后续的lock操作 |
| volatile变量规则 | volatile写happens-before后续的volatile读 |
| 传递性规则 | A happens-before B,B happens-before C,则A happens-before C |
| 线程启动规则 | start()方法happens-before该线程的所有操作 |
| 线程终止规则 | 线程的所有操作happens-before其他线程检测到该线程终止 |
| 中断规则 | 调用interrupt()方法happens-before检测到中断事件 |
| 对象终结规则 | 对象构造完成happens-before finalize()方法 |
volatile: 是Java 虚拟机提供的轻量级同步机制,用于保证共享变量的可见性和有序性,但不能保证原子性。
如果需要让程序对某一个属性的操作不出现指令重排,除了满足happens-before原则之外,还可以基于volatile修饰属性,从而对这个属性的操作,就不会出现指令重排的问题了。
volatile如何实现的禁止指令重排?
- 内存屏障概念。将内存屏障看成一条指令。
- 会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。
三、锁
3.1 锁的分类
3.1.1 可重入锁、不可重入锁
定义:
可重入锁: 同一个线程可以多次获取同一把锁,不会产生死锁;
不可重入锁: 同一个线程如果已经持有锁,再次尝试获取时会阻塞;
可重入锁(ReentrantLock和synchronized都是可重入锁)
java
// 可重入锁示例(ReentrantLock和synchronized都是可重入锁)
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 第一次获取锁
try {
System.out.println("外层方法获取锁");
innerMethod(); // 同一个线程可以再次获取锁
} finally {
lock.unlock();
}
}
public void innerMethod() {
lock.lock(); // 第二次获取锁,可以成功(重入)
try {
System.out.println("内层方法再次获取锁");
// state计数器变为2
} finally {
lock.unlock(); // 释放一次,state变为1
}
}
}
不可重入锁
java
// 模拟不可重入锁的实现
class NonReentrantLock {
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
// 如果锁已被占用,即使当前线程持有锁也会阻塞
while (isLocked) {
wait();
}
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notify();
}
}
为什么要设计可重入锁
java
public class WhyReentrant {
// 场景1:递归调用
public synchronized void recursive(int n) {
if (n <= 0) return;
System.out.println("递归调用: " + n);
recursive(n - 1); // 如果是不可重入锁,这里会死锁
}
// 场景2:父子方法调用
public synchronized void parent() {
child(); // 同一个锁,可重入避免死锁
}
public synchronized void child() {
// 业务逻辑
}
// 场景3:模板方法模式
public abstract class BaseClass {
public synchronized void templateMethod() {
doSomething(); // 调用子类方法,可能也用了同一把锁
}
protected abstract void doSomething();
}
}
3.1.2 乐观锁、悲观锁
乐观锁: 假设不会发生冲突,只在更新时检查是否被修改;
悲观锁: 假设会发生并发冲突,每次访问数据都加锁;
乐观锁
java
// 乐观锁示例 - 使用CAS
public class OptimisticLock {
private AtomicInteger count = new AtomicInteger(0);
// 不加锁,CAS操作
public void increment() {
// 乐观地尝试更新,失败则重试
count.incrementAndGet(); // 内部使用CAS
}
// 自定义乐观锁实现
private volatile int value;
private volatile int version;
public boolean update(int newValue) {
int currentVersion = version;
// 模拟业务处理
if (currentVersion == version) { // 检查版本
this.value = newValue;
this.version++;
return true;
}
return false; // 版本冲突,需要重试
}
}
悲观锁
java
// 悲观锁示例
public class PessimisticLock {
private int count = 0;
private final Object lock = new Object();
// 每次操作都加锁
public void increment() {
synchronized (lock) { // 悲观锁
count++;
}
}
}
应用场景
java
// 场景1:数据库乐观锁
@Entity
public class Product {
@Id
private Long id;
private String name;
private Integer stock;
@Version // JPA版本号,乐观锁
private Integer version;
// 更新库存时,自动检查版本号
public void reduceStock(int quantity) {
// UPDATE product SET stock = stock - ?, version = version + 1
// WHERE id = ? AND version = ?
if (this.stock < quantity) {
throw new InsufficientStockException();
}
this.stock -= quantity;
// 如果version不匹配,JPA会抛出OptimisticLockException
}
}
// 场景2:Redis分布式锁(悲观锁)
public class RedisDistributedLock {
private final RedisTemplate<String, String> redisTemplate;
public boolean tryLock(String key, String value, long timeout) {
// SET key value NX EX timeout - 悲观锁
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, Duration.ofSeconds(timeout));
return Boolean.TRUE.equals(result);
}
}
// 场景3:选择策略
public class LockChoiceStrategy {
/**
* 选择原则:
* - 写操作多、竞争激烈 → 悲观锁
* - 读操作多、冲突少 → 乐观锁
* - 需要保证数据一致性 → 悲观锁
* - 追求性能、可接受重试 → 乐观锁
*/
public void chooseLock() {
// 高并发秒杀场景 - 悲观锁
seckillWithPessimisticLock();
// 用户信息更新场景 - 乐观锁
updateUserWithOptimisticLock();
}
private void seckillWithPessimisticLock() {
// 秒杀场景,竞争激烈,使用悲观锁
synchronized (this) {
// 扣减库存
}
}
private void updateUserWithOptimisticLock() {
// 用户信息更新,冲突少,使用乐观锁
AtomicReference<User> userRef = new AtomicReference<>();
User oldUser = userRef.get();
User newUser = new User(oldUser);
newUser.setName("new name");
userRef.compareAndSet(oldUser, newUser); // 乐观更新
}
}
3.1.3 公平锁、非公平锁
公平锁: 线程按照请求锁的顺序获取锁(FIFO)。
非公平锁: 线程获取锁的顺序不确定,可能插队。
性能对比
java
public class FairnessComparison {
// 公平锁
private ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁(默认)
private ReentrantLock unfairLock = new ReentrantLock(false);
public void demonstrate() throws InterruptedException {
// 测试公平锁
testLock(fairLock, "公平锁");
// 测试非公平锁
testLock(unfairLock, "非公平锁");
}
private void testLock(ReentrantLock lock, String lockType) {
for (int i = 0; i < 5; i++) {
int threadId = i;
new Thread(() -> {
lock.lock();
try {
System.out.println(lockType + " - 线程" + threadId + " 获取锁");
Thread.sleep(100); // 模拟工作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
}
}
实现原理
java
// 公平锁与非公平锁的实现差异(简化版)
class FairnessDemo {
// 公平锁的获取逻辑
class FairLock {
private final Queue<Thread> waitQueue = new ConcurrentLinkedQueue<>();
public void lock() {
Thread current = Thread.currentThread();
// 必须排队
waitQueue.offer(current);
while (waitQueue.peek() != current || !tryAcquire()) {
// 如果不是队首或获取锁失败,就等待
LockSupport.park();
}
waitQueue.poll(); // 获取锁成功,出队
}
private boolean tryAcquire() {
// 尝试获取锁的逻辑
return true;
}
}
// 非公平锁的获取逻辑
class NonFairLock {
public void lock() {
// 先尝试直接获取锁(插队)
if (tryAcquire()) {
return;
}
// 获取失败才进入等待队列
// ... 后续排队逻辑
}
private boolean tryAcquire() {
// 尝试获取锁的逻辑
return true;
}
}
}
选择建议
java
public class FairnessChoice {
/**
* 公平锁:
* - 优点:避免线程饥饿,所有线程最终都能执行
* - 缺点:性能较差(约非公平锁的1/10),需要上下文切换
* - 适用:业务要求严格的执行顺序
*/
/**
* 非公平锁:
* - 优点:性能高,吞吐量大
* - 缺点:可能导致某些线程长期获取不到锁(饥饿)
* - 适用:大多数业务场景
*/
// 示例:需要严格顺序的场景
public void needFairLock() {
ReentrantLock fairLock = new ReentrantLock(true);
// 银行转账系统,需要按顺序处理交易
// 避免某些交易长期等待
}
// 示例:高性能场景
public void needHighPerformance() {
ReentrantLock unfairLock = new ReentrantLock(false);
// 高并发缓存系统,追求吞吐量
}
}
3.1.4 互斥锁、共享锁
互斥锁: 同一时刻只能有一个线程持有锁(写锁)。
共享锁: 同一时刻可以有多个线程同时持有锁(读锁)。
读写锁实现
java
public class ReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock(); // 共享锁
private final Lock writeLock = rwLock.writeLock(); // 互斥锁
private Map<String, Object> data = new HashMap<>();
// 共享锁:多个线程可以同时读
public Object read(String key) {
readLock.lock(); // 共享锁,不互斥
try {
return data.get(key);
} finally {
readLock.unlock();
}
}
// 互斥锁:同一时刻只能一个线程写
public void write(String key, Object value) {
writeLock.lock(); // 互斥锁,其他读写线程都会阻塞
try {
data.put(key, value);
} finally {
writeLock.unlock();
}
}
// 锁升级:不能从共享锁升级为互斥锁
public void lockUpgrade(String key) {
readLock.lock();
try {
// 读锁状态下不能直接获取写锁,会导致死锁
// writeLock.lock(); // 错误!会导致死锁
// 必须先释放读锁
} finally {
readLock.unlock();
}
// 正确的做法:释放读锁后获取写锁
writeLock.lock();
try {
// 写操作
} finally {
writeLock.unlock();
}
}
// 锁降级:可以从互斥锁降级为共享锁
public void lockDowngrade(String key, Object value) {
writeLock.lock();
try {
data.put(key, value);
// 在释放写锁前获取读锁,实现锁降级
readLock.lock();
} finally {
writeLock.unlock(); // 释放写锁,仍持有读锁
}
try {
// 此时仍然是读锁状态,可以读数据
System.out.println("Lock downgrade: " + data.get(key));
} finally {
readLock.unlock();
}
}
}
互斥锁和共享锁状态实现
java
// 读写锁的状态表示(AQS实现)
class ReadWriteLockState {
/**
* 用一个int变量state表示锁状态:
* 高16位:读锁计数(共享锁)
* 低16位:写锁计数(互斥锁)
*/
private volatile int state;
// 获取读锁计数(共享锁数量)
int getReadLockCount() {
return state >>> 16; // 无符号右移16位
}
// 获取写锁计数(互斥锁数量,只能是0或1,但支持重入)
int getWriteLockCount() {
return state & 0xFFFF; // 低16位
}
// 尝试获取读锁(共享锁)
boolean tryAcquireShared() {
int c = state;
int writeCount = c & 0xFFFF;
// 如果有写锁且不是当前线程持有,获取失败
if (writeCount != 0 &&
Thread.currentThread() != getExclusiveOwnerThread()) {
return false;
}
// 增加读锁计数
int readCount = (c >>> 16) + 1;
if (compareAndSetState(c, (readCount << 16) | writeCount)) {
return true;
}
return false;
}
// 尝试获取写锁(互斥锁)
boolean tryAcquireExclusive() {
int c = state;
int writeCount = c & 0xFFFF;
int readCount = c >>> 16;
// 如果有读锁或者其他线程持有写锁,获取失败
if (readCount > 0) return false;
if (writeCount > 0 &&
Thread.currentThread() != getExclusiveOwnerThread()) {
return false;
}
// 增加写锁计数
if (compareAndSetState(c, c + 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
}
实际应用场景
java
// 场景1:缓存系统(读多写少)
public class Cache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 读操作使用共享锁,高并发读
public V get(K key) {
lock.readLock().lock();
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
// 写操作使用互斥锁,保证数据一致性
public void put(K key, V value) {
lock.writeLock().lock();
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}
// 场景2:倒排索引(读写分离)
public class InvertedIndex {
private final Map<String, Set<Integer>> index = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 搜索操作:多个线程同时搜索
public Set<Integer> search(String keyword) {
lock.readLock().lock();
try {
return new HashSet<>(index.getOrDefault(keyword, Collections.emptySet()));
} finally {
lock.readLock().unlock();
}
}
// 索引更新:互斥更新
public void addDocument(int docId, String[] keywords) {
lock.writeLock().lock();
try {
for (String keyword : keywords) {
index.computeIfAbsent(keyword, k -> new HashSet<>()).add(docId);
}
} finally {
lock.writeLock().unlock();
}
}
}
| 锁类型 | 核心特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 可重入锁 | 同一线程可重复获取 | 避免死锁,方便递归 | 需要正确释放(计数) | 递归调用、父子方法 |
| 不可重入锁 | 不能重复获取 | 实现简单 | 容易死锁 | 不推荐使用 |
| 乐观锁 | 不加锁,更新时检查 | 性能高,无阻塞 | 冲突时重试开销 | 读多写少,冲突少 |
| 悲观锁 | 每次操作都加锁 | 数据安全,无重试 | 性能低,可能死锁 | 写操作多,竞争激烈 |
| 公平锁 | FIFO顺序获取 | 无线程饥饿 | 性能差 | 需要严格顺序 |
| 非公平锁 | 可能插队 | 性能好,吞吐高 | 可能饥饿 | 大多数场景 |
| 互斥锁 | 独占锁 | 保证写操作安全 | 读操作也互斥 | 写操作 |
| 共享锁 | 多个线程可共享 | 提高读并发 | 写操作需等待 | 读操作 |
3.2 深入synchronized
3.2.1 类锁、对象锁
类锁: 锁的是Class对象,全局唯一,所有实例共享一把锁;
java
public class ClassLockDemo {
// 锁住Class对象
public static synchronized void staticMethod() {
System.out.println("类锁 - 静态方法");
}
public void method() {
// 锁住Class对象
synchronized(ClassLockDemo.class) {
System.out.println("类锁 - 代码块");
}
}
}
对象锁: 锁的是实例对象,不同实例之间的锁互不影响;
java
public class ObjectLockDemo {
// 锁住当前实例对象
public synchronized void method1() {
System.out.println("对象锁 - 实例方法");
}
// 锁住指定的对象
private final Object lock = new Object();
public void method2() {
synchronized(lock) {
System.out.println("对象锁 - 代码块");
}
}
}
3.2.2 synchronized的优化
- 锁粗化: 将多次连续的加锁解锁操作合并(扩大锁的范围,减少锁的获取/释放次数)
- 锁消除: JIT编译器检测到不可能存在竞争的锁,直接消除
- 适应性自旋: 根据历史经验调整自旋次数
java
// 锁粗化示例 - 优化前
for(int i = 0; i < 100; i++) {
synchronized(this) {
// 操作
}
}
// 锁粗化 - 优化后
synchronized(this) {
for(int i = 0; i < 100; i++) {
// 操作
}
}
3.2.3 synchronized实现原理
字节码层面
class
public class SyncPrinciple {
public void method() {
synchronized(this) {
System.out.println("sync block");
}
}
}
编译后的字节码
monitorenter
// 业务代码
monitorexit // 退出同步块
monitorexit // 异常退出路径
JVM层面
- monitorenter: 尝试获取对象监视器(Monitor)
- monitorexit: 释放对象监视器
- 每个对象都有一个Monitor与之关联
3.2.4 synchronized的锁升级
synchronized在JDK 1.6后引入了锁升级机制,从低到高依次是:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁升级过程
java
public class LockUpgradeDemo {
private static Object lock = new Object();
public static void main(String[] args) {
// 1. 无锁状态:初始状态
// 2. 偏向锁:同一个线程重复获取锁
for(int i = 0; i < 100; i++) {
synchronized(lock) {
// 同一个线程,使用偏向锁
}
}
// 3. 轻量级锁:不同线程交替执行
new Thread(() -> {
synchronized(lock) {
// 线程A获取锁,升级为轻量级锁
}
}).start();
new Thread(() -> {
synchronized(lock) {
// 线程B获取锁时发现已被持有,继续CAS自旋
}
}).start();
// 4. 重量级锁:竞争激烈,自旋失败
for(int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized(lock) {
// 多个线程同时竞争,升级为重量级锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
锁升级条件
| 锁状态 | 触发条件 | 特点 | 适用场景 |
|---|---|---|---|
| 无锁 | 初始状态 | 无锁竞争 | 对象刚创建 |
| 偏向锁 | 同一个线程重复获取锁 | 记录线程ID,无需CAS操作,性能最好 | 单线程访问 |
| 轻量级锁 | 不同线程交替执行,CAS自旋成功 | 通过CAS自旋获取锁,无阻塞,用户态操作 | 多线程交替,竞争不激烈 |
| 重量级锁 | 多线程同时竞争,CAS自旋失败超过阈值 | 阻塞等待,操作系统调度,涉及用户态到内核态切换 | 多线程激烈竞争,锁持有时间长 |
3.2.5 重量锁底层ObjectMonitor
ObjectMonitor结构
text
// HotSpot源码中的ObjectMonitor结构
ObjectMonitor() {
_header = NULL; // 对象头
_count = 0; // 计数器
_waiters = 0; // 等待线程数
_recursions = 0; // 重入次数
_object = NULL; // 关联的对象
_owner = NULL; // 当前持有锁的线程
_WaitSet = NULL; // 等待队列(调用wait的线程)
_EntryList = NULL; // 入口队列(等待获取锁的线程)
}
ObjectMonitor工作机制
java
public class MonitorDemo {
private final Object lock = new Object();
public void monitorMechanism() {
synchronized(lock) { // 1. 尝试获取Monitor
try {
lock.wait(); // 2. 线程进入_WaitSet
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify(); // 3. 唤醒_WaitSet中的线程
} // 4. 释放Monitor
}
}
核心机制详解
-
锁竞争流程
线程尝试获取锁
↓
CAS尝试获取Monitor
↓
成功? → 是 → 设置_owner为当前线程
↓否
进入_EntryList排队
↓
通过操作系统mutex阻塞
↓
被唤醒后继续竞争 -
等待/通知机制
java
// wait()操作
void ObjectMonitor::wait() {
// 1. 将当前线程加入_WaitSet
// 2. 释放锁(_owner = NULL)
// 3. 线程阻塞等待
}
// notify()操作
void ObjectMonitor::notify() {
// 1. 从_WaitSet中取出一个线程
// 2. 移动到_EntryList
// 3. 等待获取锁
}
- 重入锁实现
java
// 重入锁实现原理
if (_owner == current_thread) {
_recursions++; // 重入计数器+1
return; // 直接进入,无需再次竞争
}
3.3 深入ReentrantLock
3.3.1 ReentrantLock和synchronized的区别
两者都是 Java 中实现锁机制的可重入锁,但存在本质区别:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 底层实现 | JVM 级别的关键字,通过监视器(Monitor)实现 | JDK 级别的 API,通过 AQS 实现 |
| 锁获取方式 | 隐式获取,由 JVM 自动释放 | 显式获取 (lock()),必须显式释放 (unlock()) |
| 灵活性 | 低,不支持中断、超时 | 高,支持 tryLock、lockInterruptibly、超时等待 |
| 公平性 | 非公平锁 | 默认非公平,构造函数支持公平锁 |
| 条件变量 | 只有单一的 wait/notify 队列 |
支持多个 Condition 对象,实现更细粒度的等待/唤醒 |
| 性能 | 早期性能较差,经过优化后(偏向锁、轻量级锁)差距缩小 | 在高竞争场景下仍保持较高吞吐量 |
3.3.2 AQS概述
AbstractQueuedSynchronizer (AQS) 是 JUC 包中锁和同步器的基石。
核心思想
- 状态管理: 通过
volatile int state表示同步状态(如锁的重入次数); - 队列管理: 内置CLH锁队列(FIFO双向链表) ,管理获取同步状态失败的线程;
- 资源共享方式:
独占模式: 只有一个线程能持有资源(如 ReentrantLock);
共享模式: 多个线程可同时持有资源(如 CountDownLatch、Semaphore);
关键数据结构
java
static final class Node {
volatile int waitStatus; // 等待状态:CANCELLED(1), SIGNAL(-1), CONDITION(-2), PROPAGATE(-3)
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 当前节点对应的线程
Node nextWaiter; // 用于 Condition 队列的链接
}
3.3.3 加锁流程源码剖析
以非公平锁为例,剖析 lock() 方法的核心流程。
- 入口:NonfairSync.lock()
java
final void lock() {
// 上来先尝试 CAS 抢一次锁(非公平性的体现)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢失败则进入 AQS 流程
}
- acquire(int arg) 模板方法
java
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 再次尝试获取锁(可能处理重入)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入队列并阻塞
selfInterrupt();
}
- tryAcquire:非公平实现
java
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 重点:这里依然会 CAS 抢锁,可能被新来的线程插队
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc); // 重入计数
return true;
}
return false;
}
- addWaiter:将当前线程包装为 Node 加入队列
快速尝试入队尾部,失败则进入 enq 自旋入队。
enq 中会初始化空头节点(哨兵节点)。 - acquireQueued:节点在队列中阻塞
java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果前驱是头节点,则再尝试一次获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // 帮助 GC
failed = false;
return interrupted;
}
// 检查是否需要阻塞(park)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire:维护前驱节点的 waitStatus 为 SIGNAL,表示需要唤醒后继节点。
parkAndCheckInterrupt:调用 LockSupport.park() 阻塞线程。
3.3.4 释放锁流程源码剖析
- unlock() → release(int arg)
java
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空且 waitStatus 不为 0(说明有后继节点需要唤醒)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- tryRelease:ReentrantLock 实现
java
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
只有当 state 减到 0 时,才真正释放锁(完全释放)。
- unparkSuccessor:唤醒后继节点
- 找到头节点后第一个有效节点(waitStatus <= 0),通过 LockSupport.unpark() 唤醒该线程。
- 被唤醒的线程会从 parkAndCheckInterrupt() 返回,继续在 acquireQueued 中自旋尝试获取锁。
3.3.5 AQS中常见的问题
Q1:AQS 的队列为什么是双向链表?
单向性:从 head 向后遍历,用于唤醒后继节点。
逆向性:在节点取消(cancelAcquire)时,需要从后向前遍历跳过已取消的节点,因此需要 prev 指针。
Q2:公平锁与非公平锁的区别在哪里?
公平锁:tryAcquire 中增加 hasQueuedPredecessors() 判断,如果队列中有等待线程,则不会抢锁。
非公平锁:在 lock() 和 tryAcquire 中多次尝试 CAS 抢占,可能导致线程饥饿。
Q3:waitStatus 中的 SIGNAL 作用是什么?
表示当前节点的后继节点被阻塞(或被即将阻塞),当前节点释放锁或取消时,必须唤醒后继节点。
这是确保线程安全唤醒的关键状态。
Q4:AQS 如何避免虚假唤醒?
AQS 使用 for(;😉 循环自旋,每次被唤醒后重新检查前驱是否为头节点,并再次尝试获取锁,符合 循环检查条件 的范式。
3.3.6 ConditionObject
ConditionObject 是 AQS 的内部类,实现了 Condition 接口,用于实现等待/通知 机制。
核心结构
java
public class ConditionObject implements Condition {
private Node firstWaiter; // 条件队列的头节点
private Node lastWaiter; // 条件队列的尾节点
}
- await() 流程
- 添加到条件队列: 将当前线程封装为 Node.CONDITION 节点,插入条件队列。
- 释放锁: 调用 fullyRelease,释放当前线程持有的锁(并唤醒同步队列中的后继节点)。
- 阻塞等待: 进入 while (!isOnSyncQueue(node)) 循环,调用 LockSupport.park 阻塞。
- 重新获取锁: 被 signal 唤醒后,节点被转移到同步队列,通过 acquireQueued 重新竞争锁。
- signal() 流程
java
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
- doSignal:将条件队列的头节点转移到同步队列(通过 enq)。
- 转移前会清除 CONDITION 状态,并设置前驱节点的 waitStatus 为 SIGNAL。
3.4 深入ReentrantReadWriteLock
3.4.1 为什么要出现读写锁
问题背景
传统的互斥锁(如ReentrantLock)虽然保证了线程安全,但存在性能问题:
- 读操作之间完全互斥,即使只是读取数据也要排队
- 读多写少的场景下,并发性能很差
java
// 传统互斥锁的问题
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 多个线程同时读操作也要串行执行
readData();
} finally {
lock.unlock();
}
// 读写锁的优势
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作可以并发执行
rwLock.readLock().lock();
try {
readData(); // 多个线程可以同时执行
} finally {
rwLock.readLock().unlock();
}
核心原则
- 读-读:不互斥,可以并发
- 读-写:互斥,不能并发
- 写-写:互斥,不能并发
3.4.2 读写锁的实现原理
状态位设计
ReentrantReadWriteLock巧妙地将一个int类型的state拆分成两部分:
java
// 在AbstractQueuedSynchronizer中
// state的高16位表示读锁的持有次数
// state的低16位表示写锁的持有次数
// 例如:state = 0x00010002
// 高16位:0x0001 = 1 (读锁计数)
// 低16位:0x0002 = 2 (写锁重入次数)
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 65536
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 65535
// 获取读锁计数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写锁计数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
锁降级
java
class DataContainer {
private volatile Object data;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void processData() {
// 先获取读锁
rwLock.readLock().lock();
if (data == null) {
// 读锁释放前获取写锁(锁降级)
rwLock.readLock().unlock();
rwLock.writeLock().lock();
try {
if (data == null) {
data = loadData(); // 双重检查
}
// 写锁释放前获取读锁
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock();
}
}
try {
useData(data); // 使用数据
} finally {
rwLock.readLock().unlock();
}
}
}
3.4.3 写锁分析
写锁的分析
java
// 写锁的tryAcquire实现
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 存在锁
if (w == 0 || current != getExclusiveOwnerThread()) {
// 存在读锁 或者 当前线程不是持有写锁的线程
return false;
}
// 写锁重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
// 无锁状态,尝试获取写锁
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
写锁的特点
- 独占锁: 同一时刻只有一个线程能获得
- 可重入: 支持重入,重入次数记录在低16位
- 支持条件变量: 可以创建Condition对象
3.4.4 读锁分析
读锁的获取流程
java
// 读锁的tryAcquireShared实现
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果有写锁且持有者不是当前线程,获取失败
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
// 尝试获取读锁
if (!readerShouldBlock() && r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 记录读锁持有信息
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 使用ThreadLocal记录重入次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
rh.count++;
}
return 1;
}
// 完整获取逻辑
return fullTryAcquireShared(current);
}
读锁的特点
- 共享锁: 多个线程可以同时持有
- 可重入: 支持重入,重入次数记录在高16位
- 不支持条件变量: 读锁不能创建Condition
3.4.5 死锁问题
常见的死锁场景
- 场景1:锁升级导致死锁
java
public class LockUpgradeDeadlock {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void deadlockScenario() {
// 线程A持有读锁
rwLock.readLock().lock();
try {
// 尝试获取写锁 - 死锁!
// 读锁未释放,写锁永远获取不到
rwLock.writeLock().lock();
try {
updateData();
} finally {
rwLock.writeLock().unlock();
}
} finally {
rwLock.readLock().unlock();
}
}
}
解决方案
java
// 正确的锁降级,不要尝试锁升级
public void correctWay() {
rwLock.writeLock().lock();
try {
updateData();
// 写锁降级为读锁
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock();
}
try {
readData();
} finally {
rwLock.readLock().unlock();
}
}
- 场景2:循环依赖死锁
java
public class CircularDependencyDeadlock {
private final ReentrantReadWriteLock lock1 = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock lock2 = new ReentrantReadWriteLock();
public void method1() {
lock1.writeLock().lock();
try {
Thread.sleep(100);
lock2.readLock().lock(); // 等待lock2
try {
doSomething();
} finally {
lock2.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.writeLock().unlock();
}
}
public void method2() {
lock2.writeLock().lock();
try {
Thread.sleep(100);
lock1.readLock().lock(); // 等待lock1,形成死锁
try {
doSomething();
} finally {
lock1.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.writeLock().unlock();
}
}
}
解决方案
java
public class CircularDependencyDeadlock {
private final ReentrantReadWriteLock lock1 = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock lock2 = new ReentrantReadWriteLock();
public void method1() {
lock1.writeLock().lock();
try {
Thread.sleep(100);
lock2.readLock().lock(); // 等待lock2
try {
doSomething();
} finally {
lock2.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.writeLock().unlock();
}
}
public void method2() {
lock2.writeLock().lock();
try {
Thread.sleep(100);
lock1.readLock().lock(); // 等待lock1,形成死锁
try {
doSomething();
} finally {
lock1.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.writeLock().unlock();
}
}
}
- 场景3:线程饥饿问题
java
public class WriteStarvation {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读线程过多可能导致写线程饥饿
public void readHeavy() {
// 大量读线程
for (int i = 0; i < 100; i++) {
new Thread(() -> {
while (true) {
rwLock.readLock().lock();
try {
Thread.sleep(10); // 读操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}).start();
}
// 写线程可能永远获取不到锁
new Thread(() -> {
while (true) {
rwLock.writeLock().lock();
try {
System.out.println("写操作执行");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
}).start();
}
}
-
避免死锁的最佳实践
-
避免锁升级: 不要在读锁未释放时获取写锁
-
统一锁顺序: 多个锁时,所有线程以相同的顺序获取锁
-
使用tryLock:
java
public boolean tryLockBoth(ReentrantReadWriteLock lock1, ReentrantReadWriteLock lock2) {
if (lock1.writeLock().tryLock()) {
try {
if (lock2.writeLock().tryLock()) {
try {
// 成功获取两个锁
return true;
} finally {
lock2.writeLock().unlock();
}
}
} finally {
lock1.writeLock().unlock();
}
}
return false;
}
-
考虑使用StampedLock: Java 8引入的StampedLock提供了乐观读锁,可以避免某些死锁场景
-
设置超时时间:
java
public boolean tryLockWithTimeout(ReentrantReadWriteLock lock) {
try {
return lock.writeLock().tryLock(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return false;
}
}
四、阻塞队列
4.1 基本概念
阻塞队列是支持两个附加操作的队列:
| 操作 | 普通队列 | 阻塞队列 |
|---|---|---|
| 插入 | 满时抛异常/返回false | 满时阻塞等待 |
| 移除 | 空时抛异常/返回null | 空时阻塞等待 |
| 核心方法分类 |
java
// 抛出异常
add(e) / remove() / element()
// 返回特殊值(不阻塞)
offer(e) / poll() / peek()
// 阻塞等待(核心特性)
put(e) / take() // 无限等待
offer(e, time, unit) / poll(time, unit) // 超时等待
阻塞队列的核心价值
生产者-消费者模式的完美实现:
┌─────────┐ ┌─────────────┐ ┌─────────┐
│ Producer│────→│ BlockingQueue │────→│ Consumer│
│ (生产) │ │ (缓冲池) │ │ (消费) │
└─────────┘ └─────────────┘ └─────────┘
队列满时生产者阻塞 队列空时消费者阻塞
4.2 ArrayBlockingQueue
核心特性
| 特性 | 说明 |
|---|---|
| 底层结构 | 循环数组(Object[]) |
| 有界队列 | 必须指定容量,固定大小 |
| 锁机制 | 单锁 (ReentrantLock) |
| 公平性 | 可选择公平/非公平锁 |
| 内存 | 预先分配,内存紧凑 |
源码结构
java
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, Serializable {
// 核心成员
final Object[] items; // 循环数组存储元素
int takeIndex; // 取元素索引
int putIndex; // 放元素索引
int count; // 当前元素数量
final ReentrantLock lock; // 单锁控制所有操作
private final Condition notEmpty; // 等待队列非空
private final Condition notFull; // 等待队列非满
}
工作原理图解
初始状态(capacity=5):
[_, _, _, _, _]
t↑ p↑
放入3个元素后:
[A, B, C, _, _]
t↑ p↑
取出1个元素后(循环特性):
[_, B, C, _, _]
t↑ p↑
继续放满后:
[_, B, C, D, E]
t↑ p↑
再取2个(循环到头部):
[_, _, _, D, E]
t↑ p↑
关键代码解析
java
// 插入元素(阻塞)
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 可中断获取锁
try {
while (count == items.length) // 队列满时等待
notFull.await();
enqueue(e); // 入队
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
// 取出元素(阻塞)
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) // 队列空时等待
notEmpty.await();
return dequeue(); // 出队
} finally {
lock.unlock();
}
}
使用场景
java
// 创建:公平锁模式(按等待时间顺序获取锁)
ArrayBlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000, true);
// 典型场景:固定大小的线程池任务队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, 8, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100) // 有界队列防止OOM
);
4.3 LinkedBlockingQueue
核心特征
| 特性 | 说明 |
|---|---|
| 底层结构 | 单向链表(Node<E>) |
| 可选有界/无界 | 默认Integer.MAX_VALUE(无界) |
| 锁机制 | 双锁分离 (putLock + takeLock) |
| 并发性 | 读写可并行,吞吐量更高 |
| 内存 | 动态分配,节点有额外开销 |
源码结构
java
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, Serializable {
// 链表节点
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 双锁分离设计!
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
private final int capacity; // 容量,默认Integer.MAX_VALUE
private final AtomicInteger count = new AtomicInteger(); // 原子计数
}
双锁分离图解
传统单锁(ArrayBlockingQueue):
生产者 ──→ [ 锁 ] ←── 消费者
读写互斥
双锁分离(LinkedBlockingQueue):
生产者 ──→ [putLock]
\
[队列] ←── 读写可并行!
/
消费者 ──→ [takeLock]
关键代码图解
java
// 插入元素 - 只加putLock
public void put(E e) throws InterruptedException {
if (count.get() == capacity) { // 先检查,避免加锁
final ReentrantLock putLock = this.putLock;
putLock.lockInterruptibly();
try {
while (count.get() == capacity)
notFull.await();
enqueue(new Node<E>(e));
c = count.getAndIncrement(); // 原子操作
if (c + 1 < capacity)
notFull.signal(); // 唤醒其他生产者
} finally {
putLock.unlock();
}
}
// 插入成功后,可能需要唤醒消费者
if (c == 0)
signalNotEmpty(); // 加takeLock唤醒
}
// 取出元素 - 只加takeLock
public E take() throws InterruptedException {
// 对称实现,加takeLock操作
}
Array vs Linked 对比
| 对比项 | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| 内存 | 预分配,紧凑 | 动态分配,节点开销大 |
| 锁竞争 | 单锁,竞争大 | 双锁,读写并行 |
| 吞吐量 | 较低 | 更高(约2倍) |
| GC影响 | 小(无新对象) | 大(频繁创建Node) |
| 公平性 | 支持 | 不支持 |
| 使用建议 | 固定容量、内存敏感 | 高并发、需要动态扩容 |
4.4 PriorityBlockingQueue概念
核心特性
| 特性 | 说明 |
|---|---|
| 底层结构 | 堆(完全二叉树,数组实现) |
| 排序方式 | 自然顺序或Comparator |
| 无界队列 | 动态扩容(64→2倍增长) |
| 单锁 | ReentrantLock |
| 相等处理 | 不保证FIFO(堆不稳定) |
| 堆结构图解 |
逻辑结构(最大堆):
100
/ \
80 90
/ \ /
70 60 50
物理存储(数组):
[100, 80, 90, 70, 60, 50, ...]
0 1 2 3 4 5
核心操作复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
put/offer |
O(log n) | 插入后上浮(siftUp) |
take/poll |
O(log n) | 移除后下沉(siftDown) |
peek |
O(1) | 直接取数组[0] |
| 使用场景 |
java
// 任务优先级队列
PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>(
11, // 初始容量
Comparator.comparing(Task::getPriority).reversed() // 高优先级先执行
);
// 自定义元素必须实现Comparable
class Task implements Comparable<Task> {
int priority;
String name;
@Override
public int compareTo(Task o) {
return Integer.compare(this.priority, o.priority);
}
}
注意事项
java
// ⚠️ 重要:迭代器不保证顺序!
for (Task t : queue) { // 无序遍历
System.out.println(t);
}
// 正确获取有序元素
while (!queue.isEmpty()) {
Task t = queue.take(); // 按优先级取出
}
4.5 DelayQueue
核心特性
| 特性 | 说明 |
|---|---|
| 元素要求 | 必须实现Delayed接口 |
| 排序依据 | 按剩余延迟时间排序 |
| 获取特性 | 只有到期的元素才能被取出 |
| Leader-Follower | 优化多线程等待效率 |
| 应用 | 定时任务、缓存过期、重试机制 |
| Delayed接口 |
java
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit); // 剩余延迟时间
// 继承Comparable用于队列内排序
}
工作原理
队列内部状态:
[任务A(还剩5s), 任务B(还剩10s), 任务C(还剩30s)]
↑
take()只能获取到期的元素
线程获取流程:
1. 检查队首元素是否到期(delay <= 0)
2. 未到期:Leader线程等待剩余时间,其他线程无限等待
3. 到期后:取出元素,唤醒Follower竞争新Leader
经典实现:缓存过期
java
public class DelayedCache<K, V> {
private final Map<K, V> cache = new ConcurrentHashMap<>();
private final DelayQueue<DelayedItem<K>> delayQueue = new DelayQueue<>();
// 带过期时间的缓存项
private static class DelayedItem<K> implements Delayed {
private final K key;
private final long expireTime; // 到期时间戳
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((DelayedItem<?>)o).expireTime);
}
}
public void put(K key, V value, long timeout, TimeUnit unit) {
cache.put(key, value);
DelayedItem<K> item = new DelayedItem<>(key, unit.toNanos(timeout));
delayQueue.offer(item);
}
// 后台清理线程
public void startCleanupThread() {
Thread t = new Thread(() -> {
while (!Thread.interrupted()) {
try {
DelayedItem<K> item = delayQueue.take(); // 阻塞等待到期
cache.remove(item.key); // 自动清理
System.out.println("Expired: " + item.key);
} catch (InterruptedException e) {
break;
}
}
});
t.setDaemon(true);
t.start();
}
}
使用场景
| 场景 | 实现方式 |
|---|---|
| 定时任务调度 | ScheduledThreadPoolExecutor内部使用 |
| 订单超时取消 | 订单创建时放入DelayQueue,到期自动取消 |
| RPC重试退避 | 失败请求延迟后放入,实现指数退避 |
| 连接池空闲检测 | 空闲连接延迟回收 |
4.6 SynchronousQueue
核心特性
| 特性 | 说明 |
|---|---|
| 容量 | 零容量! 不存储任何元素 |
| 模式 | 直接传递(handoff) |
| 公平性 | 公平/非公平模式可选 |
| 实现 | 栈(非公平)或队列(公平) |
| 吞吐量 | 极高,无缓存开销 |
| 核心原理 |
SynchronousQueue = 匹配机制,不是真正的"队列"
生产者put() ──┐
├──→ 直接配对成功 → 同时完成
消费者take() ─┘
│
└──→ 未配对成功 → 阻塞等待
比喻:像面对面交易,没有柜台(缓冲区)
两种实现方式
java
// 1. 非公平模式(默认)- 栈实现(LIFO)
SynchronousQueue<Integer> stackQueue = new SynchronousQueue<>(false);
// 特点:后到的线程先匹配,吞吐量高,可能饥饿
// 2. 公平模式 - 队列实现(FIFO)
SynchronousQueue<Integer> fairQueue = new SynchronousQueue<>(true);
// 特点:先到先服务,避免饥饿,吞吐量略低
五、高频面试题
| 类别 | 问题 | 核心答案 | 关键考点 |
|---|---|---|---|
| 线程基础 | 进程与线程的区别? | 进程是资源分配单位,线程是CPU调度单位;线程共享进程资源,切换开销更小 | 资源分配 vs 调度单位 |
| 多线程的局限? | 上下文切换开销、任务拆分难度、线程安全问题(数据不一致、死锁) | 开销与风险权衡 | |
| 并发 vs 并行? | 并发是逻辑同时(单核交替),并行是物理同时(多核同时) | 逻辑同时 vs 物理同时 | |
| 创建线程的4种方式? | ①继承Thread ②实现Runnable ③实现Callable ④线程池(推荐) | 线程池是生产环境最佳实践 | |
| start() vs run()? | start()启动新线程异步执行;run()是普通方法调用,同步执行 | 是否创建新线程 | |
| 线程的6种状态? | New→Runnable→Running→Blocked/Waiting/Timed Waiting→Terminated | 状态转换条件 | |
| 线程结束的3种推荐方式? | ①自然结束 ②volatile标志位 ③interrupt()中断机制 | stop()/suspend()已废弃 | |
| 同步/异步 | 同步 vs 异步? | 同步:调用者主动等待;异步:被调用者回调通知 | 结果通知机制 |
| 阻塞 vs 非阻塞? | 阻塞:线程挂起释放CPU;非阻塞:立即返回,可能轮询 | 线程状态差异 | |
| 四种组合模式? | 同步阻塞(BIO)、同步非阻塞(轮询)、异步阻塞(矛盾)、异步非阻塞(Netty/AIO) | 异步非阻塞效率最高 | |
| epoll是异步IO吗? | 否,epoll是同步IO多路复用,数据拷贝仍需用户线程 | 常见误区 | |
| 并发三大特性 | 什么是原子性?如何保证? | 操作不可中断;synchronized、Lock、AtomicXXX(CAS) | volatile不能保证原子性 |
| 什么是可见性?如何保证? | 修改对其他线程立即可见;volatile、synchronized、Lock、final | JMM工作内存模型 | |
| 什么是有序性?如何保证? | 禁止指令重排序;volatile(内存屏障)、synchronized、happens-before | as-if-serial与happens-before规则 | |
| volatile的作用与局限? | ✅可见性+有序性;❌不能保证原子性(如i++) | 双重检查锁单例模式 | |
| 锁机制 | synchronized vs ReentrantLock? | synchronized:JVM关键字,自动释放;ReentrantLock:API级,支持中断/超时/多Condition | 灵活性与功能对比 |
| synchronized锁升级过程? | 无锁→偏向锁(同线程)→轻量级锁(CAS自旋)→重量级锁(内核阻塞) | 优化路径与触发条件 | |
| 什么是可重入锁? | 同线程可多次获取同一把锁,计数器记录重入次数;避免递归/父子方法死锁 | ReentrantLock/synchronized都是 | |
| 乐观锁 vs 悲观锁? | 乐观:不加锁,更新检查版本/CAS;悲观:每次加锁;读多用乐观,写多用悲观 | 适用场景选择 | |
| 公平锁 vs 非公平锁? | 公平:FIFO,无饥饿但性能低;非公平:可能插队,性能高(默认推荐) | 吞吐量 vs 公平性权衡 | |
| 读写锁特点? | 读-读并发,读-写/写-写互斥;支持锁降级(写→读),不支持锁升级 | ReentrantReadWriteLock | |
| AQS核心思想? | state(状态)+ CLH队列(FIFO等待队列)+ 独占/共享模式 | AbstractQueuedSynchronizer | |
| 什么是死锁?如何避免? | 四条件:互斥、请求保持、不剥夺、循环等待;避免:统一顺序、tryLock超时、避免锁升级 | 死锁检测与预防 | |
| 阻塞队列 | 核心方法分类? | 抛出异常(add/remove)、特殊值(offer/poll)、阻塞(put/take)、超时(offer/poll+time) | 根据场景选择方法 |
| Array vs LinkedBlockingQueue? | Array:单锁、有界、预分配;Linked:双锁分离、默认无界、动态分配 | 吞吐量与内存权衡 | |
| SynchronousQueue特点? | 零容量,直接传递(handoff);公平(队列)/非公平(栈)模式 | 极高吞吐量场景 | |
| DelayQueue应用场景? | 元素需实现Delayed,到期才能取出;用于:定时任务、订单超时、缓存过期 | Leader-Follower优化等待 | |
| 综合设计 | 高并发秒杀系统(线程安全)? | 乐观锁/CAS扣库存、限流削峰、异步消息队列、读写分离、本地缓存 | 多层防护策略 |
| ThreadLocal原理与内存泄漏? | ThreadLocalMap弱引用key,但value强引用;必须finally中remove() | 线程池复用场景风险 | |
| CompletableFuture优势? | 支持回调链式组合、异常处理、取消超时;替代Future阻塞获取 | 函数式异步编程 |
快速记忆口诀
| 场景 | 口诀 |
|---|---|
| 锁升级 | 无偏轻重重(无锁→偏向→轻量级→重量级) |
| 并发特性 | 原可序(原子性、可见性、有序性) |
| volatile | 可序不可原(可见性+有序性,无原子性) |
| 阻塞队列选择 | Arr有界单锁,Link无界双锁,Sync零容直传,Delay到期才取 |
| 死锁预防 | 统顺超(统一顺序、tryLock超时、避免锁升级) |