Skip to content

多线程

为什么需要多线程?

现代 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 块时:

  1. 尝试获取 Monitor 的所有权
  2. 如果 Monitor 的计数器为 0,线程获得锁,计数器 +1
  3. 如果同一个线程重入,计数器继续 +1(可重入)
  4. 退出 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 或 AtomicInteger

volatile vs synchronized

维度volatilesynchronized
保证可见性
保证原子性
禁止指令重排是(但范围有限)
线程阻塞是(重量级锁时)
适用场景状态标志、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;
    }
}

关键点

  1. wait() 释放锁,sleep() 不释放锁
  2. 必须在 synchronized 中调用,否则抛 IllegalMonitorStateException
  3. while 而非 if 检查条件,防止虚假唤醒
  4. 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)包的基石。ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 等底层都是基于 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) { ... }
}

工作流程

  1. 线程尝试获取锁(CAS 修改 state)
  2. 获取成功,state +1,线程执行业务
  3. 获取失败,线程被包装成 Node 加入等待队列尾部
  4. 前驱节点释放锁后唤醒后继节点
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()
所属类ThreadObject
释放锁不释放释放
调用要求任何地方必须在 synchronized 中
唤醒方式超时自动唤醒需要 notify/notifyAll

Q3: synchronized 和 ReentrantLock 的区别?

维度synchronizedReentrantLock
实现JVM 层面,关键字API 层面,Java 类
锁释放自动释放必须 finally 中 unlock
可中断不可中断可中断(lockInterruptibly)
公平锁非公平可选公平/非公平
条件一个条件多个 Condition
性能优化后相当优化后相当

Q4: volatile 能保证线程安全吗?

不能保证原子性。volatile 只保证可见性和禁止指令重排,对 count++ 这样的复合操作(读-改-写)无能为力。需要用 synchronizedReentrantLockAtomicInteger

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 等待队列,实现了锁和同步器的通用基础。ReentrantLockCountDownLatchSemaphore 等都是基于 AQS 实现的。