
# Java多线程面试专项单例、阻塞队列、定时器、线程池核心案例篇 本文聚焦Java多线程面试中的 **四大经典案例**——单例模式、阻塞队列生产者消费者、定时器、线程池。这些案例是面试的高频考点也是理解并发编程思想的最佳入口。## 一、单例模式Singleton Pattern### 1.1 什么是单例模式单例模式是一种创建型设计模式确保一个类在JVM中**有且仅有一个实例**并提供一个全局访问点。**常见场景**- 数据库连接池DataSource- 配置管理类- 日志记录器- 线程池实例### 1.2 饿汉模式Eager Initializationjavaclass SingletonHungry {// 类加载时即创建实例JVM保证线程安全private static final SingletonHungry instance new SingletonHungry();private SingletonHungry() {} // 构造方法私有化public static SingletonHungry getInstance() {return instance;}}**特点**- ✅ 线程安全由JVM类加载机制保证- ✅ 实现简单- ❌ 类加载即创建如果从未使用则浪费内存### 1.3 懒汉模式Lazy Initialization**① 单线程版线程不安全**javaclass SingletonLazy {private static SingletonLazy instance null;private SingletonLazy() {}public static SingletonLazy getInstance() {if (instance null) {instance new SingletonLazy(); // 多线程下可能创建多个}return instance;}}**为什么线程不安全** 两个线程同时调用getInstance()都判断instance null然后各自创建一个实例违反了单例原则。**② 加锁版安全但性能差**javaclass SingletonSync {private static SingletonSync instance null;private SingletonSync() {}public static synchronized SingletonSync getInstance() {if (instance null) {instance new SingletonSync();}return instance;}}**问题**每次调用getInstance()都要加锁而**线程安全问题只发生在首次创建时**。一旦实例创建完毕后续的调用都不需要同步加锁造成了不必要的性能损耗。**③ 双重检查锁DCL面试必考**javaclass SingletonDCL {private static volatile SingletonDCL instance null; // volatile是关键private SingletonDCL() {}public static SingletonDCL getInstance() {if (instance null) { // 第一次检查无锁synchronized (SingletonDCL.class) {if (instance null) { // 第二次检查有锁instance new SingletonDCL();}}}return instance;}}**为什么要双重检查**| 检查层 | 目的 || :--- | :--- || **外层if** | 避免每次调用都加锁提升性能。实例已创建后直接返回 || **内层if** | 多个线程可能同时通过外层if在内层排队竞争锁只有第一个线程创建实例后续的发现已创建就不再创建 |**为什么要加volatile**new SingletonDCL() 不是原子操作JVM会分为三步1. 分配内存空间2. 初始化对象3. 将引用指向内存地址JVM可能会**指令重排序**为 1 → 3 → 2先分配内存再指向引用最后初始化。如果线程A执行到第3步引用已指向内存但对象尚未初始化此时线程B进入发现instance ! null直接返回一个**尚未初始化的半成品对象**导致程序出错。volatile会禁止指令重排序保证对象**完全初始化后**引用才对其他线程可见。## 二、阻塞队列与生产者消费者模型### 2.1 什么是阻塞队列BlockingQueue阻塞队列是一种**线程安全**的队列具备以下核心特性| 条件 | 行为 || :--- | :--- || 队列**满**时执行put() | 阻塞等待直到队列有空位 || 队列**空**时执行take() | 阻塞等待直到队列有元素 |### 2.2 生产者消费者模型**为什么需要这个模型****① 解耦**生产者和消费者不直接通信通过阻塞队列间接交互。就像包饺子擀皮的人只管擀皮包的人只管包中间用“饺子皮”这个缓冲区连接。**② 削峰填谷**秒杀场景下瞬时请求量极大。所有请求先入队消费者按自身能力慢慢处理防止服务器被冲垮。### 2.3 标准库的使用javaBlockingQueueInteger queue new LinkedBlockingQueue();// 生产者每秒生产一个随机数Thread producer new Thread(() - {Random random new Random();while (true) {int num random.nextInt(1000);System.out.println(生产 num);queue.put(num); // 队列满时阻塞Thread.sleep(1000);}});// 消费者持续消费Thread consumer new Thread(() - {while (true) {int value queue.take(); // 队列空时阻塞System.out.println(消费 value);}});### 2.4 手动实现阻塞队列面试手写题javapublic class MyBlockingQueue {private int[] items new int[1000];private volatile int size 0;private volatile int head 0;private volatile int tail 0;// 入队队列满时阻塞public void put(int value) throws InterruptedException {synchronized (this) {while (size items.length) { // 必须用while防止虚假唤醒wait();}items[tail] value;tail (tail 1) % items.length;size;notifyAll(); // 唤醒可能正在等待出队的消费者}}// 出队队列空时阻塞public int take() throws InterruptedException {synchronized (this) {while (size 0) {wait();}int ret items[head];head (head 1) % items.length;size--;notifyAll(); // 唤醒可能正在等待入队的生产者return ret;}}}**核心设计要点**| 设计点 | 说明 || :--- | :--- || **循环数组** | 用数组实现环形队列head/tail指针循环移动 || **while循环wait** | 防止**虚假唤醒**。被唤醒后条件可能仍不满足需要重新检查 || **notifyAll** | 唤醒**所有**等待线程因为不知道等待的是生产者还是消费者 || **volatile保证可见性** | size/head/tail在多线程间共享需要保证可见性 |## 三、定时器Timer### 3.1 什么是定时器定时器用于**在指定时间后执行任务**类似闹钟功能。**应用场景**- 网络超时重连500ms无响应则重试- 缓存过期自动删除3s后删除过期Key- 定时任务调度凌晨执行数据统计### 3.2 标准库使用javaTimer timer new Timer();timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(3秒后执行);}}, 3000);### 3.3 定时器的核心设计面试重点**需求**实现一个可同时管理**多个**定时任务的定时器每个任务在指定的延迟后执行。**数据结构**优先级队列按执行时间排序队首是最先需要执行的任务javapublic class MyTimer {// 优先级队列按执行时间排序队首是最近需要执行的任务private PriorityQueueMyTask queue new PriorityQueue();private Object locker new Object();// 注册任务指定执行内容和延迟时间public void schedule(Runnable command, long delay) {synchronized (locker) {queue.offer(new MyTask(command, delay));locker.notify(); // 有新任务入队唤醒工作线程重新调度}}// 构造器中启动工作线程public MyTimer() {Thread worker new Thread(() - {while (true) {synchronized (locker) {while (queue.isEmpty()) {locker.wait(); // 无任务等待}MyTask task queue.peek();long curTime System.currentTimeMillis();if (curTime task.time) {// 任务到期取出并执行queue.poll();task.runnable.run();} else {// 任务未到期等待到执行时间locker.wait(task.time - curTime);}}}});worker.start();}// 任务类实现Comparable按执行时间排序private static class MyTask implements ComparableMyTask {Runnable runnable;long time; // 绝对执行时间戳public MyTask(Runnable runnable, long delay) {this.runnable runnable;this.time System.currentTimeMillis() delay;}Overridepublic int compareTo(MyTask o) {return (int)(this.time - o.time); // 时间早的优先级高}}}**工作流程**1. 工作线程循环扫描队首任务2. 队首任务的执行时间到了 → 取出执行3. 队首任务还没到时间 → wait(剩余时间)等待到点或新任务唤醒## 四、线程池ThreadPool### 4.1 为什么需要线程池创建/销毁线程虽然比进程轻量但在高并发场景下仍是可观的开销。**类比**快递站每次有包裹就临时招人送送完就解雇招聘解雇成本太高。更好的方式是招少量固定员工忙不过来时任务先记录下来等员工空闲再处理。### 4.2 标准库创建方式java// 固定10个线程ExecutorService pool Executors.newFixedThreadPool(10);pool.submit(() - System.out.println(任务执行));pool.shutdown(); // 关闭线程池**Executors工厂方法对比**| 方法 | 说明 | 风险 || :--- | :--- | :--- || newFixedThreadPool(n) | 固定n个核心线程 | 队列无界可能OOM || newCachedThreadPool() | 动态扩容空闲60s回收 | 最大线程数无限制 || newSingleThreadExecutor() | 单线程保证顺序执行 | 队列无界 || newScheduledThreadPool(n) | 支持定时/延迟执行 | — | ⚠️ **阿里巴巴规范**禁止使用Executors直接创建必须用ThreadPoolExecutor手动指定参数规避资源耗尽风险。### 4.3 ThreadPoolExecutor七大核心参数javanew ThreadPoolExecutor(int corePoolSize, // 核心线程数正式员工int maximumPoolSize, // 最大线程数正式临时long keepAliveTime, // 临时工空闲存活时间TimeUnit unit, // 时间单位BlockingQueueRunnable workQueue, // 任务队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 拒绝策略)### 4.4 任务提交流程面试必考提交任务↓核心线程数已满 → 否 → 创建核心线程执行↓ 是阻塞队列已满 → 否 → 任务入队等待↓ 是最大线程数已满 → 否 → 创建临时线程执行↓ 是执行拒绝策略### 4.5 拒绝策略RejectedExecutionHandler| 策略 | 行为 || :--- | :--- || AbortPolicy默认 | 抛出RejectedExecutionException || CallerRunsPolicy | 谁提交的任务谁自己执行回调调用者线程 || DiscardOldestPolicy | 丢弃队列中最老的任务重新提交新任务 || DiscardPolicy | 静默丢弃新任务不抛异常 |### 4.6 手动实现一个简易线程池javapublic class MyThreadPool {private BlockingQueueRunnable queue new LinkedBlockingQueue();// 提交任务public void submit(Runnable task) throws InterruptedException {queue.put(task);}// 构造器提前创建n个工作线程public MyThreadPool(int n) {for (int i 0; i n; i) {new Thread(() - {while (true) {try {Runnable task queue.take(); // 无任务时阻塞task.run(); // 执行任务} catch (InterruptedException e) {e.printStackTrace();}}}).start();}}}## 五、进程与线程对比面试题汇总### 5.1 核心区别| 维度 | 进程 | 线程 || :--- | :--- | :--- || **资源分配单位** | ✅ 是 | ❌ 否 || **调度单位** | ❌ 否 | ✅ 是 || **内存空间** | 独立互不共享 | 共享同一进程内 || **创建/销毁开销** | 大 | 小 || **通信方式** | 复杂IPC管道、信号、共享内存 | 简单共享变量 || **健壮性** | 一个进程挂了不影响其他 | 一个线程挂了可能导致整个进程崩溃 |### 5.2 线程的七大优势1. 创建/销毁线程比进程快得多2. 线程切换比进程切换开销小3. 线程占用资源少4. 能充分利用多核CPU5. IO等待期间可执行其他计算任务6. 计算密集型任务可拆分到多个线程7. IO密集型任务可通过线程重叠IO等待时间## 六、总结面试记忆口诀**单例模式** *“饿汉加载快又稳懒汉DCL双重锁volatile防重排面试必考要记牢。”***生产者消费者** *“阻塞队列做缓冲削峰填谷解耦合put/take有阻塞while循环防假醒。”***定时器** *“优先队列按时间队首任务先执行工作线程循环扫wait到点或通知。”***线程池** *“核心队列最大数拒绝策略收尾正式临时要分清任务提交走流程。”*---**后记**这四个案例涵盖了多线程编程中最核心的设计思想——单例解决“唯一性”阻塞队列解决“协作与缓冲”定时器解决“延时调度”线程池解决“资源复用”。搞懂它们Java多线程面试的“拦路虎”就基本扫清了。