Appearance
2026-08-29 · 线程怎么创建、怎么保护共享数据——Spring 并发编程的地基。
Java 多线程
1. 进程与线程
进程:正在运行的程序,有自己独立的内存空间(打开一个记事本就是开一个进程)。
线程:进程内部的执行单元,一个进程至少有一个线程。
进程(程序 + 独立内存)
├── 线程1(main 主线程)
├── 线程2
└── 线程3线程之间共享进程的内存,所以能一起干活、传数据;但也正因为共享,才会产生后面的线程安全问题。
为什么要用多线程? 让程序同时干多件事:
- 服务器同时处理多个用户请求(每个请求一个线程)
- 读写文件、发邮件这类耗时操作不卡住主流程
所有 Java 程序默认都有一个
main主线程。main里写的代码是单线程跑,遇到耗时操作就会卡住——这就是多线程要解决的。
2. 创建线程(日常两种方式)
方式一:实现 Runnable 接口(最常见)
java
public class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务执行中:" + Thread.currentThread().getName());
}
}
// 使用:任务和线程分开
Runnable task = new MyTask();
Thread t = new Thread(task); // 任务交给线程
t.start();Lambda 简写(Runnable 是函数式接口):
java
Thread t = new Thread(() -> System.out.println("Lambda 线程"));
t.start();方式二:实现 Callable 接口(要返回值时)
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// Callable 和 Runnable 的区别:能返回结果、能抛异常
Callable<Integer> task = () -> 1 + 2;
// 线程不直接接收 Callable,要用 FutureTask 包一层
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
// 获取结果(会阻塞等待,直到子线程算完)
Integer result = future.get();
System.out.println(result); // 3⚠️ 启动线程必须用
start(),不是run()——start()才是开新线程,直接调run()只是普通方法调用,还在主线程里跑。
⚠️
future.get()会阻塞当前线程直到子线程算完。需要返回值才用 Callable,不需要就用 Runnable。
核心思想:Runnable/Callable 定义任务,Thread 负责执行,两者分离。实际项目中很少手动 new Thread,都用线程池(见《Java 线程池》)。
3. 线程生命周期(6 种状态)
| 状态 | 说明 |
|---|---|
| NEW | 刚 new 出来,还没 start() |
| RUNNABLE | 已启动,可运行/正在运行 |
| BLOCKED | 阻塞:等待锁 |
| WAITING | 无限期等待 |
| TIMED_WAITING | 限期等待:sleep() 睡固定时间 |
| TERMINATED | 结束:run() 跑完了 |
NEW → RUNNABLE → TERMINATED
↑ ↓
BLOCKED / WAITING / TIMED_WAITING这 6 个状态工作中不用手写判断,但看懂日志里的线程状态(
jstack输出)能帮排查问题。
4. 常用方法速查
| 方法 | 作用 |
|---|---|
start() | 启动线程 |
sleep(毫秒) | 当前线程睡一会 |
join() | 等某个线程结束 |
setName() / getName() | 线程名字(排错有用,日志好认) |
currentThread() | 拿到当前正在跑的线程(静态方法,最常用) |
5. 线程安全:多个线程抢同一份数据
看这段经典代码——两个线程同时给同一个数 +1,各加 1 万次,结果应该是 2 万:
java
public class Counter {
private int count = 0;
public void increment() {
count++; // 这行不是一步,是三步:读值 → 加1 → 写回
}
public static void main(String[] args) throws Exception {
Counter c = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) c.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) c.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(c.count); // 经常不到 20000,比如 17892
}
}为什么小于 2 万? 因为 count++ 实际是三步:
线程1:读 count(=99) → 加1(=100) → 写回
线程2:读 count(=99,线程1还没写回) → 加1(=100) → 写回 ← 一次更新丢了两个线程同时做了"读",都各自 +1 写回,其中一次 +1 被覆盖丢掉了。这叫竞态条件(Race Condition)。
⚠️ 不是每次运行都出错,是偶尔出错、值随机。这种"时好时坏"的 bug 最难排查。
解决办法的核心思想:给共享数据加锁,同一时刻只允许一个线程碰它。
6. synchronized:最基础的锁
一个线程进入代码块时自动加锁,其他线程必须等它出来才能进:
java
// 同步方法(等价写法)
public synchronized void increment() {
count++;
}
// 同步代码块(锁住指定对象,更灵活)
public void increment() {
synchronized (this) { // this = 锁当前对象
count++;
}
}锁的是谁?
- 同步方法/
synchronized(this):锁当前对象(new 出来的那个实例) - 静态同步方法/
synchronized(类.class):锁整个类(所有实例共享)
用 synchronized 修好上面的计数器:
java
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
// 跑完一定是 20000 ✓
}使用时注意锁住的对象要一致——两个线程锁的不是同一个对象,等于没锁。静态方法锁类、实例方法锁对象,混用会失效。
7. Lock:更灵活的锁(了解即可)
synchronized 简单但锁死了就只能等。Lock 接口(常用实现 ReentrantLock)提供超时等待、公平排队等能力:
java
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 必须释放!用 finally 保证
}
}
}| 对比 | synchronized | ReentrantLock |
|---|---|---|
| 写法 | 简单,自动释放 | 要手动 lock() / unlock()(finally 里释放) |
| 超时等待 | 不支持 | tryLock(3, TimeUnit.SECONDS) 等 3 秒拿不到就放弃 |
| 性能 | 现代 JVM 已优化,差别不大 | 差别不大 |
首选 synchronized(简单安全)。手动
unlock()忘了写就出问题,只有确实需要"等超时""公平排队"时才换 Lock。
8. 原子类:不用锁的数字操作
对"数字加减"这种场景,JDK 提供了现成的原子类,内部用 CAS 实现,无锁、性能更高:
java
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子 +1,线程安全
}
}| 类 | 对应 | 常用方法 |
|---|---|---|
AtomicInteger | int | incrementAndGet() / getAndIncrement() / get() |
AtomicLong | long | 同上 |
AtomicBoolean | boolean | get() / set() |
只改数字时优先用原子类(比锁快);逻辑复杂(多步操作)才用 synchronized。
9. 小结:工作中怎么用
- 不要手动
new Thread——用线程池(Spring 的@Async、ThreadPoolTaskExecutor) - 共享数据要加锁——首选
synchronized;计数器类场景用原子类 - 并发环境用并发集合——
ConcurrentHashMap(不要用普通 HashMap) - volatile、wait/notify、死锁分析等——属于底层原理,日常开发基本不写,遇到时再查
到这里,"线程怎么创建、怎么共存、怎么不出错"讲完了。线程池(不手动 new 线程,交给池子管理)是实际项目里每天都要用的东西。