Java 并发面试题(附答案)

更新于 2025-12-29

baeldung 2025-01-09

1. 引言

Java 中的并发是技术面试中最复杂、最深入的话题之一。本文整理了一些你在面试中可能会遇到的并发相关问题及其解答。


Q1. 进程(Process)和线程(Thread)有什么区别?

进程和线程都是并发的基本单元,但它们有根本性的区别:进程之间不共享内存,而线程则共享同一进程内的内存空间

从操作系统角度看,进程是一个独立运行的软件程序,它拥有自己独立的虚拟内存空间。现代多任务操作系统必须将各个进程在内存中隔离,以防止某个进程崩溃时破坏其他进程的内存数据。

因此,进程通常是相互隔离的,它们通过操作系统提供的进程间通信(IPC)机制进行协作。

相反,线程是应用程序的一部分,同一个应用中的多个线程共享相同的内存空间。这种共享机制可以显著减少开销,并允许线程之间快速交换数据和协作。


Q2. 如何创建并运行一个线程实例?

创建线程有两种方式:

第一种:将 Runnable 实例传给 Thread 构造函数,然后调用 start() 方法。由于 Runnable 是一个函数式接口,可以用 Lambda 表达式:

Thread thread1 = new Thread(() ->
  System.out.println("Hello World from Runnable!"));
thread1.start();

第二种:继承 Thread 类,重写其 run() 方法,再调用 start()

Thread thread2 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello World from subclass!");
    }
};
thread2.start();

Q3. 线程有哪些状态?状态之间如何转换?

可以通过 Thread.getState() 方法查看线程当前状态。线程的状态由 Thread.State 枚举定义,包括:

  • NEW:新建的线程对象,尚未调用 start()
  • RUNNABLE:线程正在运行或等待 CPU 时间片(可运行状态)。调用 start() 后进入此状态。
  • BLOCKED:线程试图进入一个被其他线程持有的 synchronized 块,因此被阻塞。
  • WAITING:线程无限期等待另一个线程执行特定操作,例如调用了 Object.wait()Thread.join()
  • TIMED_WAITING:与 WAITING 类似,但带有超时时间,例如调用了 Thread.sleep(long)Object.wait(long) 等。
  • TERMINATED:线程执行完 run() 方法后终止。

Q4. RunnableCallable 接口有什么区别?如何使用?

  • Runnable

    • 只有一个 run() 方法。
    • 不能返回结果,也不能抛出受检异常(checked exception)。
    • 通常用于不需要返回值的任务。
  • Callable

    • 有一个 call() 方法。
    • 可以返回结果,也可以抛出异常。
    • 通常配合 ExecutorService 使用,通过返回的 Future 对象获取异步计算结果。

Q5. 什么是守护线程(Daemon Thread)?有哪些使用场景?如何创建?

守护线程是一种不会阻止 JVM 退出的线程。当所有非守护线程结束时,JVM 会直接终止所有守护线程并退出。

使用场景:常用于后台支持性任务,如垃圾回收、日志写入等。

创建方式:在线程启动前调用 setDaemon(true)

Thread daemon = new Thread(() -> 
  System.out.println("Hello from daemon!"));
daemon.setDaemon(true);
daemon.start();

⚠️ 注意:如果主线程很快结束,守护线程可能来不及执行,甚至无法打印输出。不应在守护线程中执行 I/O 操作,因为 JVM 可能不会让它执行 finally 块来释放资源。


Q6. 什么是线程的中断标志(Interrupt Flag)?如何设置和检查?它与 InterruptedException 有何关系?

中断标志是线程内部的一个布尔标记,表示该线程已被请求中断。

  • 调用 thread.interrupt() 可设置该标志。
  • 如果线程正处于 sleep()wait()join() 等会抛出 InterruptedException 的方法中,会立即抛出异常,并清除中断标志。
  • 如果线程不在上述方法中,则不会自动响应中断,需主动检查中断状态:
    • Thread.interrupted():静态方法,会清除中断标志。
    • thread.isInterrupted():实例方法,不会清除中断标志。

Q7. ExecutorExecutorService 是什么?它们有何区别?

  • Executor:非常简单的接口,只有一个 execute(Runnable command) 方法,用于执行任务。
  • ExecutorService:继承自 Executor,增加了管理生命周期的方法(如 shutdown())、提交带返回值任务的方法(如 submit()),以及处理 Future 的能力。

通常,业务代码应依赖 Executor 接口,而需要高级控制时才使用 ExecutorService


Q8. 标准库中有哪些 ExecutorService 的实现?

标准库提供了三种主要实现:

  1. ThreadPoolExecutor:使用线程池执行任务。任务完成后线程归还池中;若池满,新任务需排队。
  2. ScheduledThreadPoolExecutor:支持延迟执行或周期性执行任务(如 scheduleAtFixedRate)。
  3. ForkJoinPool:专为递归算法设计,采用“工作窃取”(work-stealing)算法,高效利用线程资源。

Q9. 什么是 Java 内存模型(JMM)?它的目的是什么?基本思想是什么?

