多线程核心案例
2026/8/30 21:56:27 网站建设 项目流程

# Java多线程面试专项:单例、阻塞队列、定时器、线程池(核心案例篇)

> 本文聚焦Java多线程面试中的 **四大经典案例**——单例模式、阻塞队列+生产者消费者、定时器、线程池。这些案例是面试的高频考点,也是理解并发编程思想的最佳入口。


## 一、单例模式(Singleton Pattern)

### 1.1 什么是单例模式?

单例模式是一种创建型设计模式,确保一个类在JVM中**有且仅有一个实例**,并提供一个全局访问点。

**常见场景**:
- 数据库连接池(`DataSource`)
- 配置管理类
- 日志记录器
- 线程池实例

### 1.2 饿汉模式(Eager Initialization)

```java
class SingletonHungry {
// 类加载时即创建实例,JVM保证线程安全
private static final SingletonHungry instance = new SingletonHungry();

private SingletonHungry() {} // 构造方法私有化

public static SingletonHungry getInstance() {
return instance;
}
}
```

**特点**:
- ✅ 线程安全(由JVM类加载机制保证)
- ✅ 实现简单
- ❌ 类加载即创建,如果从未使用则浪费内存

### 1.3 懒汉模式(Lazy Initialization)

**① 单线程版(线程不安全)**

```java
class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {}

public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy(); // 多线程下可能创建多个
}
return instance;
}
}
```

**为什么线程不安全?** 两个线程同时调用`getInstance()`,都判断`instance == null`,然后各自创建一个实例,违反了单例原则。

**② 加锁版(安全但性能差)**

```java
class SingletonSync {
private static SingletonSync instance = null;
private SingletonSync() {}

public static synchronized SingletonSync getInstance() {
if (instance == null) {
instance = new SingletonSync();
}
return instance;
}
}
```

**问题**:每次调用`getInstance()`都要加锁,而**线程安全问题只发生在首次创建时**。一旦实例创建完毕,后续的调用都不需要同步,加锁造成了不必要的性能损耗。

**③ 双重检查锁(DCL,面试必考)**

```java
class 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 标准库的使用

```java
BlockingQueue<Integer> 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 手动实现阻塞队列(面试手写题)

```java
public 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 标准库使用

```java
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3秒后执行");
}
}, 3000);
```

### 3.3 定时器的核心设计(面试重点)

**需求**:实现一个可同时管理**多个**定时任务的定时器,每个任务在指定的延迟后执行。

**数据结构**:优先级队列(按执行时间排序,队首是最先需要执行的任务)

```java
public class MyTimer {
// 优先级队列:按执行时间排序,队首是最近需要执行的任务
private PriorityQueue<MyTask> 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 Comparable<MyTask> {
Runnable runnable;
long time; // 绝对执行时间戳

public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}

@Override
public 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七大核心参数

```java
new ThreadPoolExecutor(
int corePoolSize, // 核心线程数(正式员工)
int maximumPoolSize, // 最大线程数(正式+临时)
long keepAliveTime, // 临时工空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
```

### 4.4 任务提交流程(面试必考)

```
提交任务

核心线程数已满? → 否 → 创建核心线程执行
↓ 是
阻塞队列已满? → 否 → 任务入队等待
↓ 是
最大线程数已满? → 否 → 创建临时线程执行
↓ 是
执行拒绝策略
```

### 4.5 拒绝策略(RejectedExecutionHandler)

| 策略 | 行为 |
| :--- | :--- |
| `AbortPolicy`(默认) | 抛出`RejectedExecutionException` |
| `CallerRunsPolicy` | 谁提交的任务谁自己执行(回调调用者线程) |
| `DiscardOldestPolicy` | 丢弃队列中最老的任务,重新提交新任务 |
| `DiscardPolicy` | 静默丢弃新任务,不抛异常 |

### 4.6 手动实现一个简易线程池

```java
public class MyThreadPool {
private BlockingQueue<Runnable> 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. 能充分利用多核CPU
5. IO等待期间可执行其他计算任务
6. 计算密集型任务可拆分到多个线程
7. IO密集型任务可通过线程重叠IO等待时间


## 六、总结:面试记忆口诀

**单例模式**:
> *“饿汉加载快又稳,懒汉DCL双重锁,volatile防重排,面试必考要记牢。”*

**生产者消费者**:
> *“阻塞队列做缓冲,削峰填谷解耦合,put/take有阻塞,while循环防假醒。”*

**定时器**:
> *“优先队列按时间,队首任务先执行,工作线程循环扫,wait到点或通知。”*

**线程池**:
> *“核心队列最大数,拒绝策略收尾,正式临时要分清,任务提交走流程。”*

---

**后记**:这四个案例涵盖了多线程编程中最核心的设计思想——单例解决“唯一性”,阻塞队列解决“协作与缓冲”,定时器解决“延时调度”,线程池解决“资源复用”。搞懂它们,Java多线程面试的“拦路虎”就基本扫清了。💪😊

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询