2.1 两阶段终止线程(优雅终止线程)
- 问题背景
- 多线程中如何安全终止一个线程?错误方式如
stop()会导致死锁,destroy()会终止整个进程。
- 多线程中如何安全终止一个线程?错误方式如
- 正确方式:两阶段终止模式
- 第一阶段:发送中断请求
- 使用
interrupt()方法设置中断标志位。 - 可唤醒处于休眠状态的线程。
- 使用
- 第二阶段:判断中断标志位并处理收尾逻辑
- 在线程内检查
Thread.interrupted()标志。 - 若为true,则执行资源释放等操作后结束线程。
- 在线程内检查
- 注意点
- 中断异常处理需重新设置中断标志位,防止标志被清除。
- 结合变量标志位使用更可靠。
- 第一阶段:发送中断请求
2.2 避免共享的设计模式
2.2.1 不变性模式
java
// 1. 类 final
public final class ImmutableUser {
// 2. 属性 private final
private final String name;
private final int age;
// 3. 构造器一次性赋值
public ImmutableUser(String name, int age) {
this.name = name;
this.age = age;
}
// 4. 只有 getter,没有 setter
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
- 思想
- 对象创建后不可修改,只读对象天然线程安全。
- 实现方式
- 类和属性用
final修饰。 - 去除setter方法,仅保留getter。
- 类和属性用
- 适用场景
- 缓存数据、配置信息、操作系统只读数据。
- 注意事项
- 可变对象即使属性为final也无法保证线程安全。
- 应避免暴露内部可变状态。
2.2.2 写实复制(Copy-on-Write)
java
import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;
// 手写极简版 CopyOnWriteArrayList
public class SimpleCopyOnWriteList<E> {
// 真正存数据的数组,volatile 保证可见性
private volatile Object[] array;
// 写操作加锁,防止多线程同时复制
private final ReentrantLock lock = new ReentrantLock();
// 构造:空数组
public SimpleCopyOnWriteList() {
array = new Object[0];
}
// ==================== 读方法:不加锁,直接读 ====================
public E get(int index) {
return (E) array[index];
}
public int size() {
return array.length;
}
// ==================== 写方法:加锁 + 复制新数组 ====================
public boolean add(E e) {
lock.lock();
try {
// 1. 获取旧数组
Object[] oldArr = array;
// 2. 复制一个新数组,长度+1
Object[] newArr = Arrays.copyOf(oldArr, oldArr.length + 1);
// 3. 在新数组上添加元素
newArr[oldArr.length] = e;
// 4. 把引用指向新数组(volatile 保证其他线程立刻看到)
array = newArr;
return true;
} finally {
lock.unlock();
}
}
}
- 原理
- 修改对象时不直接修改原对象,而是复制一份再修改。
- 典型应用
CopyOnWriteArrayList在写操作时复制数组。- 操作系统fork进程时也采用该机制。
- 优点
- 适用于读多写少场景。
- 缺点
- 内存占用高。
2.2.3 线程本地存储(ThreadLocal)
java
public class ThreadLocalTest {
// 定义 ThreadLocal
private static final ThreadLocal<Integer> TL = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 线程1 +10
new Thread(() -> {
TL.set(TL.get() + 10);
System.out.println("线程1:" + TL.get()); // 10
}).start();
// 线程2 +20
new Thread(() -> {
TL.set(TL.get() + 20);
System.out.println("线程2:" + TL.get()); // 20
}).start();
}
}
- 作用
- 每个线程拥有独立副本,避免线程间竞争。
- 典型应用
- 链路追踪上下文、全局事务ID传递、游戏玩家状态管理。
- 注意事项
- 线程池中使用
ThreadLocal需及时调用remove(),否则可能导致内存泄漏。
- 线程池中使用
2.3 多线程协作的设计模式
2.3.1 首次挂起(等待唤醒机制)
- 实现方式
- Java中使用
wait()/notify()或ReentrantLock+Condition。 - 使用CAS+park/unpark实现高性能无锁协作。
- Java中使用
- 典型应用
- 生产者消费者模型。
- 同步任务依赖,如等待某个结果后再继续执行。
2.3.2 Become模式(避免重复执行)
- 思想
- 多个线程竞争执行某项任务时,仅允许一个线程完成任务,其他线程跳过。
- 典型应用
- 单例双重校验锁(DCL)。
- 锁膨胀过程中多个线程竞争初始化monitor对象。
- 注册中心组件初始化、定时任务启动。
2.4 多线程分工模式
2.4.1 Thread-per-Message(每个任务一个线程)
- 特点
- 每个任务分配一个线程,简单但线程开销大。
- 典型应用
- BIO网络编程中为每个连接分配一个线程。
- 定时任务中单独线程执行任务。
2.4.2 Worker Thread(工作线程池)
- 优势
- 避免频繁创建销毁线程,提高性能。
- 支持任务队列缓冲,适应突发流量。
- 典型应用
- Java线程池(
ThreadPoolExecutor)。 - Netty事件循环组。
- 分布式注册中心心跳检测线程池。
- Java线程池(
2.4.3 生产者消费者模式
- 核心思想
- 生产者提交任务,消费者异步处理,解耦生产与消费过程。
- 典型应用
- 异步日志记录。
- 用户注册后异步发邮件/短信。
- 订单系统解耦库存扣减。
- 优势
- 提升接口响应速度。
- 实现服务降级。
- 削峰填谷,平滑处理高峰流量。
- 关键考量
- 消费者数量不足 → 积压。
- 队列容量不足 → 需扩容。
- 生产速度远超消费速度 → 限流。
3. 并发总结与面试指导
3.1 并发设计模式的重要性
- 并发设计模式是对常见并发问题的经验抽象与方案归纳。
- 能帮助开发者快速选择合适的解决方案,提升代码质量与稳定性。
3.2 面试高频考点
- 理论基础
- 并发与并行区别。
- 线程与进程理解。
- 线程通信机制
wait()/notify()、ThreadLocal、中断机制。
- 锁机制
synchronized、ReentrantLock、Condition。- AQS原理、锁升级流程(偏向锁→轻量级锁→重量级锁)。
- JMM模型
- happens-before原则、可见性、原子性、有序性。
- MESI协议、伪共享。
- 工具类与容器
ConcurrentHashMap、CopyOnWriteArrayList。CountDownLatch、CyclicBarrier、Semaphore。Future、CompletableFuture、ForkJoinPool。
- 线程池
- 核心参数、拒绝策略、工作队列作用。
- 如何合理配置线程池以应对不同业务场景。