Java 多线程与并发面试题
目录
线程基础
1. 什么是线程?如何创建线程?
答案要点:
- 线程的概念
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 线程池的使用
示例答案: "线程是程序执行的最小单位,是 CPU 调度的基本单位。Java 中创建线程有三种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。继承 Thread 类是最简单的方式,但 Java 不支持多重继承,限制了类的扩展性。实现 Runnable 接口更灵活,可以继承其他类,适合只需要重写 run 方法的场景。实现 Callable 接口可以返回值和抛出异常,配合 Future 使用。在实际项目中,我推荐使用线程池而不是直接创建线程,线程池可以复用线程,控制并发数量,提供更好的性能和资源管理。ExecutorService 提供了丰富的线程池实现,如 FixedThreadPool、CachedThreadPool 等。"
深入解析:
- 线程概念:程序执行的最小单位,CPU 调度基本单位
- 创建方式:继承 Thread、实现 Runnable、实现 Callable
- 线程池:复用线程,控制并发,提高性能
- 最佳实践:优先使用线程池
2. 线程的生命周期是怎样的?
答案要点:
- 线程状态
- 状态转换
- 状态监控
- 实际应用
示例答案: "Java 线程的生命周期包括六个状态:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)、TERMINATED(终止)。NEW 状态是线程创建但未启动;RUNNABLE 状态是线程正在运行或等待 CPU 调度;BLOCKED 状态是线程等待获取锁;WAITING 状态是线程无限期等待;TIMED_WAITING 状态是线程有限期等待;TERMINATED 状态是线程执行完毕。在实际项目中,我会监控线程状态,使用 Thread.getState() 获取线程状态,使用 jstack 工具分析线程转储,诊断线程问题。"
深入解析:
- NEW:线程创建,未调用 start()
- RUNNABLE:可运行状态,包括运行和就绪
- BLOCKED:阻塞状态,等待获取锁
- WAITING:等待状态,无限期等待
- TIMED_WAITING:超时等待状态
- TERMINATED:终止状态,线程执行完毕
3. 进程和线程的区别是什么?
答案要点:
- 资源占用
- 通信方式
- 创建开销
- 安全性
示例答案: "进程和线程在多个方面有重要区别。资源占用方面,进程拥有独立的内存空间,线程共享进程的内存空间。通信方式方面,进程间通信需要 IPC 机制,线程间通信可以直接访问共享变量。创建开销方面,创建进程开销大,创建线程开销小。安全性方面,进程间相互独立,一个进程崩溃不影响其他进程,线程间相互影响,一个线程崩溃可能导致整个进程崩溃。在实际项目中,我会根据需求选择:需要高隔离性使用进程,需要高性能使用线程。"
深入解析:
| 特性 | 进程 | 线程 |
|---|---|---|
| 内存空间 | 独立 | 共享 |
| 通信方式 | IPC | 共享变量 |
| 创建开销 | 大 | 小 |
| 安全性 | 高 | 低 |
| 切换开销 | 大 | 小 |
线程创建与生命周期
4. 为什么推荐实现 Runnable 而不是继承 Thread?
答案要点:
- 单继承限制
- 代码复用
- 设计原则
- 实际应用
示例答案: "推荐实现 Runnable 而不是继承 Thread 有多个原因。首先,Java 只支持单继承,继承 Thread 会占用继承机会,限制类的扩展性。其次,实现 Runnable 可以继承其他类,提高代码复用性。第三,实现 Runnable 符合面向接口编程的原则,降低耦合度。第四,实现 Runnable 可以更好地实现资源共享,多个线程可以共享同一个 Runnable 对象。在实际项目中,我会优先实现 Runnable 接口,只有在需要重写 Thread 的其他方法时才继承 Thread 类。"
深入解析:
- 单继承限制:Java 单继承,继承 Thread 占用继承机会
- 代码复用:实现 Runnable 可以继承其他类
- 设计原则:面向接口编程,降低耦合
- 资源共享:多个线程共享 Runnable 对象
5. 如何正确启动和停止线程?
答案要点:
- 启动线程
- 停止线程
- 线程中断
- 最佳实践
示例答案: "正确启动线程使用 start() 方法,它会创建新的线程并调用 run() 方法。不能直接调用 run() 方法,那只是在当前线程中执行。停止线程应该使用中断机制,调用 interrupt() 方法设置中断标志,线程检查中断状态并响应中断。不能使用已废弃的 stop() 方法,它可能导致数据不一致。在实际项目中,我会使用中断机制停止线程,在 run() 方法中检查 Thread.interrupted() 或 isInterrupted(),及时响应中断请求,确保线程能够安全退出。"
深入解析:
- 启动线程:使用 start() 方法,创建新线程
- 停止线程:使用中断机制,检查中断状态
- 中断处理:Thread.interrupted()、isInterrupted()
- 安全退出:响应中断,清理资源
6. 什么是守护线程?如何使用守护线程?
答案要点:
- 守护线程的概念
- 与用户线程的区别
- 使用场景
- 注意事项
示例答案: "守护线程是为其他线程提供服务的线程,当所有用户线程结束时,守护线程会自动结束。守护线程与用户线程的区别在于:用户线程阻止 JVM 退出,守护线程不会阻止 JVM 退出。守护线程通常用于后台任务,如垃圾回收、日志记录、监控等。设置守护线程使用 setDaemon(true) 方法,必须在 start() 之前调用。在实际项目中,我会将后台服务线程设置为守护线程,如定时任务、监控线程等,确保主程序结束时这些线程也能正常退出。"
深入解析:
- 守护线程:为其他线程提供服务,不阻止 JVM 退出
- 用户线程:主要业务线程,阻止 JVM 退出
- 设置方法:setDaemon(true),必须在 start() 前调用
- 使用场景:后台服务、监控、定时任务
线程同步
7. synchronized 关键字的作用是什么?
答案要点:
- 线程同步
- 对象锁和类锁
- 可重入性
- 性能考虑
示例答案: "synchronized 是 Java 中实现线程同步的关键字,可以修饰方法或代码块。修饰实例方法时,锁的是当前对象实例;修饰静态方法时,锁的是当前类对象;修饰代码块时,可以指定锁对象。synchronized 具有可重入性,同一个线程可以多次获取同一个锁。synchronized 是悲观锁,在竞争激烈的情况下性能较差,因为线程会进入阻塞状态。在实际项目中,我会根据同步需求选择合适的锁机制,对于简单的同步需求使用 synchronized,对于复杂的并发控制使用 ReentrantLock、ReadWriteLock 等。还会考虑使用 volatile 关键字保证可见性,使用 AtomicInteger 等原子类避免锁竞争。"
深入解析:
- 同步机制:保证同一时间只有一个线程访问
- 锁类型:对象锁、类锁
- 可重入性:同一线程可多次获取锁
- 性能影响:悲观锁,竞争激烈时性能差
8. volatile 关键字的作用是什么?
答案要点:
- 可见性保证
- 禁止指令重排
- 不保证原子性
- 使用场景
示例答案: "volatile 关键字主要用于保证变量的可见性和禁止指令重排序。当一个变量被 volatile 修饰时,一个线程对该变量的修改会立即对其他线程可见,避免了缓存一致性问题。volatile 还通过内存屏障禁止编译器和处理器对指令进行重排序,保证程序执行的有序性。但是,volatile 不保证原子性,对于复合操作(如 i++)仍然需要同步机制。volatile 适合作为状态标志,如线程的停止标志,或者作为双重检查锁定模式中的变量修饰符。在实际项目中,我会使用 volatile 保证简单变量的可见性,对于复杂的同步需求,会结合 synchronized 或 Lock 使用。"
深入解析:
- 可见性:修改立即对其他线程可见
- 有序性:禁止指令重排序
- 原子性:不保证复合操作的原子性
- 使用场景:状态标志、双重检查锁定
9. 什么是死锁?如何避免死锁?
答案要点:
- 死锁的定义
- 死锁产生的条件
- 死锁检测
- 避免策略
示例答案: "死锁是指两个或多个线程相互等待对方释放资源,导致所有线程都无法继续执行的状态。死锁产生的四个必要条件是:互斥条件、请求和保持条件、不剥夺条件、循环等待条件。检测死锁可以使用 jstack 工具分析线程转储,查看线程的锁等待关系。避免死锁的策略包括:按顺序获取锁,避免循环等待;使用超时机制,避免无限等待;使用 tryLock() 方法,获取锁失败时释放已持有的锁;减少锁的粒度,使用更细粒度的锁。在实际项目中,我会设计锁的获取顺序,使用超时机制,定期检查死锁情况。"
深入解析:
- 死锁条件:互斥、请求保持、不剥夺、循环等待
- 检测方法:jstack 分析线程转储
- 避免策略:顺序获取锁、超时机制、tryLock()
- 预防措施:减少锁粒度、避免嵌套锁
锁机制
10. ReentrantLock 和 synchronized 的区别是什么?
答案要点:
- 实现机制
- 功能特性
- 性能差异
- 使用场景
示例答案: "ReentrantLock 和 synchronized 在多个方面有重要区别。实现机制方面,synchronized 是 JVM 内置的锁机制,ReentrantLock 是 JDK 实现的锁。功能特性方面,ReentrantLock 提供更多功能,如可中断锁、超时锁、公平锁等。性能方面,在低竞争情况下 synchronized 性能更好,在高竞争情况下 ReentrantLock 性能更好。使用场景方面,synchronized 适合简单的同步需求,ReentrantLock 适合复杂的并发控制。在实际项目中,我会根据具体需求选择:简单同步使用 synchronized,需要高级功能使用 ReentrantLock。"
深入解析:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现 | JVM 内置 | JDK 实现 |
| 功能 | 基础同步 | 高级功能 |
| 性能 | 低竞争好 | 高竞争好 |
| 使用 | 简单 | 复杂 |
11. 什么是读写锁?如何使用读写锁?
答案要点:
- 读写锁的概念
- ReadWriteLock 接口
- ReentrantReadWriteLock 实现
- 使用场景
示例答案: "读写锁是一种特殊的锁机制,允许多个线程同时读取,但只允许一个线程写入。ReadWriteLock 接口定义了读写锁的规范,ReentrantReadWriteLock 是其实现。读写锁提供了 readLock() 和 writeLock() 方法获取读锁和写锁。读锁之间不互斥,读锁和写锁互斥,写锁之间互斥。读写锁适用于读多写少的场景,如缓存系统、配置管理等。在实际项目中,我会在读多写少的场景下使用读写锁,提高并发性能,注意避免写锁饥饿问题。"
深入解析:
- 读锁:多个线程可同时获取,与写锁互斥
- 写锁:独占锁,与读锁和其他写锁互斥
- 适用场景:读多写少,如缓存、配置
- 注意事项:避免写锁饥饿
12. 什么是条件变量?如何使用条件变量?
答案要点:
- 条件变量的概念
- Condition 接口
- await() 和 signal() 方法
- 使用场景
示例答案: "条件变量是一种线程同步机制,允许线程等待特定条件满足。Condition 接口提供了 await()、signal()、signalAll() 等方法。await() 方法使当前线程等待,signal() 方法唤醒一个等待线程,signalAll() 方法唤醒所有等待线程。条件变量通常与锁配合使用,实现复杂的同步逻辑。在实际项目中,我会使用条件变量实现生产者-消费者模式、等待-通知模式等,注意使用 while 循环检查条件,避免虚假唤醒。"
深入解析:
- await():等待条件满足
- signal():唤醒一个等待线程
- signalAll():唤醒所有等待线程
- 使用模式:while 循环检查条件
线程池
13. 什么是线程池?如何合理配置线程池?
答案要点:
- 线程池的优势
- 核心参数
- 拒绝策略
- 监控和调优
示例答案: "线程池是一种线程复用机制,通过预先创建一定数量的线程,避免频繁创建和销毁线程的开销。线程池的核心参数包括核心线程数、最大线程数、线程存活时间、工作队列和拒绝策略。核心线程数是线程池中保持活跃的线程数量,最大线程数是允许创建的最大线程数量。工作队列用于存储等待执行的任务,常用的有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。拒绝策略决定当线程池和队列都满时如何处理新任务。在实际项目中,我会根据任务类型和系统资源合理配置线程池,CPU 密集型任务使用核心数 + 1 的线程数,IO 密集型任务使用核心数 * 2 的线程数。还会监控线程池的运行状态,及时调整参数。"
深入解析:
- 核心线程数:保持活跃的线程数量
- 最大线程数:允许创建的最大线程数量
- 工作队列:存储等待执行的任务
- 拒绝策略:队列满时的处理策略
14. 线程池的拒绝策略有哪些?
答案要点:
- AbortPolicy
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
示例答案: "线程池的拒绝策略有四种:AbortPolicy 是默认策略,直接抛出 RejectedExecutionException 异常;CallerRunsPolicy 由调用线程执行任务,降低提交速度;DiscardPolicy 直接丢弃任务,不抛出异常;DiscardOldestPolicy 丢弃队列中最老的任务,然后重试提交。在实际项目中,我会根据业务需求选择合适的拒绝策略:对于重要任务使用 AbortPolicy,确保任务不丢失;对于可以延迟的任务使用 CallerRunsPolicy,降低提交速度;对于可以丢弃的任务使用 DiscardPolicy。"
深入解析:
- AbortPolicy:抛出异常,默认策略
- CallerRunsPolicy:调用线程执行
- DiscardPolicy:直接丢弃
- DiscardOldestPolicy:丢弃最老任务
15. 如何监控线程池的运行状态?
答案要点:
- 线程池状态
- 监控指标
- 监控工具
- 调优策略
示例答案: "监控线程池的运行状态需要关注多个指标。线程池状态包括 RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。监控指标包括:活跃线程数、队列大小、完成任务数、拒绝任务数等。可以使用 ThreadPoolExecutor 提供的方法获取这些指标,如 getActiveCount()、getQueue().size() 等。在实际项目中,我会定期监控这些指标,设置告警阈值,当指标异常时及时调整线程池参数。还会使用 JMX 或自定义监控系统进行长期监控。"
深入解析:
- 状态监控:RUNNING、SHUTDOWN、STOP 等
- 指标监控:活跃线程、队列大小、完成任务
- 监控工具:JMX、自定义监控
- 调优策略:根据指标调整参数
并发工具类
16. CountDownLatch 的作用和使用场景是什么?
答案要点:
- CountDownLatch 的概念
- 主要方法
- 使用场景
- 注意事项
示例答案: "CountDownLatch 是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。CountDownLatch 使用计数器实现,初始化为一个正数,每次调用 countDown() 方法计数器减 1,当计数器为 0 时,等待的线程被唤醒。主要方法包括 await() 等待计数器为 0,countDown() 计数器减 1。CountDownLatch 适用于一个线程等待多个线程完成的场景,如主线程等待多个子线程完成初始化。在实际项目中,我会使用 CountDownLatch 实现线程间的协调,注意计数器只能使用一次,不能重置。"
深入解析:
- 计数器机制:初始正数,countDown() 减 1
- 等待机制:await() 等待计数器为 0
- 使用场景:一个线程等待多个线程完成
- 注意事项:一次性使用,不能重置
17. CyclicBarrier 和 CountDownLatch 的区别是什么?
答案要点:
- 使用场景
- 重置能力
- 线程数量
- 异常处理
示例答案: "CyclicBarrier 和 CountDownLatch 在多个方面有重要区别。使用场景方面,CountDownLatch 是一个线程等待多个线程完成,CyclicBarrier 是多个线程相互等待。重置能力方面,CountDownLatch 不能重置,CyclicBarrier 可以重复使用。线程数量方面,CountDownLatch 的计数器可以大于等待线程数,CyclicBarrier 的屏障数必须等于等待线程数。异常处理方面,CyclicBarrier 提供了更好的异常处理机制。在实际项目中,我会根据具体需求选择:需要等待多个线程完成使用 CountDownLatch,需要多个线程同步使用 CyclicBarrier。"
深入解析:
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 使用场景 | 一个等多个 | 多个相互等 |
| 重置能力 | 不能重置 | 可以重复使用 |
| 线程数量 | 计数器可大于线程数 | 屏障数等于线程数 |
| 异常处理 | 基础 | 更好 |
18. Semaphore 的作用和使用场景是什么?
答案要点:
- Semaphore 的概念
- 信号量机制
- 主要方法
- 使用场景
示例答案: "Semaphore 是一个计数信号量,用于控制同时访问特定资源的线程数量。Semaphore 维护一个许可证计数器,acquire() 方法获取许可证,release() 方法释放许可证。当许可证数量为 0 时,acquire() 方法会阻塞。Semaphore 适用于限制并发访问的场景,如数据库连接池、文件访问等。在实际项目中,我会使用 Semaphore 控制资源访问,如限制同时下载的文件数量、限制同时访问的数据库连接数等。"
深入解析:
- 信号量机制:维护许可证计数器
- 获取许可证:acquire() 方法
- 释放许可证:release() 方法
- 使用场景:限制并发访问
原子操作
19. 什么是原子操作?Java 中有哪些原子类?
答案要点:
- 原子操作的概念
- 原子类分类
- 主要方法
- 使用场景
示例答案: "原子操作是不可分割的操作,要么全部执行,要么全部不执行。Java 提供了丰富的原子类,包括 AtomicInteger、AtomicLong、AtomicBoolean 等基本类型原子类,AtomicReference 等引用类型原子类,AtomicIntegerArray 等数组类型原子类。原子类使用 CAS(Compare and Swap)操作实现无锁并发,性能比 synchronized 更好。在实际项目中,我会使用原子类实现计数器、状态标志等,避免使用 synchronized 的性能开销。"
深入解析:
- 原子操作:不可分割的操作
- CAS 机制:Compare and Swap 无锁并发
- 原子类:AtomicInteger、AtomicLong、AtomicReference 等
- 性能优势:比 synchronized 性能更好
20. CAS 操作的原理是什么?
答案要点:
- CAS 的概念
- 实现原理
- ABA 问题
- 解决方案
示例答案: "CAS(Compare and Swap)是一种无锁并发算法,通过比较内存值和期望值,如果相等则更新为新值。CAS 操作包括三个参数:内存地址、期望值、新值。如果内存中的值等于期望值,则更新为新值,否则操作失败。CAS 操作是原子的,由 CPU 指令保证。CAS 存在 ABA 问题,即值从 A 变为 B 再变为 A,CAS 操作会认为值没有变化。解决 ABA 问题可以使用版本号或 AtomicStampedReference。在实际项目中,我会使用 CAS 操作实现无锁并发,注意处理 ABA 问题。"
深入解析:
- CAS 操作:比较并交换,原子操作
- 实现原理:CPU 指令保证原子性
- ABA 问题:值变化但最终相同
- 解决方案:版本号、AtomicStampedReference
内存模型
21. Java 内存模型(JMM)是什么?
答案要点:
- JMM 的概念
- 主内存和工作内存
- 内存可见性
- happens-before 规则
示例答案: "Java 内存模型(JMM)定义了多线程程序中变量的访问规则,确保多线程程序的正确性。JMM 将内存分为主内存和工作内存,每个线程都有自己的工作内存,存储变量的副本。线程对变量的操作在工作内存中进行,然后同步到主内存。JMM 通过 happens-before 规则保证内存可见性,如程序顺序规则、锁规则、volatile 规则等。在实际项目中,我会遵循 JMM 规则,使用 volatile 保证可见性,使用 synchronized 保证原子性和可见性。"
深入解析:
- 主内存:共享内存,存储所有变量
- 工作内存:线程私有,存储变量副本
- 内存可见性:线程间变量的可见性
- happens-before:内存操作的顺序规则
22. 什么是内存可见性?如何保证内存可见性?
答案要点:
- 内存可见性的概念
- 可见性问题
- 保证方法
- 实际应用
示例答案: "内存可见性是指一个线程对共享变量的修改对其他线程可见。由于每个线程都有自己的工作内存,线程对变量的修改可能不会立即对其他线程可见,导致可见性问题。保证内存可见性的方法包括:使用 volatile 关键字,保证变量的修改立即对其他线程可见;使用 synchronized 关键字,保证同步块内的变量对其他线程可见;使用 final 关键字,保证 final 变量的初始化对其他线程可见。在实际项目中,我会使用 volatile 保证简单变量的可见性,使用 synchronized 保证复杂操作的可见性。"
深入解析:
- 可见性问题:线程间变量修改不可见
- volatile:保证变量修改的可见性
- synchronized:保证同步块内变量的可见性
- final:保证 final 变量初始化的可见性
并发编程最佳实践
23. 并发编程的最佳实践有哪些?
答案要点:
- 线程安全设计
- 性能优化
- 错误处理
- 调试技巧
示例答案: "并发编程的最佳实践包括:优先使用不可变对象,避免共享可变状态;使用线程安全的集合类,如 ConcurrentHashMap;合理使用锁,避免死锁和性能问题;使用线程池管理线程,避免频繁创建销毁线程;正确处理异常,避免线程异常导致程序崩溃;使用适当的同步机制,如 volatile、synchronized、Lock 等;避免过度同步,只在必要时使用同步;使用并发工具类,如 CountDownLatch、CyclicBarrier 等。在实际项目中,我会遵循这些最佳实践,编写高质量、高性能的并发程序。"
深入解析:
- 线程安全:不可变对象、线程安全集合
- 性能优化:合理使用锁、线程池
- 错误处理:异常处理、资源清理
- 调试技巧:线程转储、监控工具
24. 如何调试并发程序?
答案要点:
- 调试工具
- 线程转储
- 死锁检测
- 性能分析
示例答案: "调试并发程序需要特殊的工具和技巧。调试工具包括 jstack 用于生成线程转储,jconsole 用于监控线程状态,VisualVM 用于性能分析。线程转储可以显示所有线程的状态、锁等待关系、死锁信息等。死锁检测可以通过分析线程转储中的锁等待关系实现。性能分析可以使用 JProfiler、YourKit 等专业工具。在实际项目中,我会使用这些工具诊断并发问题,分析线程状态,检测死锁,优化性能。"
深入解析:
- 调试工具:jstack、jconsole、VisualVM
- 线程转储:分析线程状态和锁关系
- 死锁检测:分析锁等待关系
- 性能分析:使用专业工具分析性能
多线程与并发总结
核心要点回顾
- 线程基础:线程概念、创建方式、生命周期
- 线程同步:synchronized、volatile、死锁
- 锁机制:ReentrantLock、读写锁、条件变量
- 线程池:线程池配置、拒绝策略、监控
- 并发工具:CountDownLatch、CyclicBarrier、Semaphore
- 原子操作:原子类、CAS 操作
- 内存模型:JMM、内存可见性
- 最佳实践:线程安全、性能优化、调试
面试重点
- 深入理解线程同步机制
- 掌握各种锁的使用场景
- 理解线程池的工作原理
- 熟悉并发工具类的应用
- 了解内存模型和可见性
- 掌握并发编程最佳实践
常见陷阱
- 忽略线程安全问题
- 过度使用同步机制
- 死锁和活锁问题
- 内存可见性问题
- 线程池配置不当
性能优化
- 合理选择同步机制
- 使用无锁并发
- 优化线程池配置
- 减少锁竞争
- 使用并发工具类
注:本文档涵盖了 Java 多线程与并发的核心面试题,在实际面试中应结合具体代码示例和实际项目经验进行回答。建议通过实际编程练习加深理解。
