Java 多线程编程
一、线程基础概念
1.1 进程与线程的区别
jin cheng
进程:操作系统资源分配的基本单位,有独立的内存空间
线程:CPU调度的基本单位,共享进程的内存空间
多线程:一个进程内同时运行多个线程,并发 / 并行执行任务。
一个进程包含多个线程,Java程序至少有一个主线程(main线程)
核心区别:
并发:单核 CPU,线程交替快速切换执行(看起来同时跑)。
并行:多核 CPU,线程真正同时执行。
1.2 Java线程的优势
1. 充分利用多核CPU
2. 提高程序响应性
3. 更高效的资源利用
1.3 多线程在工作中的使用
1)接口响应优化:批量发送短信 / 邮件、批量导出报表。
2)异步处理:日志记录、消息推送、数据同步。
3)高并发:秒杀、订单处理、服务器同时处理上千请求。
4)资源利用:充分利用多核 CPU,避免单核空闲。
二、线程的创建与启动
2.1 继承Thread类
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的业务逻辑
System.out.println("线程:" + Thread.currentThread().getName() + " 运行中");
}
}
// 测试
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.setName("线程-1");
t1.start(); // 启动线程(必须调用 start(),不能直接调用 run())
}
}
实际工作注意事项:
-
线程类命名规范:
XXXThread或XXXWorker -
避免在构造函数中启动线程
-
优先实现Runnable,避免单继承限制
-
直接调用 run() 是普通方法调用,不会开启新线程!
-
继承 Thread 有局限:Java 单继承,无法再继承其他类。
2.2 实现Runnable接口(推荐)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程:" + Thread.currentThread().getName() + " 运行中");
}
}
// 测试
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "线程-2");
t1.start();
}
}
优点:避免单继承局限,适合多个线程共享同一个任务。
2.3 实现 Callable 接口(有返回值 + 抛异常)
前两种都无返回值、不能抛检查异常,Callable 可以解决。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 模拟计算
Thread.sleep(1000);
return 100 + 200;
}
}
// 测试
public class Test {
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(task, "线程-3");
t1.start();
// 获取返回值(阻塞等待线程执行完)
Integer result = task.get();
System.out.println("结果:" + result);
}
}
// 实际工作场景:需要返回结果的异步任务
public class MyCallable implements Callable<String> {
private int taskId;
public MyCallable(int taskId) {
this.taskId = taskId;
}
@Override
public String call() throws Exception {
// 实际业务:如查询数据库、调用外部接口
Thread.sleep(1000);
return "任务#" + taskId + "完成";
}
}
// 配合ExecutorService使用
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> future = executor.submit(new MyCallable(1));
String result = future.get(); // 阻塞获取结果
工作场景:需要异步计算结果、批量任务汇总结果
三、线程状态与生命周期
3.1 线程的6种状态
Java 线程只有 6 种状态,在 Thread.State 枚举中定义:
1)NEW(新建):创建了 Thread 对象,还没调用 start()。
2)RUNNABLE(就绪 / 运行):调用了 start(),等待 CPU 调度 或 正在执行。
3)BLOCKED(阻塞):等待锁(synchronized),抢不到锁就阻塞。
4)WAITING(无限等待):调用 wait()/join()/LockSupport.park(),必须被其他线程唤醒。
5)TIMED_WAITING(限时等待):调用 sleep(ms)/wait(ms),时间到自动唤醒。
6)TERMINATED(终止):线程执行完毕 / 异常退出。
状态流转细节:
1)NEW → RUNNABLE:调用 start()
2)RUNNABLE → BLOCKED:等待 synchronized 锁
3)RUNNABLE → WAITING:无参 wait/join
4)RUNNABLE → TIMED_WAITING:带时间的 sleep/wait/join
5)任何状态(除 NEW)→ TERMINATED:执行结束 / 异常
线程转换详情实例代码:
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
// 进入TIMED_WAITING
Thread.sleep(1000);
synchronized (ThreadStateDemo.class) {
// 持有锁
ThreadStateDemo.class.wait(); // 进入WAITING
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("创建后状态:" + thread.getState()); // NEW
thread.start();
Thread.sleep(100);
System.out.println("启动后状态:" + thread.getState()); // TIMED_WAITING
Thread.sleep(2000);
System.out.println("sleep后状态:" + thread.getState()); // WAITING
thread.interrupt();
Thread.sleep(100);
System.out.println("终止前状态:" + thread.getState()); // TERMINATED
}
}
四、线程的核心方法
4.1 sleep () vs wait ()(高频面试 + 工作必用)
| 方法 | 所属类 | 锁资源 | 使用场景 |
|---|---|---|---|
| sleep(long) | Thread | 不释放锁 | 暂停当前线程,延时执行 |
| wait() | Object | 释放锁 | 线程间通信、等待唤醒 |
关键细节:
sleep 是线程休眠,时间到自动恢复,不依赖唤醒。
wait 必须在同步代码块(synchronized) 中使用,必须用 notify/notifyAll 唤醒。
4.2 start() vs run()
start():开启新线程,JVM 自动调用 run()。
run():只是普通方法,不会开启线程。
4.3 join ():等待线程执行完成
作用:让调用线程等待该线程执行完毕再继续。
工作场景:主线程需要等待子线程计算完成再汇总。
Thread t1 = new Thread(() -> {
try { Thread.sleep(1000); } catch (Exception e) {}
});
t1.start();
t1.join(); // 主线程阻塞,等待 t1 执行完
System.out.println("t1 执行完毕");
4.4 yield ():让出 CPU 执行权
作用:当前线程让出 CPU,回到就绪状态,不释放锁。
工作中极少用,不稳定。
4.5 interrupt ():中断线程
不是强制停止线程,只是设置中断标志位。
正确用法:
Thread t1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行业务
}
});
t1.start();
t1.interrupt(); // 设置中断标志,优雅停止
五、线程安全与同步
5.1 竞态条件Race Condition)
// 实际工作场景:库存扣减、计数器等
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 非原子操作:读取-修改-写入
}
public int getCount() {
return count;
}
}
// 并发执行时会导致数据不一致
5.2 synchronized关键字
synchronized(内置锁)
作用:保证原子性、可见性、有序性,同一时间只有一个线程执行。
用法:
1、修饰实例方法:锁当前对象
2、修饰静态方法:锁当前类
3、修饰代码块:锁指定对象(推荐,粒度更小)
// 1. 同步方法
public class SafeCounter {
private int count = 0;
// 实例方法锁:锁当前对象
public synchronized void increment() {
count++;
}
// 静态方法锁:锁类对象
public static synchronized void staticMethod() {
// ...
}
}
// 2. 同步代码块(更细粒度控制)
public class OptimizedCounter {
private int count = 0;
private final Object lock = new Object(); // 专门用于锁的对象
public void increment() {
// 只同步必要的代码
synchronized (lock) {
count++;
}
// 其他非线程安全操作
}
}
实际工作注意事项:
-
避免在synchronized块中调用耗时操作
-
锁的粒度要尽可能小
-
避免锁嵌套,防止死锁
-
使用专门的锁对象,不要锁字符串常量或基础类型
5.3 Lock 接口(JDK 1.5+ 显式锁)
比 synchronized 更灵活:可尝试锁、可超时、可公平锁。
常用实现:ReentrantLock
Lock lock = new ReentrantLock();
public void sale() {
lock.lock(); // 加锁
try {
if (count > 0) {
count--;
}
} finally {
lock.unlock(); // 必须在 finally 释放锁!
}
}
工作原则:
简单场景用 synchronized(简洁、JVM 优化好)。
需要灵活控制(尝试获取锁、超时)用 Lock。
5.4 解决方案 3:ThreadLocal(线程本地变量)
作用:每个线程独立拥有一份变量副本,互不干扰,无锁安全。
工作场景:用户登录信息、数据库连接、事务上下文。
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 每个线程操作自己的值,绝对安全
5.5 volatile关键字
1. volatile 是什么?
volatile 是 Java 轻量级同步关键字,修饰成员变量、静态变量;
不能修饰局部变量、不能修饰方法、不能修饰类。
核心三大能力(精准版):
1)保证多线程可见性
2)禁止指令重排序
3)不保证原子性
4)不具备互斥性(不能锁代码块)
2. 学习 volatile 的背景 --> Java 内存模型 JMM?
JMM 内存结构
1)主内存:共享变量存在这里
2)工作内存:每个线程独有缓存(CPU 缓存、寄存器)
规则:
1)线程不能直接读写主内存
2)线程先把主内存变量拷贝到自己工作内存
3)修改只改自己工作内存,不一定立刻刷回主内存
4)别的线程也只读自己缓存,看不到别人修改 → 可见性问题
3. 普通变量存在的问题
boolean flag = true;
// 线程1
while (flag) {
// 死循环
}
// 线程2 稍后执行
flag = false;
现象:
线程 2 改了 flag=false,线程 1 永远感知不到,一直死循环。
原因:
线程 1 把 flag 缓存在工作内存,不再去主内存刷新读取。
4. volatile 第一大作用:保证可见性
1)volatile 修饰变量后的规则
读操作:每次都强制从主内存读取最新值,不使用线程本地缓存
写操作:修改后强制立刻刷新回主内存,清空本地缓存
其他线程下次读,必须从主内存拿新值
2)改造成 volatile 就正常
volatile boolean flag = true;
线程 2 修改 → 立刻刷回主内存;
线程 1 下次循环 从主内存读新值,感知到 false,退出循环。
3)可见性底层原理:内存屏障
volatile 底层插入 CPU 内存屏障(Memory Barrier):
写屏障:写完强制刷新到主存
读屏障:读之前强制从主存加载
禁止指令跨屏障乱序、强制内存刷新
5. volatile 第二大作用:禁止指令重排序
1)什么是指令重排
CPU / JVM 为了执行效率,会打乱代码书写顺序,不改变单线程结果,但多线程会出 Bug。
比如:
正常的代码执行顺序:分配内存-->初始化对象-->引用赋值
CPU 可能重排成:分配内存 -->引用赋值 -->初始化对象
2)经典坑:DCL 单例不加 volatile 会空指针
// 不加 volatile 有问题
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
// 这里会被指令重排
instance = new Singleton();
}
}
}
return instance;
}
new Singleton() 底层三步:
1)开辟堆内存空间
2)构造方法初始化对象
3)把内存地址赋值给 instance
重排后顺序:1 → 3 → 2
其他线程可能拿到 还没初始化完的半初始化对象,直接使用报错。
3)加 volatile 解决
private static volatile Singleton instance;
volatile 禁止前后指令重排序,保证 1→2→3 顺序执行,DCL 线程安全。
6. volatile 最大短板:不保证原子性
1)什么是原子性
一次操作不可分割,要么全成功、要么全失败,中途不能被其他线程插队。
2)典型错误:volatile + i++
volatile int num = 0; // 多线程同时执行 10000 次 num++
num++ 拆解三步:
1)从主存读 num
2)工作内存 +1
3)写回主存
volatile 只能保证每次读写可见,不能把三步合成原子操作。
多线程并发会覆盖结果,最终值小于预期。
3)怎么保证原子性
三种方案:
1)synchronized 加锁
2)Lock 可重入锁
3)原子类:AtomicInteger / AtomicLong
7. volatile 不具备的能力
1)不能保证原子性(i++ 线程不安全)
2)没有互斥锁能力
多个线程可以同时读写 volatile 变量,不能保证同一时刻只有一个线程进入临界区
3)不能修饰方法、类、局部变量
4)不能替代 synchronized
8. volatile 典型使用场景(只这两类)
场景 1:状态标记量(停止线程、开关控制)
// 线程停止标记
volatile boolean isStop = false;
public void run() {
while (!isStop) {
// 业务循环
}
}
场景 2:DCL 双重检查锁单例模式
必须用 volatile 防指令重排。
场景 3:简单读写、多线程只赋值不复合运算
只做 set / get,不做 i++、i = i+1 这种复合操作。
9. 一句话终极总结
volatile 修饰共享变量,保证可见性、禁止指令重排;
不保证原子性、没有互斥锁;
适合状态开关、DCL 单例;
复合自增、并发计算必须用 synchronized / 原子类。
10. 面试必背标准答案
volatile 是 Java 轻量级同步关键字,作用:
1)基于 JMM 内存模型,保证多线程变量可见性,强制读写主内存,不使用线程本地缓存;
2)禁止指令重排序,解决 DCL 单例重排漏洞;
3)不保证原子性,无法解决 i++ 这类复合操作线程安全问题;
4)无互斥性,仅适合状态标记、简单共享变量场景,不能替代锁。
实例演示:
// 实际场景:标志位控制、双重检查锁定
public class VolatileExample {
// volatile保证可见性和禁止指令重排
private volatile boolean running = true;
public void start() {
new Thread(() -> {
while (running) {
// 执行任务
}
}).start();
}
public void stop() {
running = false; // 其他线程立即可见
}
}
5.6 原子类
1. Java 最经典、最高效、无锁线程安全写法 ——原子类(Atomic)
AtomicInteger、AtomicReference
无锁、线程安全、高性能,靠 CAS 机制实现,比 synchronized 快很多!
2. Atomic 最厉害的特点
1)无锁(没有 synchronized)
2)非阻塞
3)CPU 指令级原子操作
4)高并发下性能远超锁
5)完全解决 volatile 不能保证原子性的问题!
3. 最常用 Atomic 工具
AtomicInteger // 原子int
AtomicLong // 原子long
AtomicBoolean // 原子布尔
AtomicReference // 原子对象引用
AtomicStampedReference // 解决ABA问题
4. 核心原理
流程:
1)拿当前值
2)计算新值
3)比较:内存里的值 == 我刚才拿的值吗?
相等 → 替换
不相等 → 重来
整个过程是 CPU 原语指令,不可分割!
5. 总结
1)Atomic 系列 = 无锁、线程安全
2)靠 CAS 实现,比锁快得多
3)AtomicInteger:解决 i++ 安全问题
4)AtomicReference:安全修改对象引用
5)高并发首选,比 synchronized 轻量 N 倍!
// 实际场景:计数器、序列生成
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicExample {
// CAS(Compare-And-Swap)实现
private AtomicInteger counter = new AtomicInteger(0);
private AtomicReference<String> latestData = new AtomicReference<>();
public void safeIncrement() {
// 非阻塞同步
counter.incrementAndGet();
}
public void updateData(String newData) {
// CAS更新
latestData.set(newData);
}
}
六、线程通信
多个线程需要协调执行,比如:生产者 - 消费者模式。
6.1 wait + notify/notifyAll(配合 synchronized)
// 生产者
synchronized (obj) {
// 仓库满了
obj.wait(); // 释放锁,等待
}
// 消费者
synchronized (obj) {
// 消费数据
obj.notifyAll(); // 唤醒所有等待线程
}
细节:
1) 必须使用synchronized同步
2)必须用 notifyAll(),少用 notify(),避免线程饥饿。
3)必须在循环中用 wait(),防止虚假唤醒。
// 生产者-消费者模式
public class MessageQueue {
private Queue<String> queue = new LinkedList<>();
private int maxSize = 10;
public synchronized void produce(String message) throws InterruptedException {
// 使用while防止虚假唤醒
while (queue.size() == maxSize) {
wait(); // 释放锁,进入等待
}
queue.offer(message);
notifyAll(); // 通知消费者
}
public synchronized String consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
String message = queue.poll();
notifyAll(); // 通知生产者
return message;
}
}
6.2 Lock + Condition(JDK 1.5+)
更灵活,可分组唤醒:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待
condition.await();
// 唤醒
condition.signalAll();
具体实例:
// 更灵活的等待/通知机制
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<T> {
private final T[] items;
private int putPtr, takePtr, count;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 等待不满
}
items[putPtr] = x;
if (++putPtr == items.length) putPtr = 0;
++count;
notEmpty.signal(); // 通知不空
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock(); // 抢盘子架
try {
// 如果空了 → 等待
while (count == 0) {
notEmpty.await(); // 等“不空”信号
}
// 拿盘子
T item = items[takePtr];
// 指针循环
if (++takePtr == items.length) takePtr = 0;
// 数量-1
--count;
// 通知厨师:有空位了,可以继续放
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
6.3 消费者和生产者概念熟知
1. 什么是生产者 - 消费者模型?
生产者:生产数据的线程
消费者:处理数据的线程
缓冲区:生产者放数据、消费者取数据的队列
模型结构:生产者 → 缓冲区(队列)→ 消费者
2. 核心规则
缓冲区满了 → 生产者必须等待
缓冲区空了 → 消费者必须等待
生产者放入数据 → 唤醒消费者
消费者取出数据 → 唤醒生产者
3. 为什么要用这种模式?(工作场景)
场景 1:解耦(生产者和消费者不直接通信)
1)生产者只管生产
2)消费者只管消费
3)互不影响,代码更清晰
场景 2:削峰 / 限流(高并发必备)
1)突然大量请求 → 不直接处理,先放入队列
2)消费者慢慢处理,不会压垮系统。
场景 3:异步处理(提高响应速度)
比如:
1)用户注册 → 发送短信、发送邮件
2)主线程不等待发送完成,丢到队列里,后台线程慢慢发
→ 接口响应极快
场景 4:快慢平衡
1)生产者生产快,消费者处理慢 → 队列缓冲
2)消费者处理快,生产者慢 → 消费者等待
4. 现实生活中的比喻(秒懂)
餐厅吃饭:
1)厨师 = 生产者
2)顾客 = 消费者
3)餐盘架子 = 缓冲区
规则:
1)盘子满了 → 厨师停下来等
2)盘子空了 → 顾客等
3)厨师放盘子 → 通知顾客
4)顾客拿走盘子 → 通知厨师
5. 这种模式可以用在那种技术里?
1)MQ 消息队列(RabbitMQ / RocketMQ / Kafka)
2)线程池队列
3)Netty 异步处理
4)订单系统、日志系统、推送系统
5)秒杀、高并发削峰
所有高并发、异步、解耦的系统都离不开它!
七、并发工具类
7.1 CountDownLatch
1. CountDownLatch是什么?
CountDownLatch = 倒计时门栓 / 计数器发令枪
作用:让一个线程等待,直到其他线程把计数器减到 0,它才开始运行。
核心就 3 个方法:
1)new CountDownLatch(3) → 初始化计数器 = 3
2)countDown() → 计数器 -1
3)await() → 等待计数器变成 0,再继续执行
2. 生活化比喻(秒懂)
运动会百米赛跑
裁判 = 调用 await() 的线程 → 必须等所有运动员准备好
运动员 = 其他线程
3 个运动员 → new CountDownLatch(3)
每个运动员准备好 → countDown()
3 个都准备好 → 计数器 = 0 → 裁判鸣枪开跑!
大概流程就是:
裁判等(await)
运动员1准备好 → countDown(2)
运动员2准备好 → countDown(1)
运动员3准备好 → countDown(0)
✅ 计数器=0 → 裁判开始执行!
3. CountDownLatch的3个核心方法
1)CountDownLatch(int count)
创建计数器,必须指定等待多少个任务完成。
2)countDown()
计数器 -1
每调用一次,就代表一个线程完成了。
3)await()
阻塞等待
当前线程停在这里不动,直到计数器变成 0 才继续执行。
4. CountDownLatch 最常用两大场景
场景 1:主线程等待多个子线程全部执行完再继续
比如:
系统启动时,等3 个配置加载线程全部完成
等5 个接口调用全部返回
等数据初始化完成
场景 2:模拟并发同时执行
所有线程先 await(),主线程最后 countDown,大家一起开始执行。
(模拟高并发压力测试)
5. CountDownLatch 特点(面试必背)
1)一次性:计数器到 0 就不能重置
2)不可重复使用:用完就废
3)一个线程等多个线程
4)底层基于 AQS 实现
6. 和 Thread.join () 的区别
join():必须等线程完全结束
CountDownLatch:更灵活,线程可以不结束,只需要调用 countDown ()
使用案例如下:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器 = 3(等3个线程执行完)
CountDownLatch latch = new CountDownLatch(3);
// 线程1
new Thread(() -> {
System.out.println("线程1执行完");
latch.countDown(); // 计数器 -1 → 2
}).start();
// 线程2
new Thread(() -> {
System.out.println("线程2执行完");
latch.countDown(); // 计数器 -1 → 1
}).start();
// 线程3
new Thread(() -> {
System.out.println("线程3执行完");
latch.countDown(); // 计数器 -1 → 0
}).start();
// 主线程等待!直到计数器=0
System.out.println("主线程等待所有线程执行完...");
latch.await();
// 只有计数器=0 才会执行这里!
System.out.println("所有线程执行完毕,主线程继续运行!");
}
}
运行结果:
主线程等待所有线程执行完...
线程1执行完
线程2执行完
线程3执行完
所有线程执行完毕,主线程继续运行!
7.2 CyclicBarrier
1. CyclicBarrier是什么?
CyclicBarrier:循环栅栏
让一组线程互相等待,全部到达栅栏位置后,再一起同时放行;
而且可以循环复用,这是和 CountDownLatch 最大区别。
关键词:凑齐人、一起走、可循环
2. 生活化比喻(秒懂)
场景:组团旅游集合
一个旅游团固定 5 个人发车
先来的游客在景区门口等待
等到5 个人全部到齐,统一进景区
游玩结束出来,还可以再次集合、再次出发(可循环)
每一个线程 = 一个游客
栅栏屏障 = 景区门口集合点
凑齐设定数量 → 全员同时放行
用完可以重置再来一轮
3. 核心构造 & 核心方法
1)构造器
// 设定拦截线程数:凑齐 3 个就放行
CyclicBarrier barrier = new CyclicBarrier(3);
// 凑齐后,优先执行栅栏任务
CyclicBarrier barrier = new CyclicBarrier(3, ()->{
System.out.println("所有人到齐了,统一发车!");
});
2)核心方法
// 线程到达栅栏,阻塞等待,凑齐数量才放行
barrier.await();
每个线程调用 await():表示我到栅栏了
栅栏没凑够人数:线程阻塞
凑够设定人数:所有线程同时唤醒往下执行
4. CyclicBarrier 三大核心特点
1)线程互相等待
CountDownLatch 是 一个线程等其他线程;
CyclicBarrier 是 一组线程互相等。
2)可循环复用
一轮凑齐放行后,自动重置,可以直接开启下一轮等待;
CountDownLatch 倒计时到 0 一次性作废,不能复用。
3)支持栅栏回调任务
凑齐所有人后,优先执行传入的 Runnable,再唤醒所有业务线程。
5. 真实工作常用场景
场景 1:多线程分段计算,最后合并结果
1)开启多线程分别计算一部分数据
2)每个线程计算完到栅栏等待
3)全部计算完成,统一进入合并汇总阶段
场景 2:多阶段任务并行
比如大数据处理:阶段 1 全部做完,再统一进入阶段 2,一轮一轮循环执行。
场景 3:模拟分布式节点同步
所有节点执行到检查点,互相等待全部就绪,再统一往下走。
场景 4:压力测试模拟统一并发
多个线程准备就绪,等全部到位再同时发起请求。
6. 底层简单原理
1)基于 AQS + 可重入锁 + 条件队列 实现
2)内部维护 parties (总人数)、count (当前剩余等待数)
3)每个 await():count--,count≠0 就阻塞
4)count=0 时:执行栅栏任务 → 唤醒所有等待线程 → 自动重置 count,支持下一轮循环
7. 面试标准背诵版
1)CyclicBarrier 是循环栅栏,让一组线程到达屏障后互相等待,凑齐数量才同时放行;
2)支持循环复用,一轮结束自动重置,区别于 CountDownLatch 一次性;
3)可传入栅栏任务,凑齐线程后优先执行;
4)适用于多线程分段计算、多阶段同步、并行任务统一推进场景;
5)底层基于 AQS 实现,利用锁和条件队列做等待唤醒。
CyclicBarrier vs CountDownLatch 重点对比
| 对比维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 作用 | 一个线程等 N 个线程完成 | N 个线程互相等待,凑齐一起走 |
| 复用性 | 一次性,不能重置 | 可循环复用,自动重置 |
| 计数器递减 | 只能减不能加 | 内部自动重置 |
| 等待关系 | 主等子 | 子线程互相等 |
| 回调任务 | 无 | 支持凑齐后执行栅栏任务 |
| 典型场景 | 等待初始化、等待批量任务完成 | 分段任务并行、多人同步开会、多阶段任务 |
实例如下:
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 凑齐3个线程就一起放行,凑齐先执行栅栏任务
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("✅ 3个线程全部到达,统一放行!");
});
// 开启3个线程
for (int i = 1; i <= 3; i++) {
int num = i;
new Thread(() -> {
try {
System.out.println("线程" + num + ":到达栅栏,等待同伴...");
// 阻塞等其他人
barrier.await();
System.out.println("线程" + num + ":继续往下执行业务");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
7.3 Semaphore
1. Semaphore 是什么?
Semaphore = 许可证管理器
作用:控制最多有多少个线程能同时执行某段代码!
例如:
就像厕所坑位:
1)一共 3 个坑位
2)最多 3 个人 同时上厕所
3)其他人必须排队等待
4)有人出来(释放许可证),下一个才能进去
2. 核心概念
1)构造方法
// 最多允许 3 个线程同时执行
Semaphore semaphore = new Semaphore(3); // 3 = 许可证数量
2)两个核心方法
semaphore.acquire(); // 获取许可证(没有就阻塞等待)
semaphore.release(); // 释放许可证(用完还回去)
3. 执行流程(厕所模型)
1)线程来了 → acquire() 申请许可证
2)有许可证 → 进去执行
3)没有 → 阻塞等待
4)执行完 → release() 释放许可证
5)下一个线程才能拿到许可证进去
4. Semaphore 两大核心用途(工作必用)
1)用途1: 控制并发线程数(限流)
比如:
接口限流:最多 100 个请求同时访问
数据库连接控制:最多 20 个连接
避免高并发压垮系统
这就是 Semaphore 最常用场景!
2)用途2: 实现互斥锁(相当于 1 个许可证)
Semaphore semaphore = new Semaphore(1);
效果 = synchronized / ReentrantLock
同一时间只有 1 个线程执行
5. semaphore的重要特性
1)公平 / 非公平
// 非公平(默认,性能高)
new Semaphore(3);
// 公平锁:先到先得
new Semaphore(3, true);
2)acquire () 会阻塞
拿不到许可证就一直等
3)release () 必须执行
一般放在 finally 里,防止死锁
4)可以一次性获取多个许可证
semaphore.acquire(2);
semaphore.release(2);
6. Semaphore 通俗总结(背会)
Semaphore = 许可证
acquire() = 拿证
release() = 还证
控制最多多少线程同时运行
限流神器!
同步工具对比:
| 工具 | 作用 | 一句话记忆 |
|---|---|---|
| CountDownLatch | 一个线程等多个线程 | 倒计时,到 0 放行 |
| CyclicBarrier | 一组线程互相等待 | 凑齐人一起走 |
| Semaphore | 控制并发线程数量 | 许可证限流 |
实例如下:
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
// 最多 3 个线程同时执行
Semaphore semaphore = new Semaphore(3);
// 启动 10 个线程
for (int i = 1; i <= 10; i++) {
int num = i;
new Thread(() -> {
try {
// 获取许可证
semaphore.acquire();
System.out.println("线程" + num + " 正在执行");
Thread.sleep(2000); // 模拟业务
System.out.println("线程" + num + " 执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可证(必须放 finally)
semaphore.release();
}
}).start();
}
}
}
7.4 Exchanger
1. 什么是 Exchanger?
Exchanger = 两个线程之间的「数据交换站」
作用:
只有两个线程都到了,才能互相交换数据;
只要有一个没到,另一个就必须一直等!
2. 场景:两个人交换物品
线程 A = 甲
线程 B = 乙
Exchanger = 交换点
规则:
1)甲带着数据来到交换点 → 等待乙
2)乙带着数据来到交换点 → 等待甲
3)两个人都到了 → 瞬间交换数据
4)各自拿着对方的数据离开
3. 核心 API(就一个方法)
Exchanger<String> exchanger = new Exchanger<>();
// 线程调用这个方法:把自己的数据交出去,等待并获取对方的数据
String otherData = exchanger.exchange(myData);
exchange (我的数据)
阻塞等待另一个线程到来
对方到了 → 交换数据
交换完成 → 两个线程同时唤醒
4. 代码执行流程(超级清晰)
1)线程 A 先到交换点
调用 exchange()
阻塞等待,直到线程 B 来
2)线程 B 后到交换点
调用 exchange()
两个线程相遇
3)瞬间交换数据
A 把自己的数据给 B
B 把自己的数据给 A
4)交换完成
两个线程同时唤醒
各自拿到对方的数据继续执行
4. Exchanger 三大特点(面试必背)
1)只能用于 两个线程 交换
第三个线程来会一直阻塞,永远交换不了。
2)双向阻塞
一个线程到了必须等另一个,谁先到谁等。
3)无锁、高效
底层使用 CAS 实现,比加锁交换快很多。
4)可以循环交换
交换完一次,还能继续交换多次。
5. 使用场景(工作中用在哪?)
场景 1:两个线程之间交换数据
生产者 A → 给 → 消费者 B
生产者 B → 给 → 消费者 A
一对一交换
场景 2:线程间实时传输数据
游戏:玩家交易
数据流交换
管道通信
场景 3:错误恢复 / 数据校对
一个线程计算一半,把中间结果交给另一个线程继续算。
Java 四大同步工具对比总结:
| 工具 | 作用 | 一句话 |
|---|---|---|
| CountDownLatch | 一个线程等多个线程 | 倒计时到 0 放行 |
| CyclicBarrier | 一组线程互相等待 | 凑齐人一起走 |
| Semaphore | 控制并发数量 | 许可证限流 |
| Exchanger | 两个线程交换数据 | 你换我,我换你 |
实用案例演示:
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
// 创建交换器
Exchanger<String> exchanger = new Exchanger<>();
// 线程A
new Thread(() -> {
try {
String dataA = "我是A的数据";
System.out.println("线程A 到达交换点,等待对方...");
// 交换:把A的数据交出去,拿到B的数据
String dataB = exchanger.exchange(dataA);
System.out.println("线程A 收到:" + dataB);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A").start();
// 线程B
new Thread(() -> {
try {
String dataB = "我是B的数据";
System.out.println("线程B 到达交换点,等待对方...");
// 交换:把B的数据交出去,拿到A的数据
String dataA = exchanger.exchange(dataB);
System.out.println("线程B 收到:" + dataA);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
}
}
输出结果:
线程A 到达交换点,等待对方...
线程B 到达交换点,等待对方...
线程A 收到:我是B的数据
线程B 收到:我是A的数据
用通俗话理解:
线程 A 先到交换点 → 阻塞等待
线程 B 后到交换点 → 触发交换
互相交换数据
两个线程同时被唤醒,继续运行
八、线程池
8.1 为什么要用线程池?
1. 不用线程池的坏处
手动 new Thread() 弊端:
1)频繁创建销毁线程 开销极大
2)无限制创建线程,CPU 爆满、OOM
3)线程无管理,不可监控、不可复用
4)任务来了就新建,高并发直接打垮系统
2. 线程池核心好处(工作实际价值)
1)线程复用:线程创建一次,反复处理任务,减少开销
2)控制并发数:限制最大线程数,保护 CPU、不雪崩
3)统一管理:线程统一调度、监控、拒绝策略
4)异步解耦:业务异步处理,提升接口响应速度
5)定时 / 周期任务:支持延迟、定时执行
一句话:工作中绝不允许手动 new Thread,一律用线程池。
8.2 线程池核心底层架构
1. 核心组成
1)核心线程:常驻线程,不随便回收
2)任务队列:存放等待执行的任务(BlockingQueue)
3)最大线程:任务爆了之后扩容到的上限
4)空闲时间:非核心线程空闲多久就回收
5)拒绝策略:队列 + 线程都满了,怎么拒绝新任务
2. 工作执行流程(重中之重)
1)来了任务 → 先看核心线程数,没满就新建核心线程执行
2)核心线程满了 → 进任务队列排队
3)队列也满了 → 新建非核心线程,直到达到最大线程数
4)最大线程也满了 + 队列也满了 → 触发拒绝策略
背下来:核心线程 → 队列 → 最大线程 → 拒绝策略
8.3 ThreadPoolExecutor 7 大核心参数
public ThreadPoolExecutor(
int corePoolSize, // 1.核心线程数
int maximumPoolSize, // 2.最大线程数
long keepAliveTime, // 3.空闲线程存活时间
TimeUnit unit, // 4.时间单位
BlockingQueue<Runnable> workQueue, // 5.任务队列
ThreadFactory threadFactory, // 6.线程工厂
RejectedExecutionHandler handler // 7.拒绝策略
)
详细讲解核心参数:
1. corePoolSize 核心线程数
1)常驻线程,不会被回收
2)业务低峰也保留,随时处理任务
3)实际配置:
CPU 密集型:核心数 = CPU 核心数
IO 密集型(接口、DB、网络):核心数设大一点 10~30
2. maximumPoolSize 最大线程数
1)系统能承受的最大并发线程数
2)超过这个数就不再建新线程
3. keepAliveTime 空闲存活时间
1)非核心线程空闲多久就销毁
2)避免空闲线程一直占资源
4. workQueue 任务阻塞队列
常用队列:
1)ArrayBlockingQueue:有界队列,固定容量(工作最常用)
2)LinkedBlockingQueue:无界 / 有界,不设容量就无限放任务 → 容易 OOM
3)SynchronousQueue:不存任务,来了直接新建线程
4)DelayQueue:延迟任务队列
工作禁忌:不要用无界队列,请求暴涨直接内存溢出。
5. ThreadFactory 线程工厂
1)自定义线程名称、优先级、是否守护线程
2)工作必须自定义:方便日志排查、定位问题
6. RejectedExecutionHandler 拒绝策略(4 种)
1)AbortPolicy:直接抛异常(默认)
2)CallerRunsPolicy:谁提交任务谁自己跑(主线程执行)
3)DiscardPolicy:直接丢弃任务,不报错
4)DiscardOldestPolicy:丢弃队列最老任务,执行新任务
工作场景:
1)支付、订单:用 AbortPolicy 抛异常告警
2)日志、统计:用 DiscardPolicy 丢弃无关紧要任务
3)非核心业务:CallerRunsPolicy 降级自己执行
8.4 JDK 内置 4 种线程池(工作禁止直接用,面试要懂)
1. Executors.newFixedThreadPool(n)
固定核心线程 = 最大线程
队列无界 → 高并发 OOM
2. Executors.newCachedThreadPool()
无核心线程,最大无限
任务多就无限建线程 → CPU 打爆
3. Executors.newSingleThreadExecutor()
单线程,串行执行
适合顺序任务,但无界队列有风险
4. Executors.newScheduledThreadPool()
定时、周期性任务
阿里开发规范:禁止使用 Executors 创建线程池,必须手动 new ThreadPoolExecutor 7 个参数自己配置
原因:内置线程池要么无界队列、要么无限线程,生产容易事故。
8.5 实际工作中怎么手动创建线程池(标准模板)
import java.util.concurrent.*;
public class ThreadPoolConfig {
// 自定义线程工厂
private static final ThreadFactory NAMED_THREAD_FACTORY =
new ThreadFactoryBuilder()
.setNameFormat("biz-pool-%d")
.setDaemon(false)
.build();
// 业务线程池 全局单例
public static final ThreadPoolExecutor BIZ_POOL = new ThreadPoolExecutor(
10, // 核心线程
30, // 最大线程
60L, // 空闲存活60秒
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
NAMED_THREAD_FACTORY,
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
}
工作用法:全局一个线程池,各处复用提交任务。
8.6 线程池提交任务的两种方式
- execute 无返回值
BIZ_POOL.execute(() -> {
// 异步业务:记录日志、推送消息
});
- submit 有返回值(Callable + Future)
Future<Integer> future = BIZ_POOL.submit(() -> {
return 100 + 200;
});
// 阻塞获取结果
Integer res = future.get();
适用:需要异步计算、拿到结果的业务。
8.7 实际工作常用业务场景(全覆盖)
场景 1:接口异步解耦
用户下单:
主线程:落库、返回订单号
线程池异步:发短信、发推送、记录日志、积分发放
提升接口响应速度
场景 2:批量任务处理
批量导入、批量对账、批量数据同步
用线程池多线程分片处理,提速
场景 3:限流保护
限制最大线程数,防止瞬间流量打垮 DB / 接口
场景 4:定时任务
用 ScheduledThreadPool 做延迟任务、周期巡检
场景 5:多线程并行计算
大数据统计、报表生成,多线程拆分计算再汇总
8.8 线程池常见坑(工作中容易踩的坑)
1、用无界队列 → 流量暴涨 OOM
2、不自定义线程名 → 线上故障无法排查堆栈
3、不配置拒绝策略 → 默认抛异常无告警
4、线程池不全局复用 → 到处新建线程池,资源浪费
5、get () 不设超时 → 无限阻塞卡死业务
6、任务内部不捕获异常 → 线程挂掉,线程池少线程
8.9 线程池核心知识点 面试极简背诵版
1、线程池优势:线程复用、控制并发、资源管控、异步解耦
2、执行流程:核心线程 → 队列 → 最大线程 → 拒绝策略
3、7 大参数:核心、最大、空闲时间、单位、队列、线程工厂、拒绝策略
4、4 种拒绝策略:中止、调用者运行、丢弃、丢弃最旧
5、工作规范:不用 Executors,手动 new ThreadPoolExecutor,有界队列、自定义线程名
6、IO 密集型线程数配大,CPU 密集型配接近核心数
8.10 实例演示:
// 实际工作中建议手动创建线程池
public class ThreadPoolConfig {
public ExecutorService createThreadPool() {
int corePoolSize = 5; // 核心线程数
int maxPoolSize = 20; // 最大线程数
long keepAliveTime = 60L; // 空闲线程存活时间
int queueCapacity = 100; // 队列容量
return new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new CustomThreadFactory(), // 自定义线程工厂
new CustomRejectedHandler() // 拒绝策略
);
}
}
// 自定义线程工厂
class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "custom-pool-" + threadNumber.getAndIncrement());
thread.setDaemon(false);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}
// 自定义拒绝策略
class CustomRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志
System.err.println("任务被拒绝:" + r);
// 可选的降级策略
if (!executor.isShutdown()) {
r.run(); // 由调用线程执行
}
}
}
九、并发集合
9.1 ConcurrentHashMap
1. ConcurrentHashMap 是什么?
这是一个线程安全的本地缓存工具类
用 ConcurrentHashMap 实现,高并发下绝对安全,用来做:
1)本地缓存
2)共享配置
3)热点数据存储
4)多线程读写安全的 KV 存储
2. ConcurrentHashMap 原理?
ConcurrentHashMap = 线程安全的高效哈希表
它解决了 2 个问题:
1)HashMap 多线程不安全(会死循环、数据丢失)
2)Hashtable 效率极低(整张表加锁,并发差)
ConcurrentHashMap 原理(简单讲)
1)分段锁 / 桶锁(JDK 1.7 分段,JDK 1.8+ CAS + synchronized)
2)只锁一个哈希桶,不锁整张表
3)读不加锁,写轻量锁
4)高并发下性能极高
3. 这个工具类在实际工作中怎么用?(真实业务场景)
场景 1:本地缓存(最常用)
ConcurrentCache<String, User> userCache = new ConcurrentCache<>();
// 高并发下安全获取用户
User user = userCache.getOrCreate(userId, id -> userService.getById(id));
场景 2:多线程共享计数器 / 配置
ConcurrentCache<String, Integer> config = new ConcurrentCache<>();
场景 3:接口限流、缓存标记、分布式锁标记
cache.getOrCreate("limit:user:1001", key -> 1);
场景 4:全局临时缓存
比如:
商品详情缓存
首页热点数据
高频访问的配置
4. ConcurrentHashMap 最关键特点
1)线程安全
多线程读写不会出错
2)高性能
读操作无锁
写操作只锁一个节点
并发能力远超 Hashtable
3)key 和 value 都不能为 null
这是它和 HashMap 的重要区别
4. 常用原子方法(工作必用)
// 不存在才 put(原子)
map.putIfAbsent(key, value);
// 不存在则创建(原子,最常用)
map.computeIfAbsent(key, k -> createValue(k));
// 修改值(原子)
map.compute(key, (k, v) -> v + 1);
// 替换(原子)
map.replace(key, oldValue, newValue);
工作中最标准、最常用、最稳定的【线程安全本地缓存】写法如下:
public class ConcurrentCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
// 原子获取/创建(缓存最常用)
public V getOrCreate(K key, Function<K, V> creator) {
return cache.computeIfAbsent(key, creator);
}
// 线程安全批量更新
public void batchUpdate(Map<K, V> updates) {
cache.putAll(updates);
}
}
9.2 CopyOnWriteArrayList
1. 核心原理
核心原理:写时复制(Copy-On-Write)
规则只有一句话:
只要修改(add/set/remove),就复制一个新数组,操作新数组,最后替换引用。
1)写操作流程(add)
加锁(保证多线程写不乱)
复制原数组 → 创建一个更长的新数组
把新元素加到新数组
将引用指向新数组
释放锁
2)读操作流程(get / 遍历)
不加锁
直接读取当前引用指向的数组
速度 = 普通 ArrayList 一样快
2. 优缺点
优点
1)读性能极高
2)遍历安全,不会抛异常
3)高并发下非常稳定
4)多线程读写可同时进行(读无锁、写锁)
缺点
1)写性能差
2)写时内存占用翻倍(复制数组)
3)数据不是强实时(读快照)
4)不能用于频繁修改的场景
3. 使用场景
必须满足:读多写少!
最典型场景:
1)事件监听器列表
2)白名单、黑名单
3)系统配置列表(很少变,经常读)
4)本地缓存的集合
5)高并发遍历、不能加锁的场景
经典代码(时间监听器列表)
// 实际场景:读多写少的场景
public class EventManager {
private final CopyOnWriteArrayList<EventListener> listeners =
new CopyOnWriteArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener); // 写时复制
}
public void fireEvent(Event event) {
for (EventListener listener : listeners) { // 快照迭代
listener.onEvent(event);
}
}
}
9.3 BlockingQueue
1. 什么是BlockingQueue?
BlockingQueue = 线程安全的阻塞队列
是「生产者 - 消费者模型」的官方最佳实现!
核心规则:
1)队列满了 → 再往里放的线程会阻塞(等待)
2)队列空了 → 再往外拿的线程会阻塞(等待)
3)自带线程阻塞、唤醒能力,不用自己写 wait/notify
2. 生活中的比喻(秒懂)
BlockingQueue 就是一个排队通道:
1)生产者 = 往通道放货物的人
2)消费者 = 从通道拿货物的人
3)通道满了 → 生产者必须等
4)通道空了 → 消费者必须等
5)JVM 自动帮你阻塞、唤醒
3. 常用 5 大 BlockingQueue 实现类(工作 + 面试)
1)ArrayBlockingQueue(最常用!有界队列)
数组实现
有界(必须指定容量)
公平 / 非公平可选
生产环境首选!
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
2)LinkedBlockingQueue(无界 / 有界链表)
链表实现
如果不指定容量 = 无限大小(可能 OOM)
吞吐量比 Array 高
慎用无界!
3)SynchronousQueue(不存储元素的队列)
容量 = 0
放一个必须等另一个取走
适合任务量大、直接交换
Executors.newCachedThreadPool () 底层用它
4)PriorityBlockingQueue(优先级队列)
按优先级出队,不是 FIFO
无界
5) DelayQueue(延迟队列)
到期才能取出
适合:订单超时取消、任务延迟执行
4. BlockingQueue 最核心特点
1)线程安全(底层用 ReentrantLock)
2)自动阻塞、自动唤醒
3)有界队列可以防止无限任务导致 OOM
4)高并发、高性能
5)是线程池、MQ、RPC 框架的底层核心组件
5. BlockingQueue 面试必背总结
1)BlockingQueue 是线程安全的阻塞队列
2)满了 put 阻塞,空了 take 阻塞
3)常用:ArrayBlockingQueue(有界安全)
4)底层:Lock + Condition 实现等待唤醒
5)核心用途:生产者 - 消费者、线程池、削峰、异步解耦
6)生产必须用有界队列,避免 OOM
实例演示:
// 实际场景:生产者-消费者
public class DataProcessor {
private final BlockingQueue<Data> queue = new LinkedBlockingQueue<>(100);
public void start() {
// 生产者
new Thread(() -> {
while (true) {
Data data = produceData();
try {
queue.put(data); // 队列满时阻塞
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
// 消费者
for (int i = 0; i < 3; i++) {
new Thread(() -> {
while (true) {
try {
Data data = queue.take(); // 队列空时阻塞
processData(data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
}
}
}
十、线程池监控与调优
10.1 监控指标
public class ThreadPoolMonitor {
private final ThreadPoolExecutor executor;
public void printStats() {
System.out.println("核心线程数:" + executor.getCorePoolSize());
System.out.println("活动线程数:" + executor.getActiveCount());
System.out.println("最大线程数:" + executor.getMaximumPoolSize());
System.out.println("队列大小:" + executor.getQueue().size());
System.out.println("完成任务数:" + executor.getCompletedTaskCount());
}
}
10.2 参数调优策略
public class ThreadPoolTuning {
/**
* CPU密集型任务
* 核心线程数 = CPU核数 + 1
*/
public ExecutorService cpuIntensivePool() {
int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
return new ThreadPoolExecutor(
corePoolSize,
corePoolSize * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
}
/**
* IO密集型任务
* 核心线程数 = CPU核数 * 2
*/
public ExecutorService ioIntensivePool() {
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
return new ThreadPoolExecutor(
corePoolSize,
corePoolSize * 2,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<>() // 直接交接,无队列
);
}
}
十一、实际工作最佳实践
11.1 异常处理
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 统一异常处理工具类
* 处理:普通线程异常 + 线程池线程异常
*/
public class ThreadExceptionHandler {
/**
* 全局统一异常处理
*/
public void handleThreadException() {
// ====================== 1. 全局默认未捕获异常处理器(所有线程通用) ======================
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
System.err.println("\n【全局线程异常捕获】");
System.err.println("线程名称:" + thread.getName());
System.err.println("线程ID:" + thread.getId());
System.err.println("异常信息:" + throwable.getMessage());
System.err.println("异常堆栈:");
throwable.printStackTrace(); // 打印完整堆栈
// 实际业务可在这里:
// 1. 记录日志(log.error)
// 2. 发送告警(邮件/短信/钉钉)
// 3. 线程自动恢复
});
// ====================== 2. 线程池专用异常处理器 ======================
ExecutorService executor = new ThreadPoolExecutor(
5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
// 自定义线程工厂,给池内每个线程设置异常处理器
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("thread-pool-" + t.getId());
// 给线程池线程单独设置异常处理器
t.setUncaughtExceptionHandler((poolThread, throwable) -> {
System.err.println("\n【线程池线程异常捕获】");
System.err.println("线程名称:" + poolThread.getName());
System.err.println("异常信息:" + throwable.getMessage());
throwable.printStackTrace();
});
return t;
}
}
);
}
}
11.2 资源清理
// 确保线程池正确关闭
public class ResourceCleanup {
public void shutdownGracefully(ExecutorService executor) {
executor.shutdown(); // 停止接收新任务
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 取消正在执行的任务
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未正常关闭");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
11.3 避免常见的一些陷阱
1、死锁预防:按固定顺序获取锁
2、活锁避免:加入随机退让
3、线程泄漏:确保线程正确关闭
4、上下文切换开销:避免创建过多线程
5、内存可见性:正确使用volatile/final
浙公网安备 33010602011771号