Skip to content
第 21 章 后端 ⏱ 12 分钟阅读

第 21 章:线程基础 ​

学习目标 ​

  • 建立"线程 = 工厂里的工人"的心智模型
  • 会创建线程,知道 3 种方式什么时候用哪个
  • 会用 synchronized / volatile 解决共享数据的问题
  • 分清"内存要加锁"和"数据库改行不用管"

一、线程 = 工厂里的工人 ​

先把模型立起来,后面所有概念都套这个:

术语工厂比喻你写代码时的意思
进程一整个工厂(独立厂房、自己的材料库)一个跑起来的 Java 程序
线程厂房里的工人程序里负责干活的一条执行线
共享变量所有工人共用的材料库多个线程都能读写的变量

为什么程序默认要多个工人? 一件事太慢。比如下 100 个文件,1 个工人只能一个个下;叫 10 个工人每人下 10 个,快 10 倍。Web 服务器同理:一个请求派一个工人处理,1000 个请求就 1000 个工人,大家不用排队。

⚠️ 坑 1:工人共用一个材料库,如果两个工人同时改同一个数,结果会乱。这是整个章节唯一要操心的事。

java
int count = 5;
// 工人 A 看了一眼箱子:5
// 工人 B 也看了一眼箱子:5
// A 写 6, B 也写 6 —— 明明加了两次,该是 7,结果却变 6!

因为 count++ 底层是"读 → +1 → 写回"三步,两个工人可能都读到旧的 5。这就叫线程不安全。

二、怎么叫工人干活:创建线程 ​

记住一句话:默认用 Runnable,需要拿返回值才用 Callable。

java
// ① 最常用:Runnable + Lambda(任务 = 一段代码)
Thread t = new Thread(() -> {
    System.out.println("工人开工:" + Thread.currentThread().getName());
});
t.start();                    // start() 才真正开一个新工人

// ② Callable:要返回值 / 要抛异常时才用
FutureTask<String> task = new FutureTask<>(() -> {
    Thread.sleep(1000);                // 模拟干 1 秒
    return "计算结果";
});
new Thread(task).start();             // 也要包 Thread 才能开
String result = task.get();           // 阻塞等结果,拿到 "计算结果"

为什么有 Callable? 因为 Runnable.run() 没返回值、也不能抛异常。当你想"这个工人干完给我个结果",Runnable 做不到,只能用 Callable。其他情况别用它。

⚠️ 坑 2:必须 start(),不是 run()。

java
Thread t = new Thread(() -> System.out.println("干活"));
t.run();    // ❌ 只是普通方法调用,还是当前线程在跑,没开新工人
t.start();  // ✅ 才真开一条新线程

三、线程状态:别背 6 种,关心 3 种就够 ​

网上都在画"6 状态图",但你写代码真正要关心的是 3 种:新建、运行、结束。其他状态是你用 wait / sleep / 加锁时自动进去的,不用你管。

java
Thread t = new Thread(() -> {});   // NEW:new 出来,还没 start
t.start();                         // RUNNABLE:能跑/在跑
t.join();                          // 等 t 跑完(当前线程挂起等它)
// t 结束 → TERMINATED

想亲眼看到状态?用 getState() 打印:

java
Thread t = new Thread(() -> { try { Thread.sleep(100); } catch (Exception e) {} });
System.out.println(t.getState());  // NEW(还没 start)
t.start();
Thread.sleep(50);
System.out.println(t.getState());  // TIMED_WAITING(sleep 睡觉中)
t.join();
System.out.println(t.getState());  // TERMINATED(干完收工)

💡 记法:sleep 是"定闹钟到点自醒",wait 是"排队等人叫",加锁排队是 BLOCKED。这些状态的出现=你在用对应的方法,不用背图。

四、解决"同时改":synchronized(让工人排队) ​

回到坑 1:两个工人同时改同一个 count。解决:在箱子前放把锁,想改的人排队,一次只放一个进去。

java
class Counter {
    private int count = 0;

    public synchronized void increment() {   // 加锁:同一时刻只有一个线程能进
        count++;
    }

    public synchronized int getCount() {     // 读也锁,保证读到最新
        return count;
    }
}

