主题
界面
主题
并发让程序"同时做几件事":一边处理请求,一边写日志,一边等数据库返回。
本节只讲入门的核心:怎么创建线程、什么叫线程不安全、怎么让它安全。并发编程的完整知识(线程池调优、锁优化、并发容器、JMM)在 S4 企业开发进阶 展开,本节的目标是建立一个不会写错的底线认知。
完成本节后,你应该能够:
start() 而不是 run();volatile 能做什么、不能做什么(这是本节最重要的一个点);synchronized 与 AtomicInteger 两种方式保证计数正确;三个现实:
第一,CPU 是多核的。 单线程程序只能用上一个核,其余核闲着。
第二,等待是常态。 一次数据库查询可能要 10 ms,这 10 ms 里 CPU 没事干。多线程让"等待"与"干活"重叠。
第三,也是最关键的——并发是 bug 的温床。 同一份代码,单线程跑一万次都对,多线程跑十次可能错一次。这种"偶发错误"是生产事故里最难排查的一类。
看一个真实的错觉:
private volatile int count; // 很多人的第一反应:加 volatile 就线程安全了
void increment() {
count++; // 但这一行不是原子操作!
}count++ 实际是三步:读 count → 加 1 → 写回。两个线程可能同时读到 100,各自加 1 后都写回 101——一次自增凭空消失。本节会让你亲眼看到这个现象。
| 进程 | 线程 | |
|---|---|---|
| 是什么 | 运行中的程序实例 | 进程内的执行单元 |
| 资源 | 独立内存空间 | 共享进程的内存 |
| 开销 | 大(创建、切换) | 小 |
| 通信 | 进程间通信(IPC) | 直接读写共享变量(也是危险来源) |
关键点:线程共享内存既带来了高效通信,也带来了数据竞争。
| 状态 | 含义 |
|---|---|
NEW | 已创建,还没 start() |
RUNNABLE | 可运行(可能在等 CPU 时间片) |
BLOCKED | 等待获取 synchronized 锁 |
WAITING | 无限期等待(wait()、join()) |
TIMED_WAITING | 限时等待(sleep(ms)) |
TERMINATED | 已结束 |
注意:sleep() 只让出 CPU,不释放锁;wait() 会释放锁。这是高频面试点,S4 并发章节会深入。
// 方式一:继承 Thread(不推荐:占用唯一的继承名额)
class MyThread extends Thread {
@Override
public void run() { }
}
// 方式二:实现 Runnable(推荐的"传任务"写法)
Runnable task = () -> System.out.println("任务执行");
new Thread(task).start();
// 方式三:实现 Callable(有返回值、能抛异常)
Callable<Integer> callable = () -> 6 * 7;
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
futureTask.get(); // 42为什么推荐"传任务":任务与线程解耦。同一个 Runnable 可以交给普通线程、线程池、虚拟线程——《Java 9-21 新特性与 Record》 的虚拟线程演示就靠这一点。
必须调用 start() 而不是 run():
| 调用 | 结果 |
|---|---|
thread.start() | 启动新线程,由新线程执行 run() |
thread.run() | 只是普通方法调用,在当前线程执行 |
写错了不会报错,只是"悄悄变成了单线程"——这是并发 bug 的经典伪装。
| 要素 | 含义 | 破坏它的典型场景 |
|---|---|---|
| 原子性 | 操作不可分割 | count++(读改写三步) |
| 可见性 | 一个线程的修改,其他线程能看见 | 缓存、编译器优化 |
| 有序性 | 代码执行顺序符合预期 | 指令重排 |
volatile 保证可见性与有序性(禁止指令重排),但不保证原子性:
private volatile int count;
void increment() {
count++; // 可见:能读到最新值;不原子:读改写仍可能交错
}synchronized 同时解决三个问题(互斥 + 内存屏障),代价是同一时刻只有一个线程能进入。
AtomicInteger 用 CAS 解决原子性,无锁,并发度高,但只适用于单变量操作。
// 1. 修饰实例方法:锁是 this
synchronized void increment() { count++; }
// 2. 修饰静态方法:锁是 Class 对象
static synchronized void staticIncrement() { }
// 3. 修饰代码块:锁是括号里的对象(推荐,粒度更细)
synchronized (lock) {
count++;
}关键认知:synchronized 锁的是对象,不是代码。两个线程用不同的对象锁,起不到互斥作用。
粒度选择:锁的范围越小越好(减少等待),但必须覆盖需要保护的完整操作。锁范围过小会把非原子操作露在外面,等于没锁。
死锁的四个必要条件(破坏任意一个即可避免):
| 条件 | 含义 | 破坏方式 |
|---|---|---|
| 互斥 | 资源同时只能被一个线程持有 | 无法破坏(锁的本质) |
| 持有并等待 | 拿着一个锁,再等另一个锁 | 一次性申请全部锁 |
| 不可剥夺 | 已持有的锁不能被抢走 | 用 tryLock 超时放弃 |
| 循环等待 | A 等 B,B 等 A | 固定加锁顺序 ✓ 最实用 |
实践中最常用的两招:
tryLock 带超时(拿不到就放弃,不无限等待)。创建线程有成本(栈内存、系统调用),切换线程也有成本。所以:
// 反模式:每个任务创建一个线程
for (int i = 0; i < 10_000; i++) {
new Thread(task).start(); // 创建 1 万个线程
}
// 正确:交给线程池复用
try (ExecutorService pool = Executors.newFixedThreadPool(200)) {
for (int i = 0; i < 10_000; i++) {
pool.submit(task); // 200 个线程轮流干
}
}线程池的完整知识(核心参数、拒绝策略、监控)在 S4 章节;本节只需建立"线程要复用"的直觉。
Thread worker = new Thread(() -> System.out.println("子线程执行"), "worker-1");
System.out.println(worker.getState()); // NEW
worker.start();
worker.join(); // 主线程等它结束
System.out.println(worker.getState()); // TERMINATEDclass Counter {
private int count;
synchronized void increment() { count++; } // 去掉 synchronized 就会丢更新
int value() { return count; }
}private volatile int count;
void increment() {
count++; // 错!volatile 不保证原子性
}这会读到"看起来对、偶尔错"的结果——比直接崩溃更难排查。第 7 节的实测会把两种写法放在一起对比。
文件:labs/concurrency/src/main/java/cn/kb/learn/concurrent/ConcurrencyLesson.java(节选核心部分)
package cn.kb.learn.concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntSupplier;
public class ConcurrencyLesson {
static final int THREADS = 4;
static final int TIMES = 50_000;
static final int EXPECTED = THREADS * TIMES;
/** 普通 int:既没有可见性保证,也没有原子性保证 */
static class UnsafeCounter {
private int count;
void increment() { count++; }
int value() { return count; }
}
/** volatile int:保证可见性与有序性,count++ 仍然不是原子操作 */
static class VolatileCounter {
private volatile int count;
void increment() { count++; }
int value() { return count; }
}
/** synchronized:互斥 + 可见性,结果正确但同一时刻只有一个线程能进入 */
static class SyncCounter {
private int count;
synchronized void increment() { count++; }
int value() { return count; }
}
/** AtomicInteger:CAS 无锁操作,结果正确且并发度更高 */
static class AtomicCounter {
private final AtomicInteger count = new AtomicInteger();
void increment() { count.incrementAndGet(); }
int value() { return count.get(); }
}
/** 启动若干线程各自累加,返回最终读数 */
static int countWith(Runnable increment, IntSupplier reader) throws InterruptedException {
Thread[] workers = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
workers[i] = new Thread(() -> {
for (int j = 0; j < TIMES; j++) {
increment.run();
}
}, "worker-" + i);
}
for (Thread worker : workers) {
worker.start();
}
for (Thread worker : workers) {
worker.join();
}
return reader.getAsInt();
}
public static void main(String[] args) throws Exception {
// 1. 线程的创建与生命周期
Thread worker = new Thread(() -> {
System.out.println(" 子线程执行: " + Thread.currentThread().getName());
}, "worker-1");
System.out.println("start 之前的状态: " + worker.getState());
worker.start();
worker.join();
System.out.println("join 之后的状态: " + worker.getState() + ",还活着吗: " + worker.isAlive());
// 2. 中断:不是强制停止,而是发出协作信号
Thread sleeper = new Thread(() -> {
try {
Thread.sleep(5_000);
System.out.println(" 正常醒来");
} catch (InterruptedException exception) {
System.out.println(" 被打断: " + exception.getClass().getSimpleName()
+ ",中断标记已被清除: " + Thread.currentThread().isInterrupted());
}
}, "sleeper");
sleeper.start();
Thread.sleep(100);
sleeper.interrupt();
sleeper.join();
// 3. 竞态条件:四种计数器的对比
System.out.println("四种计数器各自的目标值: " + EXPECTED);
UnsafeCounter unsafe = new UnsafeCounter();
System.out.println("普通 int : " + countWith(unsafe::increment, unsafe::value) + " ← 丢更新");
VolatileCounter volatileCounter = new VolatileCounter();
System.out.println("volatile int : " + countWith(volatileCounter::increment, volatileCounter::value) + " ← 仍然丢更新");
SyncCounter sync = new SyncCounter();
System.out.println("synchronized : " + countWith(sync::increment, sync::value) + " ← 正确");
AtomicCounter atomic = new AtomicCounter();
System.out.println("AtomicInteger : " + countWith(atomic::increment, atomic::value) + " ← 正确");
// 4. 死锁与 tryLock(完整实现见仓库代码)
// 两个线程以相反顺序获取两把锁,用 tryLock 超时放弃而不是无限等待
// 5. 线程池:线程是昂贵资源,应该复用
try (ExecutorService pool = Executors.newFixedThreadPool(3)) {
Future<Integer> future = pool.submit(() -> 6 * 7);
System.out.println("线程池任务结果: " + future.get());
}
}
}运行命令:
cd labs
./mvnw -pl concurrency compile
java -cp concurrency/target/classes cn.kb.learn.concurrent.ConcurrencyLesson预期输出(前两个计数器的数值每次运行都会不同,这正是竞态的特征):
start 之前的状态: NEW
子线程执行: worker-1
join 之后的状态: TERMINATED,还活着吗: false
被打断: InterruptedException,中断标记已被清除: false
四种计数器各自的目标值: 200000
普通 int : 55584 ← 丢更新
volatile int : 94248 ← 仍然丢更新
synchronized : 200000 ← 正确
AtomicInteger : 200000 ← 正确
线程 A(先 A 后 B): 放弃第二把锁(tryLock 避免了真死锁)
线程 B(先 B 后 A): 放弃第二把锁(tryLock 避免了真死锁)
线程池任务结果: 42这张对比表是本节的核心证据:
| 写法 | 实测结果 | 结论 |
|---|---|---|
普通 int | 55584 / 200000 | 丢掉了 72% 的更新 |
volatile int | 94248 / 200000 | 仍然丢掉 53%——可见性 ≠ 原子性 |
synchronized | 200000 | 正确 |
AtomicInteger | 200000 | 正确 |
请重点看第二行:volatile 确实提升了结果(说明可见性起了作用),但离正确还差很远。"加了 volatile 就安全"是一个代价高昂的误解。
关于 中断标记已被清除: false:sleep 抛 InterruptedException 时会清空中断标记,所以 catch 后要按需重新设置(Thread.currentThread().interrupt()),否则上层代码感知不到中断——第 9 节会讲这个坑。
countWith 的 worker.join() 处断点,查看调试器的线程列表:能看到 worker-0 到 worker-3 四个线程,状态在 RUNNABLE 与 TERMINATED 间变化;SyncCounter.increment() 的 count++ 处断点,线程切换观察:任一时刻只能有一个线程停在这里——这就是互斥;synchronized 去掉后重复第 2 步:会看到多个线程同时停在同一行——这就是竞态之所以发生;sleeper 线程,interrupt() 之前它是 TIMED_WAITING(停在 sleep 上),打断后立刻进入 RUNNABLE;lock() 而不是 tryLock(),这里会永远挂住。run() 而不是 start() Thread thread = new Thread(task);
thread.run(); // 在当前线程执行,新线程根本没启动症状:程序行为完全正确,只是没有并发。压测时才暴露出性能问题。
volatile 能保证原子性 见 7.2 节实测。volatile 的适用场景:一个线程写、其他线程读的状态标志(如 volatile boolean running)。不适用:count++、复合判断、多变量一致性。
synchronized 块里做耗时操作 synchronized (lock) {
Thread.sleep(1000); // 拿着锁睡觉:所有线程陪你等
callRemoteService(); // 网络调用更糟
}修复:锁内只做必要的共享状态操作,把耗时操作移到锁外。
synchronized (new Object()) { // 每次都是新对象 → 每个线程一把自己的锁
count++;
}修复:锁对象必须是共享的同一个实例(常用 private final Object lock = new Object();)。
InterruptedException 后什么都不做 try {
Thread.sleep(1000);
} catch (InterruptedException exception) {
// 空实现:中断信号被吞掉了
}修复:要么重新抛出,要么恢复中断标记:
} catch (InterruptedException exception) {
Thread.currentThread().interrupt(); // 恢复标记,交给上层处理
}单线程能跑完的任务,加了多线程只会更难调试。并发的收益来自"等待时间远大于计算时间",或者"确实有多个核可以并行"。
目标:亲手复现竞态,再亲手修好它。
步骤:
int 与 volatile int 的读数——观察每次都不一样;THREADS 改成 8、TIMES 改成 20_000(总量不变),再跑——观察丢更新的比例变化;transfer(Account from, Account to, long amount):先按参数顺序加锁,用两个线程做 A→B 与 B→A 的循环转账,观察是否卡住(记得用 tryLock,不然程序会挂);验收方式:能解释第 5 步中"为什么加锁顺序能打破死锁"(提示:循环等待条件)。
扩展任务:用 AtomicLong 实现一个"请求计数器",要求:increment() 返回自增后的值、get() 无锁读取,并说明它为什么比 synchronized 更适合"高频只增不减"的场景。
基础题
volatile 能保证 count++ 的线程安全。(对 / 错)NEW 到 RUNNABLE 需要调用 ______;主线程等待子线程结束用 ______。thread.start() 与 thread.run() 的区别是什么?代码阅读题
class Holder {
private int value;
private volatile boolean ready;
void produce() {
value = 42;
ready = true;
}
int consume() {
while (!ready) {
Thread.onSpinWait();
}
return value;
}
}ready 用 volatile 修饰是必须的吗?如果用普通 boolean,可能出现什么现象?value 需要加 volatile 吗?结合"volatile 写之前的操作对读线程可见"说明理由。编程题
int 数组的和: threads 段,每段一个任务;Callable 返回每段的和,主线程汇总;500000500000;综合任务
Account 有 name 与 balance,balance() 用 synchronized 保证读到一致值;transfer(from, to, amount) 要求:余额不足抛异常、不能死锁;提示 1(方向):第 4 题的关键是"可见性"——读线程可能永远读不到写线程的修改。
提示 2(关键点):第 6 题的每段任务只写自己的局部变量,只有主线程读结果,没有共享可变状态就没有竞态。
提示 3(接近答案):第 7 题的加锁顺序按 name 排序即可——所有线程都以"A → B → C"的顺序加锁,就不可能形成环路。
volatile 只保证可见性与有序性,不保证原子性。count++ 是"读、改、写"三步,即使每个线程都读到最新值,仍可能互相覆盖。实测中它丢掉了约一半的更新。start();join()。start() 会启动一个新线程,由新线程执行 run();run() 就是普通方法调用,在调用者所在线程执行。写错不会报错,后果是"程序变回单线程"。第 4 题:ready 必须用 volatile。
若用普通 boolean,consume() 中的 while (!ready) 可能永远读不到写线程的修改——读线程可能一直从自己的缓存/寄存器里读旧值,导致死循环。volatile 保证写操作立刻对其他线程可见。
第 5 题:value 不需要 volatile。
因为 volatile 写的语义包含:写 ready 之前的操作,对读到 ready = true 的线程都可见(这是 JMM 的 happens-before 规则)。既然 value = 42 在 ready = true 之前,那么看到 ready 为真就能看到 value 为 42。
但这是精细的推理,容易写错。 工程上更稳的做法:value 也加 volatile,或用 synchronized、AtomicReference 承载这一组状态——用明显正确的写法,而不是依赖对内存模型的精确记忆。
package cn.kb.learn.concurrent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ParallelSum {
static long sumParallel(int[] numbers, int threads) throws Exception {
int chunkSize = (numbers.length + threads - 1) / threads;
try (ExecutorService pool = Executors.newFixedThreadPool(threads)) {
List<Future<Long>> futures = new ArrayList<>();
for (int start = 0; start < numbers.length; start += chunkSize) {
int from = start;
int to = Math.min(start + chunkSize, numbers.length);
futures.add(pool.submit(() -> {
long sum = 0;
for (int i = from; i < to; i++) {
sum += numbers[i];
}
return sum;
}));
}
long total = 0;
for (Future<Long> future : futures) {
total += future.get();
}
return total;
}
}
public static void main(String[] args) throws Exception {
int size = 1_000_000;
int[] numbers = new int[size];
for (int i = 0; i < size; i++) {
numbers[i] = i + 1;
}
long expected = (long) size * (size + 1) / 2;
System.out.println("预期和: " + expected);
System.out.println("4 线程: " + sumParallel(numbers, 4));
System.out.println("8 线程: " + sumParallel(numbers, 8));
System.out.println("结果一致: " + (sumParallel(numbers, 4) == expected && sumParallel(numbers, 8) == expected));
}
}为什么"汇总"不需要加锁:每个任务只写自己的局部变量 sum,然后把结果通过 Future 交给主线程;主线程是唯一的汇总者。没有共享可变状态,就没有竞态。
这就是"能不共享就不共享"——并发编程最有效的策略不是加锁,而是缩小共享范围。
预期输出:
预期和: 500000500000
4 线程: 500000500000
8 线程: 500000500000
结果一致: truepackage cn.kb.learn.concurrent;
public class ThreadSafeAccount {
static class Account {
private final String name;
private long balance;
Account(String name, long balance) {
this.name = name;
this.balance = balance;
}
synchronized long balance() {
return balance;
}
/**
* 按账户名顺序获取锁:所有调用者以相同顺序加锁,从根本上消除"循环等待"条件。
* 如果按参数顺序加锁,A→B 与 B→A 并发时就会互相等待。
*/
static void transfer(Account from, Account to, long amount) {
Account first = from.name.compareTo(to.name) <= 0 ? from : to;
Account second = first == from ? to : from;
synchronized (first) {
synchronized (second) {
if (from.balance < amount) {
throw new IllegalStateException(from.name + " 余额不足");
}
from.balance -= amount;
to.balance += amount;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Account a = new Account("A", 100_000);
Account b = new Account("B", 100_000);
Account c = new Account("C", 100_000);
long expected = a.balance() + b.balance() + c.balance();
Thread ab = new Thread(() -> {
for (int i = 0; i < 5_000; i++) {
Account.transfer(a, b, 1);
}
}, "A->B");
Thread bc = new Thread(() -> {
for (int i = 0; i < 5_000; i++) {
Account.transfer(b, c, 1);
}
}, "B->C");
Thread ca = new Thread(() -> {
for (int i = 0; i < 5_000; i++) {
Account.transfer(c, a, 1);
}
}, "C->A");
for (Thread worker : new Thread[]{ab, bc, ca}) {
worker.start();
}
for (Thread worker : new Thread[]{ab, bc, ca}) {
worker.join();
}
long actual = a.balance() + b.balance() + c.balance();
System.out.println("A / B / C 余额: " + a.balance() + " / " + b.balance() + " / " + c.balance());
System.out.println("总额守恒: " + actual + "(预期 " + expected + ")→ " + (actual == expected));
}
}不用"按名称排序"时的替代方案:
tryLock 超时放弃:拿不到第二把锁就释放第一把,随机等待后重试(活锁风险低但需重试逻辑);共同点:都在破坏"持有并等待"或"循环等待"这两个条件。
预期输出:
A / B / C 余额: 100000 / 100000 / 100000
总额守恒: 300000(预期 300000)→ truestart() / run() 的区别;volatile 保证什么、不保证什么;synchronized 与 AtomicInteger 修复并发错误;volatile 与 JMM 的权威材料《Java 8 核心特性总览》:把 S1 学过的语言特性放到"版本"这条线上重新梳理一遍。