第 21 章:线程基础
学习目标
- 建立"线程 = 工厂里的工人"的心智模型
- 会创建线程,知道 3 种方式什么时候用哪个
- 会用 synchronized / volatile 解决共享数据的问题
- 分清"内存要加锁"和"数据库改行不用管"
一、线程 = 工厂里的工人
先把模型立起来,后面所有概念都套这个:
| 术语 | 工厂比喻 | 你写代码时的意思 |
|---|---|---|
| 进程 | 一整个工厂(独立厂房、自己的材料库) | 一个跑起来的 Java 程序 |
| 线程 | 厂房里的工人 | 程序里负责干活的一条执行线 |
| 共享变量 | 所有工人共用的材料库 | 多个线程都能读写的变量 |
为什么程序默认要多个工人? 一件事太慢。比如下 100 个文件,1 个工人只能一个个下;叫 10 个工人每人下 10 个,快 10 倍。Web 服务器同理:一个请求派一个工人处理,1000 个请求就 1000 个工人,大家不用排队。
⚠️ 坑 1:工人共用一个材料库,如果两个工人同时改同一个数,结果会乱。这是整个章节唯一要操心的事。
javaint count = 5; // 工人 A 看了一眼箱子:5 // 工人 B 也看了一眼箱子:5 // A 写 6, B 也写 6 —— 明明加了两次,该是 7,结果却变 6!因为
count++底层是"读 → +1 → 写回"三步,两个工人可能都读到旧的 5。这就叫线程不安全。
二、怎么叫工人干活:创建线程
记住一句话:默认用
Runnable,需要拿返回值才用Callable。
// ① 最常用: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()。javaThread t = new Thread(() -> System.out.println("干活")); t.run(); // ❌ 只是普通方法调用,还是当前线程在跑,没开新工人 t.start(); // ✅ 才真开一条新线程
三、线程状态:别背 6 种,关心 3 种就够
网上都在画"6 状态图",但你写代码真正要关心的是 3 种:新建、运行、结束。其他状态是你用 wait / sleep / 加锁时自动进去的,不用你管。
Thread t = new Thread(() -> {}); // NEW:new 出来,还没 start
t.start(); // RUNNABLE:能跑/在跑
t.join(); // 等 t 跑完(当前线程挂起等它)
// t 结束 → TERMINATED想亲眼看到状态?用 getState() 打印:
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。解决:在箱子前放把锁,想改的人排队,一次只放一个进去。
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锁范围越小越好:能锁几行,别锁整个方法,否则慢。
public void increment() {
// 不涉及共享的代码,别放进锁里
synchronized (this) { // 只锁要保护的那几行
count++;
}
}五、volatile:一块"所有人随时可见"的公告板
volatile 只解决一个场景:一个线程改,其他线程要立刻看到最新值。
class StopFlag {
private volatile boolean stop = false; // 公告板:一改,大家立刻看到
public void shutdown() { stop = true; }
public boolean shouldStop() { return stop; }
}如果不加 volatile,其他线程可能一直盯着自己兜里抄的旧纸条(缓存的旧值),stop 改了却"看不见",程序停不下来。
⚠️ 坑 3:
volatile解决不了计数。它只管"看得见最新值",不管"同一时刻只有一个人改"。javavolatile int count = 0; count++; // ❌ count++ 仍是"读→+1→写"三步,volatile 管不了这个 // 累加计数:必须用 synchronized 或 AtomicInteger
怎么选?
- 开关/标志位(如停止标志)→
volatile - 累加计数 →
synchronized或AtomicInteger
六、sleep vs wait:一个闹钟,一个排队
Thread.sleep(ms) | Object.wait() | |
|---|---|---|
| 是谁的 | Thread 静态方法 | Object 实例方法 |
| 谁会醒 | 时间到自动醒 | 要别人 notify() 叫醒 |
| 释放锁 | ❌ 不释放 | ✅ 释放 |
| 在哪用 | 随便哪 | 必须在 synchronized 块里 |
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 |
| 改数据库某一行 | 不用管,数据库行锁兜底 |
动手练习
- 建线程:用
Runnable起 5 个线程,各打印名字,看顺序是否固定 - 线程安全:写
Counter,先不开锁 1000 线程各加 100 次打印结果;加上synchronized再看 - volatile 场景:写一个 10 秒后自动停的循环,用
volatile boolean控制退出,对比不加的效果 - sleep vs wait:试在
synchronized外用wait(),看报什么错,理解为什么必须加锁
下一章:第 22 章:线程池 →