线程基础
在 Java 中,线程是最小的调度单位。理解线程创建方式、状态流转和内存可见性,是掌握并发编程的第一步。
线程与进程的区别
- 地址空间:进程拥有完整的独立地址空间;同一进程内的线程共享堆与方法区,但拥有独立的程序计数器、虚拟机栈和本地方法栈。
- 调度成本:线程切换仅需切换线程上下文;进程切换还要切换页表、文件描述符等,成本更高。
- 通信方式:线程共享内存通信简单但需同步;进程通信需要 IPC(管道、Socket、共享内存),但隔离更好。
- 崩溃影响:单线程异常可能导致整个进程崩溃;多进程架构能天然隔离故障。
线程生命周期
| 状态 | 说明 | 常见触发 |
|---|---|---|
| NEW | new Thread() 后尚未启动 | 构造阶段 |
| RUNNABLE | 包含就绪与运行,是否真正占用 CPU 由 OS 决定 | start()、阻塞恢复 |
| BLOCKED | 等待进入同步块(synchronized)的监视器锁 | 锁竞争 |
| WAITING | 无限期等待其他线程显式唤醒 | Object.wait()、LockSupport.park() |
| TIMED_WAITING | 有超时时间的等待 | sleep(n)、wait(n)、parkNanos |
| TERMINATED | 执行结束或抛出未捕获异常 | run() 退出 |
✅ 常见面试点:
sleep()不会释放锁,而wait()会释放监视器并进入等待队列。
线程常用关键字与 API
| API | 行为 | 面试/实践要点 |
|---|---|---|
Thread.sleep(ms) | 线程进入 TIMED_WAITING,让出 CPU,不释放锁 | 捕获 InterruptedException,常见问法:是否释放锁(不会) |
Thread.yield() | 提示调度器“我可以让位” | 仅为 hint,不保证立即切换,常用于自旋退避 |
Thread.join() | 等待目标线程结束 | 典型场景:主线程等待子线程汇总结果 |
thread.interrupt() | 设置中断标记,唤醒可中断方法 | 阻塞方法抛 InterruptedException;自行实现循环要检查 Thread.currentThread().isInterrupted() |
wait/notify/notifyAll | 配合 synchronized 使用,线程进入等待队列 | wait() 会释放监视器,必须在循环中判断条件避免虚假唤醒 |
LockSupport.park/unpark | 许可机制,构建同步原语 | unpark 可先于 park 调用;park 需处理中断 |
Thread.onSpinWait() | JDK 9+,提示 CPU 进行自旋优化 | 常用在高性能自旋锁实现中,降低功耗 |
public void interruptDemo() throws InterruptedException {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置标记
}
}
});
worker.start();
TimeUnit.SECONDS.sleep(3);
worker.interrupt();
}
面试提示:
interrupt()不是强制打断,而是“打标记 + 唤醒阻塞”,真正退出还要配合业务逻辑检查。
线程创建方式
// 1. 继承 Thread
class SimpleThread extends Thread {
@Override
public void run() {
System.out.println("do work");
}
}
// 2. 实现 Runnable
Runnable task = () -> System.out.println("runnable");
new Thread(task).start();
// 3. Callable + FutureTask(可返回结果/抛出检查异常)
FutureTask<Integer> futureTask = new FutureTask<>(() -> 42);
new Thread(futureTask).start();
Integer result = futureTask.get();
// 4. 交给线程池(推荐)
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> System.out.println("executor"));
| 方式 | 优点 | 场景 |
|---|---|---|
继承 Thread | 编写简单 | Demo、快速验证 |
Runnable | 可共享同一任务实例,多线程复用 | 推荐基础写法 |
Callable | 支持返回值、受检异常 | 异步任务、批处理 |
| 线程池 | 控制线程数量,复用线程 | 服务端常规方案 |
同步与可见性工具
synchronized:保证原子性与可见性,编译后生成monitorenter/monitorexit指令。适合简短的临界区。ReentrantLock:可重入,可选公平性,支持尝试加锁、条件队列,适用于复杂同步。volatile:仅保证可见性与禁止指令重排,不保证复合操作的原子性,典型场景为配置热更新、状态标记。- 原子类(
AtomicInteger,LongAdder):基于 CAS,通过无锁方式实现高性能计数器或引用更新。 ThreadLocal:为每个线程提供独立变量副本,适合保存用户上下文、格式化器等不可共享对象。
Java 内存模型 (JMM) 与 Happens-Before
Happens-Before 规则确保指令在多线程下的可见性顺序:
- 程序次序:同一线程内,代码按顺序执行。
- 监视器锁:解锁先行于后续线程的加锁。
volatile:写入先行于后续的读取。- 传递性:A Happens-Before B,B Happens-Before C,则 A Happens-Before C。
- 线程启动/终止:
Thread.start()之前的操作对run()可见;run()结束对join()后可见。
牢记:
volatile无法替代synchronized,它只解决可见性和指令重排。
线程安全设计建议
- 最小化共享:优先使用局部变量、不可变对象或线程封闭(ThreadLocal)。
- 削减临界区:缩小锁作用范围,或将大对象拆分为多个锁。
- 组合原子操作:多个相关字段的更新要放入同一锁或使用
StampedLock/ReadWriteLock。 - 避免死锁:统一加锁顺序、使用超时加锁、必要时借助
jstack排查。 - 监控线程状态:通过
jconsole,jmc,arthas thread快速定位阻塞点。
高频面试题
-
start()与run()区别?
start()通知 JVM 创建新线程并异步执行run();直接调用run()只是普通方法调用,仍在当前线程执行。 -
如何安全停止线程?
使用两个步骤:设置停止标志(如volatile boolean running = false),并确保线程在循环中定期检查该标志;阻塞方法需配合interrupt()。 -
Thread.sleep()会释放锁吗?
不会。sleep()只让出 CPU 时间片但不释放监视器;wait()、park()才会释放对应的锁或许可证。 -
什么是自旋锁,适合什么场景?
自旋锁不断循环尝试获取锁,避免线程挂起/唤醒开销,适合临界区短且竞争不激烈的场景。Java 中Atomic类的 CAS 就是一种自旋。 -
线程上下文切换的成本是什么?
保存与恢复寄存器、程序计数器、线程栈指针等状态,频繁切换会导致缓存失效、降低吞吐,因此要控制线程数量。
延伸阅读
- 🔒 锁机制详解:thread/locks,深入
synchronized、ReentrantLock、读写锁与StampedLock的适用场景。 - ⚙️ 原子类与 CAS:thread/atomic,掌握
AtomicInteger、LongAdder及 ABA 处理。 - 📚 并发集合全家桶:collection/concurrent-overview,了解各种线程安全集合的选型。
小结
- 线程是并发的基础,先掌握生命周期与内存模型,再考虑高级工具。
- 创建线程推荐交由线程池管理,避免无限制
new Thread()。 - 同步手段各有侧重:
synchronized简单、Lock灵活、volatile用于状态标记。 - 面试常从“概念 + 实战 + 排查”三个角度考察,准备时多结合具体案例。