Java并发(一):线程基础与线程间通信
Java并发(一):线程基础与线程间通信
导语:并发是 Java 面试的绝对核心。本篇从最基础处切入——进程与线程的本质差异、六种线程状态与转换、上下文切换的开销,并补齐了原文缺失但高频的「中断机制」与「如何正确停止线程」,共 11 题。
一、线程与并发基础
1. 进程与线程的区别是什么?
答:
- 进程是操作系统进行资源分配和调度的基本单位,拥有独立的地址空间、文件句柄等系统资源;进程间相互隔离,通信需借助 IPC(管道、共享内存、Socket 等)。
- 线程是 CPU 调度的最小单位,是进程中的一个执行流;同一进程内的线程共享堆、方法区等内存区域,仅各自拥有独立的程序计数器、虚拟机栈和本地方法栈。
- 进程切换开销大(涉及地址空间切换),线程切换开销小;但多线程共享内存也带来了线程安全问题。
2. 并发(Concurrency)与并行(Parallelism)的区别?
答:
- 并发:多个任务在同一时间段内交替执行,宏观上"同时",微观上可能串行,强调对 CPU 时间片的切分,主要解决"有任务在等着做"的问题(常见于单核)。
- 并行:多个任务在同一时刻真正同时执行,依赖多核/多处理器,强调同时计算。
- 一句话:并发是处理多个任务的能力,并行是同时执行多个任务的能力。
3. 创建线程有哪几种方式?
答:
- 继承
Thread类,重写run()(受单继承局限,较少用)。 - 实现
Runnable接口,作为Thread构造参数(更灵活,推荐)。 - 实现
Callable接口 +FutureTask,可返回结果、抛受检异常(JDK 5+)。 - 线程池
ExecutorService(工程上最推荐,统一管控线程资源)。
本质上 Java 创建线程只有一种方式:构造
Thread对象并调用start()。Runnable/Callable只是任务载体,最终仍由Thread执行。
4. 线程的生命周期/状态有哪些?状态如何转换?
答: 依据 Thread.State 枚举,Java 线程有 6 种状态:
| 状态 | 含义 |
|---|---|
| NEW | 创建后尚未调用 start() |
| RUNNABLE | 包含操作系统层面的"就绪"与"运行中"两种,JVM 统一为 RUNNABLE |
| BLOCKED | 等待进入 synchronized 同步块/方法(锁被其他线程占用) |
| WAITING | 无限期等待,如 Object.wait()、Thread.join()、LockSupport.park() |
| TIMED_WAITING | 带超时的等待,如 Thread.sleep()、wait(timeout)、join(timeout)、parkNanos() |
| TERMINATED | run() 执行完毕或异常退出 |
关键转换:
start()使 NEW → RUNNABLE;- 竞争
synchronized失败进入 BLOCKED; - 调用
wait()/join()/park()进入 WAITING 或 TIMED_WAITING; - 被
notify()/notifyAll()/中断/超时唤醒后回到 RUNNABLE; run()结束进入 TERMINATED。
易错点:
sleep()进入的是 TIMED_WAITING 而不是 BLOCKED;BLOCKED 特指"等锁",不要把两者混为一谈。另外 Java 没有单独的"就绪态",这是与操作系统线程状态模型的重要区别。
5. start() 和 run() 的区别?
答: 调用 run() 只是在当前线程中做一次普通方法调用,不会启动新线程;调用 start() 才会向 JVM 申请创建新线程,由新线程去执行 run()。
一个 Thread 对象只能 start() 一次,重复调用会抛 IllegalThreadStateException;但 run() 可以被多次调用(就变成了普通方法)。
6. 什么是线程上下文切换?如何减少?
答: 上下文切换指 CPU 从一个线程切换到另一个线程时,需要保存/恢复线程的私有状态(程序计数器、栈、寄存器等),开销不低(微秒级,还要污染 CPU 缓存)。
减少手段:
- 无锁化编程(CAS、
ThreadLocal),减少线程阻塞唤醒次数; - 减少
synchronized竞争、缩小锁粒度,避免频繁进入阻塞态; - 使用线程池复用线程,避免频繁创建销毁;
- 合理设置线程数(经验值:CPU 核数附近),避免过多线程互相抢占;
- 用
CAS自旋代替轻量级同步(适合锁持有极短的场景)。
7. 什么是守护线程(Daemon Thread)?
答: 守护线程是为其他线程提供后台支撑服务的线程(如 GC 线程)。当 JVM 中所有非守护线程结束时,JVM 会退出,不论守护线程是否还在运行。
- 通过
thread.setDaemon(true)设置,必须在start()之前,否则抛IllegalThreadStateException; - 守护线程中的
finally块不保证执行完(JVM 退出时会被直接终止),因此不要在其中做资源释放等关键操作。
8. 如何优雅地停止一个线程?为什么不推荐 stop()?
答: 核心原则:线程的停止必须由线程自己决定,外部只能"请求"而不能"强制"。
为什么不推荐 stop():
Thread.stop()会立即终止线程并释放它持有的所有锁,导致被这些锁保护的数据可能停留在写了一半的不一致状态(例如转账只扣了款没入账);- 它还会立即抛出
ThreadDeath错误,可能被意外捕获而破坏逻辑; - 因此
stop()、suspend()、resume()自 JDK 1.2 起就被标记为废弃(@Deprecated),在较新的 JDK 中调用stop()会直接抛UnsupportedOperationException。
推荐做法——用「标志位 + 中断」协作式停止:
public class Worker implements Runnable {
private volatile boolean running = true; // 1. volatile 保证可见性
public void shutdown() { running = false; }
@Override
public void run() {
while (running && !Thread.currentThread().isInterrupted()) {
try {
// 阻塞方法会响应中断并抛出 InterruptedException
work();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 2. 恢复中断标志
break; // 3. 退出循环
}
}
// 4. 收尾清理
}
}两个关键细节:
- 必须用
volatile,否则工作线程可能永远读不到标志位的修改(可见性问题); - 在
catch (InterruptedException)中要调用Thread.currentThread().interrupt()恢复中断标志,因为抛出该异常时 JVM 会清除中断状态,否则上层调用者将感知不到中断信号。
9. 什么是线程中断机制?interrupt()、isInterrupted()、interrupted() 有什么区别?
答: 中断(Interrupt)是 Java 提供的线程间协作机制,不是强制停止。它的本质是给目标线程打一个"中断标志位",至于是否响应、如何响应,完全由目标线程自己决定。
| 方法 | 所属 | 作用 | 是否清除标志位 |
|---|---|---|---|
interrupt() | 实例方法 | 设置目标线程的中断标志 | 否 |
isInterrupted() | 实例方法 | 只查询中断标志 | 否 |
interrupted() | 静态方法 | 查询当前线程中断标志 | 是(查询后清除) |
中断的两种表现:
- 线程处于阻塞状态(
sleep、wait、join、LockSupport.park):会立即抛出InterruptedException并清除中断标志,线程从阻塞中返回; - 线程处于运行状态:仅设置标志位,线程继续运行,需要自己在循环中检查
isInterrupted()决定是否退出。
高频追问:「为什么
catch到InterruptedException后还要再调一次interrupt()?」因为异常抛出时中断标志已被清除,如果不恢复,中断信号就"丢失"了,调用栈上层的代码将无法感知到曾经发生中断,可能导致外层无法正确终止。这被称为「中断状态的传递」。
二、线程间通信
10. sleep() 与 wait() 有什么区别?
答:
| 维度 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread 的静态方法 | Object 的实例方法 |
| 是否释放锁 | 不释放(抱着锁睡觉) | 释放锁,进入监视器的等待集 |
| 使用位置 | 任意位置 | 必须在 synchronized 内,否则抛 IllegalMonitorStateException |
| 唤醒方式 | 到时自动唤醒 | 需其他线程 notify()/notifyAll(),或超时 |
| 异常 | 抛 InterruptedException | 抛 InterruptedException |
| 线程状态 | TIMED_WAITING | WAITING / TIMED_WAITING |
最关键的区别是是否释放锁:
sleep()期间其他线程拿不到被它持有的锁;wait()会主动让出锁,这正是它能用于线程间协作的前提。
11. wait()/notify()/notifyAll() 如何使用?为什么必须在 synchronized 内调用?
答:
wait()让当前线程释放锁并等待;notify()随机唤醒一个等待该锁的线程;notifyAll()唤醒全部等待线程(它们需重新抢锁才能继续)。
为什么必须在 synchronized 内调用,原因有三:
- 语义要求:这三个方法操作的是对象的监视器(Monitor),必须先持有该监视器才能操作其等待集,否则抛
IllegalMonitorStateException; - 防止丢失唤醒(lost wake-up):若不加锁,"判断条件"与"进入等待"之间存在时序窗口——通知方可能在等待方真正
wait()之前就发出了notify(),导致等待方永远等不到通知; - 保证条件判断与等待的原子性:把"检查条件 + 等待"作为一个临界区,避免条件的竞态变化。
final Object lock = new Object();
// 等待方
synchronized (lock) {
while (!condition) lock.wait(); // 必须用 while,不用 if
// do work
}
// 通知方
synchronized (lock) {
condition = true;
lock.notifyAll();
}为什么用
while而不是if:需要抵御两种"假醒"——① 虚假唤醒(spurious wakeup,操作系统允许无理由唤醒);② 多线程竞争下,被唤醒时条件可能已被别的线程再次改变。所以醒来后必须重新检查条件,不满足则继续等待。
