跳到主要内容

线程基础

在 Java 中,线程是最小的调度单位。理解线程创建方式、状态流转和内存可见性,是掌握并发编程的第一步。


线程与进程的区别

  • 地址空间:进程拥有完整的独立地址空间;同一进程内的线程共享堆与方法区,但拥有独立的程序计数器、虚拟机栈和本地方法栈。
  • 调度成本:线程切换仅需切换线程上下文;进程切换还要切换页表、文件描述符等,成本更高。
  • 通信方式:线程共享内存通信简单但需同步;进程通信需要 IPC(管道、Socket、共享内存),但隔离更好。
  • 崩溃影响:单线程异常可能导致整个进程崩溃;多进程架构能天然隔离故障。

线程生命周期

状态说明常见触发
NEWnew 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 规则确保指令在多线程下的可见性顺序:

  1. 程序次序:同一线程内,代码按顺序执行。
  2. 监视器锁:解锁先行于后续线程的加锁。
  3. volatile:写入先行于后续的读取。
  4. 传递性:A Happens-Before B,B Happens-Before C,则 A Happens-Before C。
  5. 线程启动/终止Thread.start() 之前的操作对 run() 可见;run() 结束对 join() 后可见。

牢记:volatile 无法替代 synchronized,它只解决可见性和指令重排。


线程安全设计建议

  • 最小化共享:优先使用局部变量、不可变对象或线程封闭(ThreadLocal)。
  • 削减临界区:缩小锁作用范围,或将大对象拆分为多个锁。
  • 组合原子操作:多个相关字段的更新要放入同一锁或使用 StampedLock/ReadWriteLock
  • 避免死锁:统一加锁顺序、使用超时加锁、必要时借助 jstack 排查。
  • 监控线程状态:通过 jconsole, jmc, arthas thread 快速定位阻塞点。

高频面试题

  1. start()run() 区别?
    start() 通知 JVM 创建新线程并异步执行 run();直接调用 run() 只是普通方法调用,仍在当前线程执行。

  2. 如何安全停止线程?
    使用两个步骤:设置停止标志(如 volatile boolean running = false),并确保线程在循环中定期检查该标志;阻塞方法需配合 interrupt()

  3. Thread.sleep() 会释放锁吗?
    不会。sleep() 只让出 CPU 时间片但不释放监视器;wait()park() 才会释放对应的锁或许可证。

  4. 什么是自旋锁,适合什么场景?
    自旋锁不断循环尝试获取锁,避免线程挂起/唤醒开销,适合临界区短且竞争不激烈的场景。Java 中 Atomic 类的 CAS 就是一种自旋。

  5. 线程上下文切换的成本是什么?
    保存与恢复寄存器、程序计数器、线程栈指针等状态,频繁切换会导致缓存失效、降低吞吐,因此要控制线程数量。


延伸阅读

  • 🔒 锁机制详解:thread/locks,深入 synchronizedReentrantLock、读写锁与 StampedLock 的适用场景。
  • ⚙️ 原子类与 CAS:thread/atomic,掌握 AtomicIntegerLongAdder 及 ABA 处理。
  • 📚 并发集合全家桶:collection/concurrent-overview,了解各种线程安全集合的选型。

小结

  • 线程是并发的基础,先掌握生命周期与内存模型,再考虑高级工具。
  • 创建线程推荐交由线程池管理,避免无限制 new Thread()
  • 同步手段各有侧重:synchronized 简单、Lock 灵活、volatile 用于状态标记。
  • 面试常从“概念 + 实战 + 排查”三个角度考察,准备时多结合具体案例。