← 返回面试专题导航

🧵 线程与进程面试题

深入理解并发编程的基础概念

本页系统讲解线程与进程的区别、Java 线程的创建方式、线程状态转换等核心知识点。

🎯 难度筛选

一、进程与线程

1. 进程和线程的区别是什么? 简单
┌──────────────┬──────────────────┬──────────────────┐ │ 特性 │ 进程 │ 线程 │ ├──────────────┼──────────────────┼──────────────────┤ │ 定义 │ 资源分配的基本单位│ CPU调度的基本单位│ │ 资源 │ 独立的内存空间 │ 共享进程的内存 │ │ 开销 │ 创建/切换开销大 │ 创建/切换开销小 │ │ 通信 │ IPC(管道/消息队列)│ 共享内存/变量 │ │ 独立性 │ 高度独立 │ 相互影响 │ │ 崩溃影响 │ 不影响其他进程 │ 可能导致进程崩溃 │ └──────────────┴──────────────────┴──────────────────┘

简单记忆:进程是资源的容器,线程是执行的最小单位。

2. 为什么要使用多线程?多线程有什么优缺点? 中等

优点:

  • 提高响应速度:耗时操作放后台线程,主线程继续响应用户。
  • 提高资源利用率:充分利用多核 CPU,提高吞吐量。
  • 简化程序结构:将复杂任务拆分成多个独立线程。

缺点:

  • 线程安全问题:需要处理共享数据的同步。
  • 上下文切换开销:线程过多会增加切换成本。
  • 死锁风险:不当使用锁可能导致死锁。
  • 调试困难:并发bug难以复现和定位。

二、Java 线程创建

3. Java 中创建线程有哪几种方式? 简单

四种方式:

// 1. 继承 Thread 类 class MyThread extends Thread { @Override public void run() { System.out.println("Thread running"); } } new MyThread().start(); // 2. 实现 Runnable 接口(推荐) class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable running"); } } new Thread(new MyRunnable()).start(); // 3. 实现 Callable 接口(有返回值) class MyCallable implements Callable { @Override public String call() throws Exception { return "Callable result"; } } FutureTask task = new FutureTask<>(new MyCallable()); new Thread(task).start(); String result = task.get(); // 4. 使用线程池(推荐) ExecutorService executor = Executors.newFixedThreadPool(5); executor.submit(() -> System.out.println("Thread pool"));
💡 推荐:实现 Runnable 接口 + 线程池,避免继承 Thread 类。
4. Runnable 和 Callable 的区别? 中等
┌──────────────┬──────────────┬──────────────┐ │ 特性 │ Runnable │ Callable │ ├──────────────┼──────────────┼──────────────┤ │ 返回值 │ 无(void) │ 有(泛型T) │ │ 异常处理 │ 不能抛出异常 │ 可以抛出异常 │ │ 方法名 │ run() │ call() │ │ JDK 版本 │ 1.0 │ 1.5 │ └──────────────┴──────────────┴──────────────┘
// Runnable:无返回值 Runnable runnable = () -> { System.out.println("No return value"); }; // Callable:有返回值 Callable callable = () -> { return 42; // 可以返回结果 }; // 使用 FutureTask 获取 Callable 的结果 FutureTask task = new FutureTask<>(callable); new Thread(task).start(); Integer result = task.get(); // 阻塞等待结果

三、线程状态

5. Java 线程有哪几种状态?如何转换? 中等

六种状态(Thread.State):

线程状态转换图: NEW (新建) ↓ start() RUNNABLE (可运行) ↓ ↑ ↓ ↑ 获得CPU ↓ ↑ ┌─────────────┐ │ BLOCKED │ ← 等待锁 │ WAITING │ ← wait()/join() │ TIMED_WAITING│ ← sleep()/wait(timeout) └─────────────┘ ↓ TERMINATED (终止)
  • NEW:线程创建但未调用 start()。
  • RUNNABLE:可运行状态(包括正在运行和等待 CPU)。
  • BLOCKED:等待获取锁(synchronized)。
  • WAITING:无限期等待(wait()/join())。
  • TIMED_WAITING:有时限等待(sleep()/wait(timeout))。
  • TERMINATED:线程执行完毕或异常终止。
