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. Runnable 和 Callable 接口有什么区别?如何使用?
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. Executor 和 ExecutorService 是什么?它们有何区别?
Executor:非常简单的接口,只有一个execute(Runnable command)方法,用于执行任务。ExecutorService:继承自Executor,增加了管理生命周期的方法(如shutdown())、提交带返回值任务的方法(如submit()),以及处理Future的能力。
通常,业务代码应依赖 Executor 接口,而需要高级控制时才使用 ExecutorService。
Q8. 标准库中有哪些 ExecutorService 的实现?
标准库提供了三种主要实现:
ThreadPoolExecutor:使用线程池执行任务。任务完成后线程归还池中;若池满,新任务需排队。ScheduledThreadPoolExecutor:支持延迟执行或周期性执行任务(如scheduleAtFixedRate)。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 字段具有特殊语义:
- 可见性保证:对
volatile变量的读写是同步动作,所有线程看到的操作顺序一致。 - 禁止重排序:编译器和处理器不会对
volatile读写进行重排序。 - 原子性:对
long和double(64 位)的读写在非volatile情况下可能分两步执行,但volatile保证其原子性。
✅ 建议:若字段被多个线程访问,且至少有一个线程写入,应考虑使用
volatile。
Q11. 以下哪些操作是原子的?
- 写入非
volatile的int→ 是(32 位写入原子) - 写入
volatile的int→ 是 - 写入非
volatile的long→ 否(在 32 位系统上可能分两次) - 写入
volatile的long→ 是(JMM 保证 64 位原子) - 对
volatile long执行++→ 否(++包含读-改-写三步,非原子)
✅ 解决方案:使用
AtomicLong、AtomicInteger等原子类。
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 框架指南》。