文章目录
Java 多线程是面试中的高频考点,涉及线程基础、并发安全、线程池、锁机制等核心内容。以下是面试中经常出现的多线程问题及核心考察点:
一、线程基础与生命周期
- 线程和进程的区别?
- 进程是资源分配的最小单位(拥有独立内存、CPU 等资源),线程是 CPU 调度的最小单位(共享进程资源)。
- 进程间切换开销大,线程间切换开销小;一个进程可包含多个线程。
- Java 中创建线程的方式有哪些?
- 继承
Thread类(重写run()方法)。- 实现
Runnable接口(重写run()方法,通过Thread包装)。- 实现
Callable接口(重写call()方法,可返回结果,通过FutureTask配合Thread)。- 考察点:
Runnable与Callable的区别(返回值、异常处理)、为什么推荐实现接口而非继承Thread(Java 单继承限制)。
- 线程的生命周期有哪些状态?状态之间如何转换?
- 6 种状态(JDK 定义):
NEW(新建)→RUNNABLE(就绪/运行)→BLOCKED(阻塞,等待锁)→WAITING(无限等待)→TIMED_WAITING(超时等待)→TERMINATED(终止)。- 关键转换:
wait()使线程从RUNNABLE→WAITING(需notify()唤醒);sleep(1000)使线程 →TIMED_WAITING(时间到后自动唤醒);争夺锁失败 →BLOCKED(获取锁后回到RUNNABLE)。
二、并发安全与同步机制
- 什么是线程安全?如何保证线程安全?
- 线程安全:多线程并发访问时,程序行为符合预期(结果正确、无数据混乱)。
- 保证方式:
synchronized关键字、volatile关键字、Lock锁(ReentrantLock)、原子类(AtomicInteger)、线程封闭(ThreadLocal)等。
synchronized的实现原理?与Lock的区别?
- 原理:基于 JVM 内置锁(监视器锁
monitor),通过ACC_SYNCHRONIZED标志(方法)或monitorenter/monitorexit指令(代码块)实现。
- 区别:
| 维度 | `synchronized` | `Lock`(如 `ReentrantLock`) |
|--------------|--------------------------------|--------------------------------------|
| 锁释放 | 自动释放(异常或方法结束) | 需手动 `unlock()`(通常在 `finally` 中) |
| 灵活性 | 不可中断、不可超时 | 支持中断、超时、公平锁 |
| 条件变量 | 仅通过 `wait()`/`notify()` | 支持多条件变量(`Condition`) |
volatile的作用?能保证原子性吗?
- 作用:保证变量的可见性 (一个线程修改后,其他线程立即可见)和禁止指令重排序 (如单例模式的双重检查锁需用
volatile修饰实例)。- 不能保证原子性:例如
i++(读-改-写三步操作),多线程下可能出现数据不一致,需配合synchronized或原子类。
ThreadLocal的原理?可能导致什么问题?
- 原理:每个线程有独立的
ThreadLocalMap,存储线程私有变量(键为ThreadLocal实例,值为变量),实现线程隔离。- 问题:线程池复用线程时,若
ThreadLocal未及时remove(),会导致变量被复用(内存泄漏);底层Entry是弱引用(Key弱引用,Value强引用),可能导致Value无法回收。
三、线程池核心问题
- 线程池的核心参数有哪些?工作原理是什么?
- 核心参数:
corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(非核心线程空闲存活时间)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。- 工作原理:
- 任务提交时,若核心线程未满,创建核心线程执行任务;
- 核心线程满,任务放入队列;
- 队列满,创建非核心线程执行任务;
- 总线程数达最大线程数,触发拒绝策略(如
AbortPolicy抛出异常)。
- 线程池的拒绝策略有哪些?
AbortPolicy(默认):直接抛出RejectedExecutionException。CallerRunsPolicy:让提交任务的线程自己执行(缓解压力)。DiscardPolicy:默默丢弃任务。DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交当前任务。
- 如何合理配置线程池参数?
- CPU 密集型任务(如计算):核心线程数 = CPU 核心数 + 1(减少线程切换开销)。
- IO 密集型任务(如网络请求、数据库操作):核心线程数 = CPU 核心数 × 2(利用 IO 等待时间并行处理)。
- 关键:结合任务类型(CPU/IO 密集)、队列容量(避免过大导致 OOM)、拒绝策略(根据业务容忍度选择)。
四、锁机制与并发工具
- 什么是死锁?如何避免死锁?
- 死锁:两个或多个线程互相持有对方需要的锁,无限等待(如线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1)。
- 避免:固定锁的获取顺序、使用
tryLock(timeout)超时获取锁、使用Lock的lockInterruptibly()响应中断、定期检测死锁(ThreadMXBean)。
- 公平锁与非公平锁的区别?
ReentrantLock默认是哪种?
- 公平锁:线程获取锁的顺序按请求顺序(FIFO),避免饥饿,但性能较低。
- 非公平锁:允许"插队"(刚释放锁的线程可再次获取锁),性能高,但可能导致线程饥饿。
ReentrantLock默认是非公平锁(通过构造函数new ReentrantLock(true)开启公平锁)。
- Java 中的原子类(如
AtomicInteger)如何保证原子性?
- 基于 CAS(Compare And Swap,比较并交换)操作:通过 Unsafe 类的 native 方法,直接操作内存,实现无锁的原子更新(如
compareAndSet(expected, update))。- 问题:ABA 问题(可通过
AtomicStampedReference加版本号解决)、循环时间长导致 CPU 开销大。
CountDownLatch、CyclicBarrier、Semaphore的区别?
CountDownLatch:一个线程等待其他 N 个线程完成(倒计时,不可重置)。CyclicBarrier:N 个线程互相等待,全部到达后一起执行(可重置,支持回调)。Semaphore:控制同时访问资源的线程数(如限制并发连接数)。
五、并发容器
HashMap为什么线程不安全?ConcurrentHashMap如何实现线程安全?
HashMap线程不安全:多线程扩容时可能出现链表环(JDK 7),或put时覆盖数据。ConcurrentHashMap(JDK 1.8):采用"数组 + 链表/红黑树",通过 CAS +synchronized同步链表头节点实现线程安全(粒度比 JDK 7 的分段锁更细,性能更高)。
ArrayList和Vector的区别?CopyOnWriteArrayList适合什么场景?
Vector线程安全(方法加synchronized),但性能低;ArrayList线程不安全。CopyOnWriteArrayList:写操作时复制底层数组(add/set会创建新数组),读操作无锁,适合读多写少场景(如配置缓存),但存在数据一致性延迟和内存开销。
六、实战场景问题
- 如何实现一个生产者-消费者模型?
- 基于
synchronized+wait()/notify():用队列存储任务,生产者put时通知消费者,消费者take时等待(队列空)。- 基于
Lock+Condition:更灵活(生产者和消费者可使用不同Condition唤醒)。- 基于阻塞队列(
ArrayBlockingQueue):简化实现(队列自带阻塞功能)。
- 单例模式的线程安全实现?双重检查锁为什么需要
volatile?
- 线程安全单例:饿汉式(类加载时初始化)、懒汉式(
synchronized方法)、双重检查锁(DCL)、静态内部类。- DCL 中的
volatile:防止指令重排序(实例化对象的步骤:分配内存 → 初始化 → 赋值,重排序可能导致其他线程获取到未初始化的实例)。
- 线程池中的线程抛出异常会怎样?如何处理?
- 若任务通过
execute()提交:异常会直接抛出,线程池会销毁该线程并新建线程。- 若通过
submit()提交:异常会被封装到Future中,需调用get()才能获取异常。- 处理:重写线程池的
afterExecute()方法,或在任务内部try-catch捕获异常。
总结
多线程面试重点考察基础概念理解 (线程状态、锁机制)、并发安全保障 (
synchronized、volatile、原子类)、线程池原理与配置 、实战问题解决能力(死锁、生产者-消费者)。回答时需结合底层原理(如 CAS、监视器锁)和实际场景(如线程池参数配置依据),体现对并发编程的深度理解。