6. sleep() 和 wait() 的区别? 中等
┌──────────────┬──────────────┬──────────────┐ │ 特性 │ sleep() │ wait() │ ├──────────────┼──────────────┼──────────────┤ │ 所属类 │ Thread │ Object │ │ 释放锁 │ 不释放 │ 释放 │ │ 使用场景 │ 暂停执行 │ 线程通信 │ │ 唤醒方式 │ 自动唤醒 │ notify()唤醒 │ │ 异常 │ InterruptedException │ InterruptedException │ └──────────────┴──────────────┴──────────────┘
// sleep:不释放锁 synchronized (lock) { Thread.sleep(1000); // 持有锁,其他线程无法进入 } // wait:释放锁 synchronized (lock) { lock.wait(); // 释放锁,其他线程可以进入 // 被 notify() 唤醒后继续执行 }

四、线程方法

7. start() 和 run() 的区别? 简单
  • start():启动新线程,JVM 调用 run() 方法。
  • run():普通方法调用,不会创建新线程。
Thread thread = new Thread(() -> { System.out.println("Thread: " + Thread.currentThread().getName()); }); thread.start(); // 输出:Thread: Thread-0(新线程) thread.run(); // 输出:Thread: main(当前线程)
⚠️ 注意:直接调用 run() 不会创建新线程,只是普通方法调用!
8. join() 方法的作用是什么? 中等

作用:等待线程执行完毕。

