Appearance
多线程
为什么需要多线程?
现代 CPU 都是多核的,如果程序只有一个线程,就只能利用一个核心,其他核心空闲浪费。多线程让程序同时做多件事,充分利用 CPU 资源。
具体场景:
- Web 服务器:Tomcat 为每个请求分配一个线程,并发处理数百个请求
- 异步处理:发邮件、写日志等耗时操作异步执行,不阻塞主流程
- 并行计算:大数据处理、图像处理等任务拆分为多个子任务并行执行
- GUI 应用:主线程处理 UI 响应,后台线程处理耗时操作,防止界面卡死
多线程的核心挑战:多个线程同时访问共享数据时,如何保证数据一致性和线程安全。
线程的生命周期
Java 线程有 6 种状态,定义在 Thread.State 枚举中:
[等待获取锁]
NEW → RUNNABLE ←→ BLOCKED
↓ ↑
WAITING TIMED_WAITING
↓ ↑
TERMINATED| 状态 | 说明 | 触发条件 |
|---|---|---|
NEW | 线程已创建,尚未启动 | new Thread() |
RUNNABLE | 正在运行或等待 CPU 调度 | thread.start() |
BLOCKED | 等待获取锁 | 进入 synchronized 块时锁被占用 |
WAITING | 无限期等待其他线程唤醒 | wait()、join()、LockSupport.park() |
TIMED_WAITING | 超时等待 | sleep(ms)、wait(ms)、join(ms) |
TERMINATED | 线程执行完毕 | run() 方法结束或异常退出 |
java
// 查看线程状态
Thread thread = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
});
System.out.println(thread.getState()); // NEW
thread.start();
System.out.println(thread.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println(thread.getState()); // TIMED_WAITING
thread.join();
System.out.println(thread.getState()); // TERMINATED创建线程的方式
1. 继承 Thread 类
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
MyThread thread = new MyThread();
thread.start(); // 注意:调用 start() 而非 run()2. 实现 Runnable 接口
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
// Lambda 简化
new Thread(() -> System.out.println("running")).start();推荐使用 Runnable:Java 单继承,实现接口更灵活,且任务与线程解耦。
3. 实现 Callable + Future
Runnable 没有返回值也不能抛异常,Callable 弥补了这两个缺陷:
java
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "任务完成";
}
}
// 通过 FutureTask 包装
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
// 获取结果(会阻塞直到计算完成)
String result = futureTask.get(); // 阻塞等待
// String result = futureTask.get(2, TimeUnit.SECONDS); // 超时等待4. CompletableFuture(Java 8+)
更强大的异步编程工具,支持链式调用和组合:
java
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
// 异步任务
return "Hello";
})
.thenApply(result -> result + " World") // 转换结果
.thenAccept(System.out::println) // 消费结果
.exceptionally(ex -> { // 异常处理
System.out.println("异常: " + ex);
return null;
});
// 等待完成
future.join();创建方式对比
| 方式 | 返回值 | 抛异常 | 适用场景 |
|---|---|---|---|
Thread | 无 | 不能抛 checked | 简单场景 |
Runnable | 无 | 不能抛 checked | 通用任务,推荐 |
Callable + Future | 有 | 可以抛 | 需要返回结果 |
CompletableFuture | 有 | 可以抛 | 复杂异步编排 |
synchronized 关键字
synchronized 是 Java 最基础的同步机制,基于对象头中的 Monitor 实现。
三种使用方式
java
// 1. 同步代码块 — 指定锁对象
public void method() {
synchronized (this) { // 以当前对象为锁
// 临界区
}
}
// 2. 同步实例方法 — 锁是 this
public synchronized void instanceMethod() {
// 等价于 synchronized(this) { ... }
}
// 3. 同步静态方法 — 锁是 Class 对象
public static synchronized void staticMethod() {
// 等价于 synchronized(MyClass.class) { ... }
}Monitor 机制
每个 Java 对象都有一个 Monitor(监视器锁),存储在对象头中:
java
// 对象头结构(64位 JVM)
// |--------------------------------------------------------------------|
// | Mark Word (64 bits) |
// |--------------------------------------------------------------------|
// | 锁标志位(2) | 偏向锁位(1) | 分代年龄(4) | identity_hashcode(31) | ... |
// |--------------------------------------------------------------------|当线程进入 synchronized 块时:
- 尝试获取 Monitor 的所有权
- 如果 Monitor 的计数器为 0,线程获得锁,计数器 +1
- 如果同一个线程重入,计数器继续 +1(可重入)
- 退出 synchronized 块时计数器 -1,到 0 时释放锁
锁升级过程
JVM 对 synchronized 进行了大量优化,锁会根据竞争情况自动升级(不可降级):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁| 锁状态 | 适用场景 | 原理 | 开销 |
|---|---|---|---|
| 偏向锁 | 只有一个线程访问 | 在对象头记录线程 ID,该线程再次进入无需 CAS | 极低 |
| 轻量级锁 | 多线程交替访问(不竞争) | CAS 自旋尝试获取锁 | 低(自旋消耗 CPU) |
| 重量级锁 | 多线程激烈竞争 | 操作系统互斥量,未获取到锁的线程阻塞 | 高(线程切换开销) |
java
// 查看锁状态(需要 JOL 依赖)
// import org.openjdk.jol.info.ClassLayout;
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());volatile 关键字
可见性
volatile 保证变量的可见性:一个线程修改了 volatile 变量,其他线程能立即看到最新值。
java
class FlagExample {
private volatile boolean running = true;
public void stop() {
running = false; // 线程 A 修改
}
public void run() {
while (running) { // 线程 B 能立即看到修改
// 如果没有 volatile,线程 B 可能永远看不到 running 的变化
}
}
}happens-before 原则
volatile 建立了 happens-before 关系:
- volatile 写 happens-before 后续的 volatile 读
- 线程 A 写 volatile 变量前的所有操作,对线程 B 读 volatile 变量后的所有操作可见
java
// 线程 A
data = 123; // 1. 写普通变量
flag = true; // 2. 写 volatile 变量
// 线程 B
if (flag) { // 3. 读 volatile 变量
int x = data; // 4. 读普通变量 —— 一定能看到 data = 123
}volatile 不保证原子性
java
class Counter {
private volatile int count = 0;
public void increment() {
count++; // 不是原子操作!读-改-写 三步
}
}
// 多线程并发 increment() 会导致计数丢失
// 需要使用 synchronized 或 AtomicIntegervolatile vs synchronized
| 维度 | volatile | synchronized |
|---|---|---|
| 保证可见性 | 是 | 是 |
| 保证原子性 | 否 | 是 |
| 禁止指令重排 | 是 | 是(但范围有限) |
| 线程阻塞 | 否 | 是(重量级锁时) |
| 适用场景 | 状态标志、DCL 单例 | 复合操作、临界区 |
wait / notify 机制
wait()、notify()、notifyAll() 是 Object 类的方法,用于线程间通信,必须在 synchronized 块内调用。
生产者-消费者模型
java
class Buffer {
private int data;
private boolean empty = true;
public synchronized void produce(int value) {
while (!empty) { // 用 while 而非 if,防止虚假唤醒
try {
wait(); // 释放锁,等待消费者消费
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
data = value;
empty = false;
System.out.println("生产: " + value);
notifyAll(); // 唤醒等待的消费者
}
public synchronized int consume() {
while (empty) {
try {
wait(); // 释放锁,等待生产者生产
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
}
}
empty = true;
System.out.println("消费: " + data);
notifyAll(); // 唤醒等待的生产者
return data;
}
}关键点
- wait() 释放锁,sleep() 不释放锁
- 必须在 synchronized 中调用,否则抛
IllegalMonitorStateException - 用
while而非if检查条件,防止虚假唤醒 - notifyAll() 优于 notify(),避免信号丢失
ThreadLocal
ThreadLocal 为每个线程提供独立的变量副本,线程间互不干扰。
基本原理
java
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 从当前线程取值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) return (T) e.value;
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) map.set(this, value);
else createMap(t, value);
}
}每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是副本值。
使用示例
java
// 典型用法:数据库连接、Session 管理
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = createConnection();
connectionHolder.set(conn);
}
return conn;
}
public static void removeConnection() {
connectionHolder.remove(); // 重要:防止内存泄漏
}
}内存泄漏风险
ThreadLocalMap 的 key(ThreadLocal 对象)是弱引用,但 value 是强引用。如果 ThreadLocal 被 GC 回收(key 变为 null),但线程还在(线程池场景),value 无法被访问也无法被回收,造成内存泄漏。
解决方案:使用完 ThreadLocal 后务必调用 remove()。
线程池
为什么需要线程池?
线程的创建和销毁开销很大(分配内存、栈空间、系统调用)。线程池复用已有线程,减少创建/销毁开销,同时控制并发数量。
ThreadPoolExecutor 核心参数
java
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) // 拒绝策略线程池工作流程
新任务提交
↓
核心线程数未满? → 是 → 创建新线程执行
↓ 否
任务队列未满? → 是 → 放入队列等待
↓ 否
最大线程数未满? → 是 → 创建新线程执行
↓ 否
执行拒绝策略常用线程池
| 线程池 | 特点 | 适用场景 |
|---|---|---|
newFixedThreadPool(n) | 固定大小 | 稳定并发量 |
newCachedThreadPool() | 可伸缩,60s 回收 | 短时异步任务 |
newSingleThreadExecutor() | 单线程顺序执行 | 需要顺序处理 |
newScheduledThreadPool(n) | 支持定时和周期 | 定时任务 |
推荐使用
new ThreadPoolExecutor()显式创建,而不是Executors的工厂方法。Executors创建的线程池可能使用无界队列(LinkedBlockingQueue),会导致 OOM。
拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) | 抛 RejectedExecutionException |
CallerRunsPolicy | 由提交任务的线程自己执行 |
DiscardPolicy | 直接丢弃,不抛异常 |
DiscardOldestPolicy | 丢弃队列中最旧的任务 |
完整示例
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadFactory() { // 自定义线程工厂
private int count = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "pool-thread-" + count++);
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交任务
executor.execute(() -> System.out.println("Runnable 任务"));
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Callable 完成";
});
// 优雅关闭
executor.shutdown(); // 不再接受新任务,等待已有任务完成
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 强制终止
}
} catch (InterruptedException e) {
executor.shutdownNow();
}AQS(AbstractQueuedSynchronizer)
AQS 是整个 JUC(java.util.concurrent)包的基石。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等底层都是基于 AQS 实现的。
核心思想
AQS 维护一个 volatile int state(同步状态)和一个 FIFO 等待队列(CLH 队列变体):
java
// AQS 简化结构
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态
private transient volatile Node head; // 等待队列头
private transient volatile Node tail; // 等待队列尾
// 需要子类实现的方法
protected boolean tryAcquire(int arg) { ... }
protected boolean tryRelease(int arg) { ... }
protected int tryAcquireShared(int arg) { ... }
protected boolean tryReleaseShared(int arg) { ... }
}工作流程
- 线程尝试获取锁(CAS 修改 state)
- 获取成功,state +1,线程执行业务
- 获取失败,线程被包装成 Node 加入等待队列尾部
- 前驱节点释放锁后唤醒后继节点
java
// ReentrantLock 基于 AQS 的核心逻辑(简化)
class ReentrantLock {
private Sync sync = new NonfairSync();
public void lock() {
sync.acquire(1); // 调用 AQS 的 acquire()
}
public void unlock() {
sync.release(1); // 调用 AQS 的 release()
}
}常见面试题
Q1: start() 和 run() 的区别?
start() 会创建新线程并在新线程中执行 run();直接调用 run() 就是在当前线程执行普通方法,不会创建新线程。
Q2: sleep() 和 wait() 的区别?
| 维度 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 释放锁 | 不释放 | 释放 |
| 调用要求 | 任何地方 | 必须在 synchronized 中 |
| 唤醒方式 | 超时自动唤醒 | 需要 notify/notifyAll |
Q3: synchronized 和 ReentrantLock 的区别?
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现 | JVM 层面,关键字 | API 层面,Java 类 |
| 锁释放 | 自动释放 | 必须 finally 中 unlock |
| 可中断 | 不可中断 | 可中断(lockInterruptibly) |
| 公平锁 | 非公平 | 可选公平/非公平 |
| 条件 | 一个条件 | 多个 Condition |
| 性能 | 优化后相当 | 优化后相当 |
Q4: volatile 能保证线程安全吗?
不能保证原子性。volatile 只保证可见性和禁止指令重排,对 count++ 这样的复合操作(读-改-写)无能为力。需要用 synchronized、ReentrantLock 或 AtomicInteger。
Q5: ThreadLocal 内存泄漏的原因和解决方案?
ThreadLocalMap 的 key 是弱引用,但 value 是强引用。当 ThreadLocal 没有外部强引用时被 GC 回收,key 变为 null,但 value 无法被回收(线程池线程长期存活)。解决方案:使用完 ThreadLocal 后调用 remove() 方法。
Q6: 线程池的核心参数有哪些?
corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(空闲线程存活时间)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。
Q7: 为什么推荐用 ThreadPoolExecutor 直接创建而不是 Executors 工厂方法?
Executors.newFixedThreadPool() 和 newSingleThreadExecutor() 使用 LinkedBlockingQueue(无界队列),newCachedThreadPool() 允许创建无限线程数,都可能导致 OOM。直接创建 ThreadPoolExecutor 可以指定有界队列和拒绝策略,更安全可控。
Q8: 什么是虚假唤醒(spurious wakeup)?
线程在没有被通知、中断或超时的情况下意外醒来。这是操作系统层面的问题。因此 wait 条件判断必须用 while 循环而非 if,确保唤醒后重新检查条件。
Q9: AQS 是什么?它的作用?
AQS(AbstractQueuedSynchronizer)是 JUC 包的核心框架,通过维护一个 volatile int state 同步状态和一个 FIFO 等待队列,实现了锁和同步器的通用基础。ReentrantLock、CountDownLatch、Semaphore 等都是基于 AQS 实现的。