// 1000 个线程各加 100 次
Counter c = new Counter();
for (int i = 0; i < 1000; i++) {
    new Thread(() -> { for (int j = 0; j < 100; j++) c.increment(); }).start();
}
// 不加 synchronized → 大概率不是 100000;加了锁 → 一定是 100000

锁范围越小越好:能锁几行,别锁整个方法,否则慢。

java
public void increment() {
    // 不涉及共享的代码,别放进锁里
    synchronized (this) {   // 只锁要保护的那几行
        count++;
    }
}

五、volatile:一块"所有人随时可见"的公告板 ​

volatile 只解决一个场景:一个线程改,其他线程要立刻看到最新值。

java
class StopFlag {
    private volatile boolean stop = false;   // 公告板:一改,大家立刻看到
    public void shutdown() { stop = true; }
    public boolean shouldStop() { return stop; }
}

如果不加 volatile,其他线程可能一直盯着自己兜里抄的旧纸条(缓存的旧值),stop 改了却"看不见",程序停不下来。

⚠️ 坑 3:volatile 解决不了计数。它只管"看得见最新值",不管"同一时刻只有一个人改"。

java
volatile int count = 0;
count++;   // ❌ count++ 仍是"读→+1→写"三步,volatile 管不了这个
// 累加计数:必须用 synchronized 或 AtomicInteger

怎么选?

  • 开关/标志位(如停止标志)→ volatile
  • 累加计数 → synchronized 或 AtomicInteger

六、sleep vs wait:一个闹钟,一个排队 ​

Thread.sleep(ms)Object.wait()
是谁的Thread 静态方法Object 实例方法
谁会醒时间到自动醒要别人 notify() 叫醒
释放锁❌ 不释放✅ 释放
在哪用随便哪必须在 synchronized 块里
java
Thread.sleep(1000);   // 定闹钟:睡 1 秒,到点自醒,锁不放下

synchronized (lock) { // 排队办业务:把锁先交出去,等柜员叫号
    lock.wait();      // 挂起并释放锁,等别人 notify 再回来
    lock.notify();    // 唤醒一个在 wait 的线程(自己仍在锁内)
}

💡 记法:只想"停一下"→ sleep(不松锁,到点自醒);要"等某个条件满足"→ wait(松锁给别人,等 notify)。生产代码里 wait/notify 很少手写,大多用 Lock 或并发工具,这里懂概念即可。

七、关键澄清:什么时候才需要自己加锁? ​

新手最容易懵的就是这里。加锁只防"内存里的共享变量被抢";改数据库根本不用你管线程。

场景会不会同时?谁来保证安全
同一个 HTTP 请求(一次点击)❌ 不会,一个请求一个线程Tomcat 线程池
两个用户同时改同一行数据✅ 会,两个线程同时改一行数据库行锁,不是线程
两个线程抢改内存里的变量✅ 会synchronized
分布式多实例抢共享资源✅ 会分布式锁(后面架构章讲)

💡 记法:普通 CRUD 改数据库,MySQL 会给被改的那一行自动加行锁,同一行第二个请求会等第一个提交完,你不用写代码。synchronized 是给内存变量用的。分清这两个,线程就没那么可怕了。

八、本章小结 ​

想做什么用什么
起一个线程干活new Thread(() -> ...).start()
线程要返回结果FutureTask + Callable
防两个线程改同一变量synchronized 加锁
让别的线程看到最新值volatile
只想暂停一下Thread.sleep()
要等某个条件Object.wait()/notify()
累加计数AtomicInteger
改数据库某一行不用管,数据库行锁兜底

动手练习 ​

  1. 建线程:用 Runnable 起 5 个线程,各打印名字,看顺序是否固定
  2. 线程安全:写 Counter,先不开锁 1000 线程各加 100 次打印结果;加上 synchronized 再看
  3. volatile 场景:写一个 10 秒后自动停的循环,用 volatile boolean 控制退出,对比不加的效果
  4. sleep vs wait:试在 synchronized 外用 wait(),看报什么错,理解为什么必须加锁

下一章:第 22 章:线程池 →

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