Thread thread = new Thread(() -> { try { Thread.sleep(2000); System.out.println("Thread finished"); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); thread.join(); // 主线程等待 thread 执行完毕 System.out.println("Main finished"); // 输出顺序: // Thread finished // Main finished

应用场景:需要等待子线程完成后再继续执行(如汇总多个线程的计算结果)。

9. 如何优雅地停止一个线程? 中等

推荐方式:使用中断标志

// ❌ 不推荐:stop() 方法已废弃 thread.stop(); // 强制停止,可能导致数据不一致 // ✅ 推荐:使用 interrupt() + 检查中断标志 class MyThread extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { // 执行任务 try { Thread.sleep(100); } catch (InterruptedException e) { // 捕获中断异常后,重新设置中断标志 Thread.currentThread().interrupt(); break; } } // 清理资源 } } MyThread thread = new MyThread(); thread.start(); thread.interrupt(); // 请求中断
10. 守护线程(Daemon Thread)是什么?有什么应用场景? 困难

定义:守护线程是为其他线程提供服务的后台线程,当所有非守护线程结束时,JVM 会退出,守护线程也会被强制终止。

Thread daemon = new Thread(() -> { while (true) { System.out.println("Daemon running"); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } }); daemon.setDaemon(true); // 设置为守护线程 daemon.start(); // 主线程结束后,守护线程也会自动终止

应用场景:

  • 垃圾回收线程(GC)
  • 日志记录线程
  • 监控线程
  • 心跳检测线程
⚠️ 注意:守护线程不应该访问共享资源(如文件、数据库),因为可能在操作未完成时被强制终止。

五、线程安全与同步

11. 什么是线程安全?如何保证线程安全? 中等

线程安全:多线程访问共享资源时,程序能正确执行,不会产生数据不一致或异常。

保证线程安全的方式: ┌─────────────────────────────────────────────────────────────┐ │ 方式 │ 实现 │ 适用场景 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 不可变对象 │ final + 不可变类 │ 配置、常量 │ │ 2. 线程封闭 │ ThreadLocal │ 用户会话、连接 │ │ 3. 同步机制 │ synchronized/Lock │ 共享资源访问 │ │ 4. 原子类 │ AtomicInteger等 │ 计数器、标志位 │ │ 5. 并发容器 │ ConcurrentHashMap │ 高并发集合操作 │ │ 6. 无状态设计 │ 方法内局部变量 │ 工具类、服务类 │ └─────────────────────────────────────────────────────────────┘
// 1. synchronized 关键字 public synchronized void increment() { count++; } // 2. Lock 接口 private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } // 3. 原子类 private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); }
12. synchronized 和 Lock 的区别? 困难
┌──────────────────┬──────────────────┬──────────────────────┐ │ 特性 │ synchronized │ Lock (ReentrantLock)│ ├──────────────────┼──────────────────┼──────────────────────┤ │ 实现方式 │ JVM 内置关键字 │ Java API 接口 │ │ 锁的获取 │ 自动获取/释放 │ 手动 lock()/unlock() │ │ 可中断 │ 不可中断 │ lockInterruptibly() │ │ 超时获取 │ 不支持 │ tryLock(timeout) │ │ 公平锁 │ 非公平 │ 可选公平/非公平 │ │ 条件变量 │ 单一 wait/notify │ 多个 Condition │ │ 性能 │ JDK6后优化较好 │ 高并发下更优 │ │ 异常处理 │ 自动释放锁 │ 需 finally 释放 │ └──────────────────┴──────────────────┴──────────────────────┘
// Lock 的高级特性示例 ReentrantLock lock = new ReentrantLock(true); // 公平锁 // 1. 可中断获取锁 lock.lockInterruptibly(); // 2. 超时获取锁 if (lock.tryLock(1, TimeUnit.SECONDS)) { try { /* 业务逻辑 */ } finally { lock.unlock(); } } // 3. 多条件变量 Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition();
💡 选择建议:简单场景用 synchronized,需要高级特性(超时、可中断、公平锁)用 Lock。
13. volatile 关键字的作用?和 synchronized 有什么区别? 困难

volatile 的两个作用:

  • 可见性:一个线程修改后,其他线程立即可见。
  • 禁止指令重排序:防止编译器和 CPU 优化导致的乱序执行。
volatile vs synchronized 对比: ┌──────────────┬──────────────────┬──────────────────┐ │ 特性 │ volatile │ synchronized │ ├──────────────┼──────────────────┼──────────────────┤ │ 可见性 │ ✅ │ ✅ │ │ 原子性 │ ❌(仅单次读写) │ ✅ │ │ 有序性 │ ✅ │ ✅ │ │ 阻塞 │ 不阻塞 │ 阻塞 │ │ 性能 │ 高 │ 相对较低 │ │ 适用场景 │ 状态标志、DCL │ 复合操作 │ └──────────────┴──────────────────┴──────────────────┘
// volatile 典型应用:状态标志 private volatile boolean running = true; public void stop() { running = false; } public void run() { while (running) { /* 工作 */ } } // volatile 不能保证原子性! private volatile int count = 0; count++; // ❌ 非原子操作,线程不安全 // DCL 单例模式中的 volatile private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 防止重排序 } } } return instance; }

六、线程通信

14. wait()、notify()、notifyAll() 的使用方式? 中等

核心要点:必须在 synchronized 块内调用,且调用对象必须是锁对象。

// 生产者-消费者模型 class Buffer { private Queue queue = new LinkedList<>(); private int capacity = 10; public synchronized void produce(int item) throws InterruptedException { while (queue.size() == capacity) { wait(); // 队列满,等待消费者 } queue.add(item); notifyAll(); // 通知消费者 } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); // 队列空,等待生产者 } int item = queue.poll(); notifyAll(); // 通知生产者 return item; } }
⚠️ 注意:wait() 必须在 while 循环中检查条件,防止虚假唤醒。
15. ThreadLocal 是什么?有什么应用场景? 中等

定义:ThreadLocal 为每个线程提供独立的变量副本,实现线程隔离。

ThreadLocal 原理: Thread 对象 ↓ ThreadLocalMap (每个线程独有) ↓ ┌─────────────┬─────────────┐ │ ThreadLocal │ Value │ │ (key) │ (副本值) │ └─────────────┴─────────────┘
// 典型应用:用户上下文 public class UserContext { private static ThreadLocal userHolder = new ThreadLocal<>(); public static void setUser(User user) { userHolder.set(user); } public static User getUser() { return userHolder.get(); } public static void clear() { userHolder.remove(); } // 防止内存泄漏 } // 使用示例 UserContext.setUser(currentUser); try { // 业务逻辑中随时获取用户 User user = UserContext.getUser(); } finally { UserContext.clear(); // 必须清理! }

应用场景:用户会话、数据库连接、日期格式化器、事务管理。

⚠️ 内存泄漏:使用线程池时,线程复用会导致 ThreadLocal 值残留,必须在 finally 中调用 remove()。

七、死锁与并发问题

16. 什么是死锁?如何避免死锁? 困难

死锁四个必要条件:

死锁产生条件(同时满足): 1. 互斥条件:资源只能被一个线程持有 2. 占有并等待:持有资源的同时等待其他资源 3. 不可剥夺:资源只能由持有者主动释放 4. 循环等待:线程之间形成环形等待链 死锁示例: ┌─────────┐ ┌─────────┐ │ Thread1 │ ──持有→ │ LockA │ │ │ ←等待── │ │ └─────────┘ └─────────┘ ↑ ↓ 等待 持有 ↓ ↑ ┌─────────┐ ┌─────────┐ │ LockB │ ←持有── │ Thread2 │ │ │ ──等待→ │ │ └─────────┘ └─────────┘
// ❌ 死锁代码示例 Thread t1 = new Thread(() -> { synchronized (lockA) { Thread.sleep(100); synchronized (lockB) { /* 业务 */ } } }); Thread t2 = new Thread(() -> { synchronized (lockB) { Thread.sleep(100); synchronized (lockA) { /* 业务 */ } } }); // ✅ 避免死锁:固定加锁顺序 Thread t1 = new Thread(() -> { synchronized (lockA) { synchronized (lockB) { /* 业务 */ } } }); Thread t2 = new Thread(() -> { synchronized (lockA) { // 相同顺序 synchronized (lockB) { /* 业务 */ } } });

避免死锁的方法:

  • 固定加锁顺序:所有线程按相同顺序获取锁。
  • 超时获取锁:使用 tryLock(timeout) 避免无限等待。
  • 死锁检测:使用 jstack 或 JConsole 检测死锁。
  • 减少锁粒度:缩小同步代码块范围。
17. 什么是线程饥饿和活锁? 中等
┌──────────────┬──────────────────────────────────────────┐ │ 问题 │ 描述 │ ├──────────────┼──────────────────────────────────────────┤ │ 线程饥饿 │ 线程长期无法获取资源(如低优先级线程) │ │ 活锁 │ 线程不断重试但无法推进(如互相让步) │ │ 死锁 │ 线程互相等待对方释放资源,永久阻塞 │ └──────────────┴──────────────────────────────────────────┘
// 活锁示例:两个线程互相让步 while (true) { if (other.isWaiting()) { Thread.yield(); // 让步给对方 continue; // 但对方也在让步,导致都无法执行 } // 执行业务 } // 解决方案:引入随机等待时间 while (true) { if (other.isWaiting()) { Thread.sleep(random.nextInt(100)); // 随机等待 continue; } // 执行业务 }

八、线程池

18. 线程池的核心参数有哪些?如何配置? 困难
ThreadPoolExecutor 7大核心参数: ┌─────────────────────────────────────────────────────────────┐ │ 参数 │ 说明 │ ├─────────────────────────────────────────────────────────────┤ │ corePoolSize │ 核心线程数(常驻线程) │ │ maximumPoolSize │ 最大线程数 │ │ keepAliveTime │ 非核心线程空闲存活时间 │ │ unit │ 时间单位 │ │ workQueue │ 任务队列(阻塞队列) │ │ threadFactory │ 线程工厂(创建线程) │ │ handler │ 拒绝策略(队列满时处理) │ └─────────────────────────────────────────────────────────────┘ 执行流程: 任务提交 → 核心线程 → 队列 → 非核心线程 → 拒绝策略
// 自定义线程池(推荐) ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, // 核心线程数 10, // 最大线程数 60, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue<>(100), // 任务队列 new ThreadFactory() { // 自定义线程工厂 private AtomicInteger count = new AtomicInteger(0); public Thread newThread(Runnable r) { return new Thread(r, "MyThread-" + count.incrementAndGet()); } }, new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); // 线程数配置建议 // CPU密集型:N + 1(N为CPU核心数) // IO密集型:2N 或 N / (1 - 阻塞系数)
⚠️ 不推荐:Executors.newFixedThreadPool() 等工厂方法,因为队列无界可能导致 OOM。
19. 线程池的拒绝策略有哪些? 中等
四种内置拒绝策略: ┌─────────────────────┬──────────────────────────────────────┐ │ 策略 │ 行为 │ ├─────────────────────┼──────────────────────────────────────┤ │ AbortPolicy │ 抛出 RejectedExecutionException(默认)│ │ CallerRunsPolicy │ 由提交任务的线程执行 │ │ DiscardPolicy │ 静默丢弃任务 │ │ DiscardOldestPolicy │ 丢弃队列中最老的任务,重新提交 │ └─────────────────────┴──────────────────────────────────────┘
// 自定义拒绝策略 RejectedExecutionHandler handler = (r, executor) -> { // 记录日志 log.warn("任务被拒绝: " + r.toString()); // 可选:持久化到数据库或消息队列 // 可选:重试机制 };
💡 推荐:CallerRunsPolicy 可以降低提交速度,实现优雅降级。

九、并发工具类

20. CountDownLatch 和 CyclicBarrier 的区别? 中等
┌──────────────────┬──────────────────┬──────────────────┐ │ 特性 │ CountDownLatch │ CyclicBarrier │ ├──────────────────┼──────────────────┼──────────────────┤ │ 计数方式 │ 递减到0 │ 递增到阈值 │ │ 可重用 │ ❌ 一次性 │ ✅ 可重置 │ │ 等待方式 │ 一个线程等多个 │ 多个线程互相等待 │ │ 典型场景 │ 主线程等待子任务 │ 多线程同时开始 │ └──────────────────┴──────────────────┴──────────────────┘
// CountDownLatch:主线程等待多个子任务完成 CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { executor.submit(() -> { doTask(); latch.countDown(); // 完成一个任务 }); } latch.await(); // 主线程等待所有任务完成 // CyclicBarrier:多个线程互相等待,同时开始 CyclicBarrier barrier = new CyclicBarrier(3, () -> { System.out.println("所有线程就绪,开始执行"); }); for (int i = 0; i < 3; i++) { executor.submit(() -> { prepare(); barrier.await(); // 等待其他线程 execute(); // 同时开始执行 }); }
21. Semaphore 信号量的作用和使用场景? 中等

作用:控制同时访问资源的线程数量(限流)。

// 限制同时访问数据库的连接数 Semaphore semaphore = new Semaphore(10); // 最多10个并发 public void accessDatabase() { try { semaphore.acquire(); // 获取许可 // 访问数据库 } finally { semaphore.release(); // 释放许可 } } // 应用场景: // 1. 数据库连接池限流 // 2. 接口限流 // 3. 资源池管理

🏆 高频面试真题

22. 【阿里】如何实现一个线程安全的单例模式? 困难
// 方式1:双重检查锁(DCL) public class Singleton { private static volatile Singleton instance; // volatile 防止重排序 private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } } // 方式2:静态内部类(推荐) public class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; // 类加载时初始化,天然线程安全 } } // 方式3:枚举(最简洁) public enum Singleton { INSTANCE; public void doSomething() { } }
💡 推荐:静态内部类或枚举方式,简洁且线程安全。
23. 【字节】三个线程交替打印 ABC,如何实现? 困难
// 方式1:使用 Lock + Condition class PrintABC { private int state = 0; private Lock lock = new ReentrantLock(); private Condition condA = lock.newCondition(); private Condition condB = lock.newCondition(); private Condition condC = lock.newCondition(); public void printA() { lock.lock(); try { while (state % 3 != 0) condA.await(); System.out.print("A"); state++; condB.signal(); } finally { lock.unlock(); } } public void printB() { lock.lock(); try { while (state % 3 != 1) condB.await(); System.out.print("B"); state++; condC.signal(); } finally { lock.unlock(); } } public void printC() { lock.lock(); try { while (state % 3 != 2) condC.await(); System.out.print("C"); state++; condA.signal(); } finally { lock.unlock(); } } } // 方式2:使用 Semaphore Semaphore sa = new Semaphore(1); Semaphore sb = new Semaphore(0); Semaphore sc = new Semaphore(0); // 线程A:sa.acquire() → print("A") → sb.release() // 线程B:sb.acquire() → print("B") → sc.release() // 线程C:sc.acquire() → print("C") → sa.release()
24. 【腾讯】如何设计一个高性能的生产者-消费者模型? 困难
// 方式1:BlockingQueue(推荐) BlockingQueue queue = new LinkedBlockingQueue<>(1000); // 生产者 executor.submit(() -> { while (running) { Task task = createTask(); queue.put(task); // 队列满时阻塞 } }); // 消费者 executor.submit(() -> { while (running) { Task task = queue.take(); // 队列空时阻塞 process(task); } }); // 方式2:Disruptor(高性能) // 无锁环形队列,适用于超高并发场景 // 设计要点: // 1. 队列容量:根据生产/消费速度设置 // 2. 消费者数量:根据处理能力配置 // 3. 背压机制:队列满时的处理策略 // 4. 优雅关闭:处理完队列中的任务再关闭