Java 内存模型(JMM)是 Java 语言规范第 17.4 节定义的内容,用于规定多线程环境下如何访问共享内存,以及一个线程对内存的修改何时对其他线程可见。

背景:编译器、JIT、CPU 都可能对内存读写进行重排序或优化(只要单线程行为不变),但在多线程下会导致不可预测的结果。此外,现代 CPU 有多级缓存,不同核心看到的内存状态可能不一致。

JMM 的核心目标:提供跨平台的、可预测的并发语义,确保“一次编写,到处运行”。

关键概念

  • 动作(Actions):线程间可观察的操作,如读/写变量、加锁等。
  • 同步动作(Synchronization Actions):如读写 volatile 变量、加锁/解锁。
  • 程序顺序(Program Order, PO):单线程内动作的顺序。
  • 同步顺序(Synchronization Order, SO):所有同步动作的全局顺序。
  • synchronizes-with(SW):如解锁 → 同一 monitor 的加锁。
  • happens-before(HB):PO + SW 的传递闭包。若 A happens-before B,则 A 的结果对 B 可见。
  • happens-before 一致性:每个读操作都能看到 HB 顺序中最近的写,或通过数据竞争看到其他写。

正确同步的程序在 JMM 下表现为顺序一致性(Sequential Consistency),便于推理。


Q10. 什么是 volatile 字段?JMM 对它有哪些保证?

volatile 字段具有特殊语义:

  1. 可见性保证:对 volatile 变量的读写是同步动作,所有线程看到的操作顺序一致。
  2. 禁止重排序:编译器和处理器不会对 volatile 读写进行重排序。
  3. 原子性:对 longdouble(64 位)的读写在非 volatile 情况下可能分两步执行,但 volatile 保证其原子性。

✅ 建议:若字段被多个线程访问,且至少有一个线程写入,应考虑使用 volatile


Q11. 以下哪些操作是原子的?

  • 写入非 volatileint(32 位写入原子)
  • 写入 volatileint
  • 写入非 volatilelong(在 32 位系统上可能分两次)
  • 写入 volatilelong(JMM 保证 64 位原子)
  • volatile long 执行 ++++ 包含读-改-写三步,非原子)

✅ 解决方案:使用 AtomicLongAtomicInteger 等原子类。


Q12. JMM 对类的 final 字段有哪些特殊保证?

JVM 保证:在对象引用对其他线程可见之前,其 final 字段一定已完成初始化

这防止了因指令重排序导致其他线程看到“部分构造”的对象。

✅ 最佳实践:创建不可变对象时,应将所有字段声明为 final


Q13. synchronized 关键字在方法、静态方法和代码块中的含义是什么?

  • synchronized(object) { ... }:线程必须先获取 object 的监视器(monitor)才能进入代码块。
  • 实例方法上的 synchronized:隐式使用 this 作为 monitor。
  • 静态方法上的 synchronized:使用该类的 Class 对象作为 monitor。

Q14. 两个线程同时调用不同对象实例的 synchronized 方法,会阻塞吗?如果是静态方法呢?

  • 实例方法:每个实例有自己的 monitor,因此不会阻塞
  • 静态方法:所有线程共享同一个 Class 对象作为 monitor,因此会阻塞

Q15. Object 类的 wait()notify()notifyAll() 方法的作用是什么?

这些方法用于线程间的协作:

  • wait():当前线程释放 monitor 并进入等待状态,直到被唤醒。
  • notify():唤醒一个等待该 monitor 的线程。
  • notifyAll():唤醒所有等待该 monitor 的线程。

⚠️ 必须在 synchronized 块中调用,否则抛出 IllegalMonitorStateException

示例:阻塞队列

public class BlockingQueue<T> {
    private List<T> queue = new LinkedList<>();
    private int limit = 10;

    public synchronized void put(T item) {
        while (queue.size() == limit) {
            try { wait(); } catch (InterruptedException e) {}
        }
        if (queue.isEmpty()) notifyAll();
        queue.add(item);
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            try { wait(); } catch (InterruptedException e) {}
        }
        if (queue.size() == limit) notifyAll();
        return queue.remove(0);
    }
}

Q16. 死锁、活锁和饥饿的定义及成因是什么?

  • 死锁(Deadlock):

    • 多个线程互相持有对方所需的资源,彼此等待,无法继续。
    • 经典条件:互斥、持有并等待、不可抢占、循环等待。
  • 活锁(Livelock):

    • 线程不断响应彼此的动作,反复重试但无法前进(如两人在走廊互相避让却始终撞上)。
    • 线程未阻塞,但无实质进展。
  • 饥饿(Starvation):

    • 某线程因资源长期被高优先级线程占用而无法获得执行机会。

Q17. Fork/Join 框架的用途和使用场景是什么?

Fork/Join 框架用于高效并行化递归算法(如快速排序、分治算法)。

问题:若用普通线程池处理递归,高层线程会阻塞等待子任务完成,导致线程浪费。

解决方案ForkJoinPool 实现“工作窃取”算法——空闲线程从其他线程的任务队列尾部“偷取”任务执行,提高 CPU 利用率。

入口类ForkJoinPool(实现了 ExecutorService)。

📚 更多信息可参考《Java Fork/Join 框架指南》。