🧵 线程基础
线程生命周期 · 创建方式 · wait/notify/join/sleep · 线程安全三要素
1. 线程的状态有哪些?如何转换?
Java 线程六种状态(Thread.State 枚举,注意与 OS 状态的区别):
- NEW:new Thread() 后尚未 start()
- RUNNABLE:可运行状态。包括"正在运行"和"等待 CPU 时间片"(OS 层的 Running + Ready 合二为一)
- BLOCKED:等待监视器锁(synchronized 没抢到锁)
- WAITING:无限期等待 —— Object.wait()(无超时)、join()、LockSupport.park()
- TIMED_WAITING:限期等待 —— sleep(ms)、wait(timeout)、join(timeout)、parkNanos
- TERMINATED:run() 执行完或异常退出
关键转换:BLOCKED → 拿到锁进 RUNNABLE;WAITING → 被 notify/notifyAll 唤醒回 RUNNABLE;sleep 结束回 RUNNABLE。sleep 不释放锁,wait 释放锁——最常考的区别。
🎯 面试要点
- jstack 里看到的对应:synchronized 竞争 → "waiting for monitor entry";wait() → "in Object.wait()"
- stop()/suspend()/resume() 已废弃(stop 直接杀线程不释放资源,会破坏数据一致性)
- 线程终止只能靠:run 结束 / 中断标志协作(interrupt)
2. 创建线程的几种方式?推荐哪种?
- 继承 Thread:重写 run()。缺点:Java 单继承,占用继承位;任务与线程耦合
- 实现 Runnable:任务与线程分离,可复用同一任务多线程执行。没有返回值
- 实现 Callable<V>:有返回值(call()),可抛异常;配 FutureTask 获取结果
- 线程池 ExecutorService:生产首选——统一管理生命周期,避免频繁创建销毁线程的开销
Runnable / Callable / FutureTask
// Runnable:无返回值
Runnable r = () -> System.out.println("run");
new Thread(r).start();
// Callable:有返回值 + 异常
Callable<Integer> task = () -> { return 42; };
FutureTask<Integer> ft = new FutureTask<>(task);
new Thread(ft).start();
Integer result = ft.get(); // 阻塞等待结果(可设超时)
// 线程池(生产推荐)
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(task);
🎯 面试要点
- Runnable 是函数式接口(void run),Callable 是(V call throws Exception)
- run() 和 start() 的区别:start 创建新线程并执行 run;直接调 run 只是当前线程同步执行
- Future.get() 会阻塞;get(timeout) 超时抛 TimeoutException
3. wait / notify / join / sleep / yield 的区别?
- wait():Object 方法,释放锁并进入 WAITING,需在 synchronized 块内调用;notify 唤醒一个(不保证哪个),notifyAll 唤醒全部(推荐,避免信号丢失)
- sleep():Thread 静态方法,不释放锁,到时间自动醒
- join():当前线程等目标线程执行完(本质是 wait 直到目标线程结束)
- yield():让出 CPU 时间片,但仍在 RUNNABLE,可能立刻又被调度
为什么 wait/notify 必须配合 synchronized:wait 的语义是"释放锁并等待",前提是你持有锁;notify 要"唤醒正在该锁上等待的线程",也要持有同一把锁。这是 Java 内置监视器(Monitor)协议,防止 lost wakeup(丢失唤醒)。
经典生产者-消费者(wait/notify)
class Queue {
private final LinkedList<Integer> list = new LinkedList<>();
private final int cap;
public synchronized void put(int v) throws InterruptedException {
while (list.size() >= cap) wait(); // 满则等(用 while 防虚假唤醒)
list.addLast(v);
notifyAll(); // 唤醒消费者
}
public synchronized int take() throws InterruptedException {
while (list.isEmpty()) wait(); // 空则等
int v = list.removeFirst();
notifyAll();
return v;
}
}
// 生产实践:直接用 BlockingQueue(ArrayBlockingQueue 等)更简单
🎯 面试要点
- 判断条件用 while 不用 if:防止"虚假唤醒"(wait 可能被莫名唤醒)
- 手写生产者消费者 → 优先答 BlockingQueue,再补充 wait/notify 版细节
- wait 会释放锁,sleep 不会;join 本质是 wait(0)
4. 什么是线程安全?出现并发问题的三个条件?
定义:多个线程同时操作共享数据时,结果与单线程串行执行一致,且无需额外同步。要做到这一点,必须同时满足三个必要条件(也是并发 bug 的三个根源):
- 原子性:操作不可分割。i++ 是"读-改-写"三步,可能被打断。解决:synchronized / Lock / CAS / AtomicInteger
- 可见性:线程 A 改了值,线程 B 不一定立刻看到(CPU 缓存/寄存器/指令重排)。解决:volatile / synchronized / final
- 有序性:指令重排(编译器/CPU)可能改变执行顺序。解决:volatile(禁重排)/ happens-before
i++ 为什么不是原子操作
static int count = 0;
// 两个线程各执行 10000 次 count++,结果往往 < 20000
// 原因:count++ 分三步
1. temp = count; // 读
2. temp = temp + 1; // 改
3. count = temp; // 写 —— 线程可能在这期间被切走
// 解决:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 保证原子
🎯 面试要点
- 经典组合:volatile 保证可见性+有序性,不保证原子性;synchronized 三者都保证
- long/double 在 32 位 JVM 上读写可能不是原子的(JLS 允许非 volatile 分两次写)
- ThreadLocal 是"以空间换安全":每个线程一份副本,无共享