1. 线程的状态有哪些?如何转换?

Java 线程六种状态(Thread.State 枚举,注意与 OS 状态的区别):

  1. NEW:new Thread() 后尚未 start()
  2. RUNNABLE:可运行状态。包括"正在运行"和"等待 CPU 时间片"(OS 层的 Running + Ready 合二为一)
  3. BLOCKED:等待监视器锁(synchronized 没抢到锁)
  4. WAITING:无限期等待 —— Object.wait()(无超时)、join()、LockSupport.park()
  5. TIMED_WAITING:限期等待 —— sleep(ms)、wait(timeout)、join(timeout)、parkNanos
  6. 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. 创建线程的几种方式?推荐哪种?

  1. 继承 Thread:重写 run()。缺点:Java 单继承,占用继承位;任务与线程耦合
  2. 实现 Runnable:任务与线程分离,可复用同一任务多线程执行。没有返回值
  3. 实现 Callable<V>:有返回值(call()),可抛异常;配 FutureTask 获取结果
  4. 线程池 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/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 的三个根源):

  1. 原子性:操作不可分割。i++ 是"读-改-写"三步,可能被打断。解决:synchronized / Lock / CAS / AtomicInteger
  2. 可见性:线程 A 改了值,线程 B 不一定立刻看到(CPU 缓存/寄存器/指令重排)。解决:volatile / synchronized / final
  3. 有序性:指令重排(编译器/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 是"以空间换安全":每个线程一份副本,无共享

🎤 常见面试追问

  1. sleep 和 wait 的区别?——sleep 不释放锁、必须指定时间、Thread 静态方法;wait 释放锁、可被 notify 唤醒、Object 方法且必须在 synchronized 块里。
  2. 为什么 wait/notify 必须配合 synchronized?——wait 的语义是"释放锁并等待",前提是持有锁;notify 唤醒"在某个监视器上等待的线程"也要持有同一把锁——防止丢失唤醒。
  3. 线程安全怎么保证?——三选一:① 无共享(局部变量/ThreadLocal);② 不可变(final/String);③ 同步(synchronized/Lock/原子类)。能无共享就别加锁。
  4. 为什么判断条件用 while 不用 if?——防止"虚假唤醒":wait 可能被莫名唤醒(平台原因),醒来要重新检查条件,用 while 循环保证条件不满足继续等。
  5. 创建线程为什么推荐线程池?——线程创建销毁开销大(内核资源 + 上下文切换),线程池复用线程、控制数量、统一管理,避免线程爆炸。

📖 名词解释(本页术语)

术语 大白话解释
线程程序里并发执行的任务线。Java 里 new Thread().start() 开一条,线程池统一管理。
线程安全多线程同时操作共享数据,结果与单线程串行执行一致。需要同时满足原子性、可见性、有序性三个条件;任一条被破坏就会出现并发 bug。
原子性操作不可分割:i++ 是"读-改-写"三步,可能被打断。解决:synchronized/原子类(CAS)。
可见性A 线程改了值 B 线程看不到(CPU 缓存)。解决:volatile/synchronized。
Runnable / Callable两种任务接口:Runnable 无返回值(void run);Callable 有返回值可抛异常(V call)。
阻塞 / 非阻塞阻塞 = 等不到就干等(线程挂起);非阻塞 = 立即返回结果/失败。阻塞要唤醒开销,非阻塞要轮询/回调。
ThreadLocal每个线程一份独立副本的容器(线程私有数据)。注意用完 remove(),否则线程池复用会串数据。
上下文切换CPU 从一个线程切到另一个:保存/恢复寄存器 + 缓存失效。切换有成本,线程不是越多越好。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。