Skip to content
第 38 / 250 章后端⏱ 10 分钟阅读

第 38 章:多线程基础

学习目标

  • 理解进程、线程、并发、并行
  • 掌握线程创建的四种方式
  • 理解线程安全问题的根源
  • 掌握 synchronized 与 volatile

一、基本概念

概念说明
进程一个运行中的程序,独立内存空间
线程进程内的执行单元,共享进程内存
并发多个任务交替执行(单核也能并发)
并行多个任务同时执行(必须多核)

共享堆内存 = 线程安全问题的根源。多个线程改同一个对象,就可能出错。

二、创建线程的四种方式

java
// ① 继承 Thread(不推荐:Java 单继承,占用了继承名额)
class MyThread extends Thread {
    @Override
    public void run() { System.out.println("running"); }
}
new MyThread().start();

// ② 实现 Runnable(推荐:解耦任务和线程)
new Thread(() -> System.out.println("running")).start();

// ③ 实现 Callable(有返回值 + 能抛异常)
FutureTask<String> task = new FutureTask<>(() -> "结果");
new Thread(task).start();
String result = task.get();       // 阻塞等待结果

// ④ 线程池(生产环境唯一正确姿势,见第 39 章)
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<String> f = pool.submit(() -> "结果");
方式返回值异常推荐度
Thread只能内部处理
Runnable只能内部处理⭐⭐⭐
Callable可抛出,get() 时接到⭐⭐⭐⭐
线程池同上⭐⭐⭐⭐⭐

⚠️ 必须调 start() 不能调 run()run() 只是普通方法调用,还在当前线程执行,压根没开新线程。

三、线程生命周期

状态说明
NEW已创建未启动
RUNNABLE可运行(含就绪和运行中)
BLOCKED等待 synchronized
WAITING无限等待,需别人唤醒
TIMED_WAITING限时等待
TERMINATED已结束

四、常用方法

java
Thread.sleep(1000);              // ① 睡眠,不释放锁
thread.join();                   // ② 等待该线程结束
thread.interrupt();              // ③ 设置中断标记(不是强制停止!)
Thread.currentThread().getName();
thread.setDaemon(true);          // ④ 守护线程,主线程结束它就结束(必须在 start 前设置)

sleep vs wait

维度Thread.sleep()Object.wait()
所属Thread 静态方法Object 实例方法
释放锁不释放✅ 释放
唤醒时间到自动醒notify() / notifyAll()
使用位置任意必须在 synchronized 块内

如何优雅停止线程?

java
// ❌ stop() 已废弃:强制杀死会导致资源不释放、数据不一致

// ✅ 用中断标记
public class Worker implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {    // ① 检查中断标记
            try {
                doWork();
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();          // ② 恢复中断标记!
                break;                                        // ③ 退出循环
            }
        }
        cleanup();                                            // ④ 优雅清理
    }
}

② 为什么要恢复中断标记?sleep() 抛出 InterruptedException 时会清除中断标记。不恢复的话,外层的 isInterrupted() 会返回 false,循环继续跑,中断请求就失效了。

五、线程安全问题的根源

java
public class Counter {
    private int count = 0;

    public void increment() {
        count++;         // ❌ 看起来一行,实际是三步:读 → 加 1 → 写回
    }
}

// 1000 个线程各加 1000 次,结果远小于 100 万

三大特性

特性含义破坏原因
原子性操作不可分割count++ 是三步
可见性一个线程改了,别的线程立刻看到CPU 缓存未同步到主内存
有序性代码按写的顺序执行编译器/CPU 指令重排

六、synchronized

java
// ① 同步方法:锁的是 this
public synchronized void increment() { count++; }

// ② 同步静态方法:锁的是 Counter.class
public static synchronized void staticMethod() { }

// ③ 同步代码块:锁范围更小,性能更好(推荐)
public void increment() {
    // 非临界区代码...
    synchronized (this) {          // 只锁必要的部分
        count++;
    }
}

// ④ 自定义锁对象(最清晰)
private final Object lock = new Object();
public void increment() {
    synchronized (lock) { count++; }
}

synchronized 保证三大特性全部满足

锁升级(JDK 6+ 优化)

所以「synchronized 很慢」是老黄历了。无竞争场景下开销极小。

坑:锁错对象

java
// ❌ 锁 Integer:自动装箱后每次都是不同对象,锁不住
private Integer lock = 0;
synchronized (lock) { lock++; }     // ❌ lock++ 后换了个对象

// ❌ 锁 String 字面量:常量池共享,可能和别的代码抢同一把锁
synchronized ("lock") { }

// ✅ 用 final Object
private final Object lock = new Object();

七、volatile

java
public class Flag {
    private volatile boolean running = true;   // ① 保证可见性

    public void stop() { running = false; }

    public void run() {
        while (running) { }    // ② 没有 volatile 时,可能永远读到缓存的 true
    }
}
保证volatilesynchronized
可见性
有序性✅(禁止重排)
原子性
java
// ❌ volatile 不保证原子性
private volatile int count = 0;
count++;      // 依然是三步,依然线程不安全

// ✅ 用 AtomicInteger(CAS 无锁)
private final AtomicInteger count = new AtomicInteger();
count.incrementAndGet();

volatile 适用场景一写多读的状态标记位。涉及「读-改-写」的一律用锁或原子类。

经典应用:双重检查锁单例

java
public class Singleton {

    private static volatile Singleton instance;    // ① volatile 不能省!

    private Singleton() { }

    public static Singleton getInstance() {
        if (instance == null) {                    // ② 第一次检查:避免每次都加锁
            synchronized (Singleton.class) {
                if (instance == null) {            // ③ 第二次检查:防止重复创建
                    instance = new Singleton();    // ④ 这行不是原子操作!
                }
            }
        }
        return instance;
    }
}

④ 为什么必须加 volatile?new Singleton() 实际分三步:(1) 分配内存 (2) 初始化对象 (3) 引用指向内存。 JVM 可能重排为 (1)(3)(2)。此时另一个线程在 ② 处看到 instance != null 就直接返回了一个还没初始化完成的对象,使用时报错。 volatile 禁止这种重排。

八、本章小结

要点关键
创建线程生产环境只用线程池
start() vs run()只有 start 才开新线程
停止线程用中断标记,别用 stop
三大特性原子性 / 可见性 / 有序性
synchronized三个都保证,有锁升级优化
volatile只保证可见性 + 有序性
双检锁volatile 不能省

动手练习

练习 1:基础题

写一个多线程计数器,10 个线程各加 10000 次。分别用「不加锁」「synchronized」「AtomicInteger」三种方式,对比结果和耗时。

练习 2:进阶题

wait / notify 实现生产者-消费者模型:容量为 10 的缓冲区,3 个生产者、2 个消费者。


下一章第 39 章:线程池

本站基于 VitePress 构建 · 由 Codebook 团队维护