1. 线程与进程的基本概念
进程是操作系统进行资源分配和调度的基本单位,拥有独立的地址空间;线程是进程内的执行单元,共享进程的堆和方法区,但每个线程拥有独立的虚拟机栈和程序计数器。
Java 中创建线程主要有三种方式:
- 继承
Thread类,重写run()方法。 - 实现
Runnable接口,实现run()方法。 - 实现
Callable接口,配合FutureTask获取返回值。
推荐使用Runnable或Callable,因为 Java 是单继承,实现接口更灵活,且便于解耦任务与执行。
2. 线程的生命周期与状态转换
Java 线程共有六种状态:
- NEW:创建后尚未启动。
- RUNNABLE:就绪或运行中,等待 CPU 调度。
- BLOCKED:等待获取监视器锁,进入同步块/方法时阻塞。
- WAITING:调用
wait()、join()或LockSupport.park()后无限期等待。 - TIMED_WAITING:调用
sleep()、带超时的wait()或join()后有限期等待。 - TERMINATED:执行完毕或异常退出。
高频考点:sleep()不释放锁,wait()释放锁;wait()必须在同步代码块中调用,否则抛出IllegalMonitorStateException。
3. 并发编程的三大特性
原子性:一个操作或多个操作要么全部执行且不被中断,要么全部不执行。Java 通过synchronized、Lock、AtomicInteger等保证原子性。
可见性:一个线程修改共享变量后,其他线程能立即看到修改。Java 通过volatile、synchronized、final和Lock保证可见性。
有序性:程序执行顺序按代码顺序进行。编译器和 CPU 可能重排序,volatile通过内存屏障禁止指令重排,synchronized通过锁保证临界区内的有序性。
4. volatile 关键字详解
volatile是轻量级同步机制,保证可见性和有序性,但不保证原子性。
适用场景:
- 状态标志位:如
volatile boolean running = true,用于控制线程停止。 - 双重检查锁单例模式中,
instance必须声明为volatile,防止指令重排导致返回未初始化对象。
典型面试题:volatile能否保证原子性?答案是不能。例如count++是读-改-写三步操作,volatile无法保证整体原子性,需使用AtomicInteger或加锁。
5. synchronized 关键字与锁升级
synchronized是 JVM 内置的互斥锁,可修饰实例方法、静态方法和代码块。锁的对象分别是当前实例、Class 对象和指定对象。
JDK 1.6 引入锁升级机制,锁状态从低到高为:
- 无锁:无竞争状态。
- 偏向锁:只有一个线程访问,记录线程 ID,避免 CAS 开销。
- 轻量级锁:多个线程交替访问,通过 CAS 自旋获取锁。
- 重量级锁:竞争激烈,线程阻塞并唤醒,涉及操作系统互斥量。
锁只能升级不能降级,目的是减少锁竞争带来的性能开销。
6. Lock 接口与 ReentrantLock
ReentrantLock是Lock接口的主要实现,与synchronized相比具备以下优势:
- 可中断:通过
lockInterruptibly()响应中断。 - 可超时:通过
tryLock(timeout, unit)避免死锁。 - 公平锁:构造参数传入
true时按 FIFO 顺序获取锁。 - 多个条件变量:通过
newCondition()实现精准唤醒。
使用Lock时必须在finally块中调用unlock(),否则锁无法释放。
7. 线程池的核心参数与执行流程
线程池通过ThreadPoolExecutor创建,核心参数包括:
- corePoolSize:核心线程数,常驻线程。
- maximumPoolSize:最大线程数。
- keepAliveTime:非核心线程空闲存活时间。
- workQueue:任务阻塞队列。
- threadFactory:线程工厂,用于设置线程名和优先级。
- handler:拒绝策略。
执行流程:提交任务后,先判断核心线程是否已满;未满则创建核心线程执行,已满则放入阻塞队列;队列满则创建非核心线程执行;线程数达到最大值则执行拒绝策略。
四种拒绝策略:
AbortPolicy:直接抛出RejectedExecutionException。CallerRunsPolicy:由提交任务的线程执行。DiscardPolicy:直接丢弃任务。DiscardOldestPolicy:丢弃队列中最旧的任务。
生产环境推荐使用ThreadPoolExecutor手动创建,避免Executors工具类带来的隐患,如无界队列导致 OOM。
8. 线程池的常见问题与调优
高频面试题:如何合理设置线程池大小?
- CPU 密集型:设置为 CPU 核数 + 1,减少上下文切换。
- IO 密集型:设置为 CPU 核数 × 2,或使用公式:CPU 核数 /(1 - 阻塞系数),阻塞系数通常取 0.8 到 0.9。
如何监控线程池?可以通过getPoolSize()、getActiveCount()、getQueue().size()等方法定期采集指标,并结合日志和告警系统分析。
9. 死锁的产生条件与排查方法
死锁产生的四个必要条件:
- 互斥:资源只能被一个线程占用。
- 持有并等待:线程持有资源的同时等待其他资源。
- 不可剥夺:资源不能被强制抢占。
- 循环等待:多个线程形成环形等待链。
避免死锁的方法:
- 按固定顺序获取锁,破坏循环等待。
- 使用
tryLock加超时,避免无限等待。 - 尽量缩小同步代码块范围,减少锁持有时间。
排查方法:使用jstack导出线程快照,搜索 "deadlock" 关键字定位死锁线程和锁对象。
10. ThreadLocal 的原理与内存泄漏
ThreadLocal为每个线程提供独立的变量副本,实现线程隔离。每个线程内部持有ThreadLocalMap,键为ThreadLocal的弱引用,值为变量副本。
内存泄漏原因:ThreadLocalMap的键是弱引用,但值是强引用。当ThreadLocal被回收后,键变为null,但值仍被线程持有,无法被回收。
解决方案:使用完ThreadLocal后调用remove()方法清除;在线程池场景中,线程复用导致变量残留,必须在任务结束后清理。
11. CAS 与原子类
CAS(Compare And Swap)是乐观锁的核心实现,包含三个操作数:内存地址 V、期望值 A、新值 B。仅当 V 的值等于 A 时,才将 V 更新为 B,否则重试。
CAS 的三大问题:
- ABA 问题:变量被修改后又恢复原值,CAS 无法感知。可通过
AtomicStampedReference加版本号解决。 - 自旋开销:竞争激烈时 CPU 空转,可使用
LongAdder分段累加降低竞争。 - 只能保证一个变量的原子性:多个变量需使用锁或
AtomicReference封装。
常用原子类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference,底层通过Unsafe类的compareAndSwapInt实现。
12. AQS 原理与 ReentrantLock 实现
AQS(AbstractQueuedSynchronizer)是 JUC 锁和同步器的基石,内部维护一个volatile int state和一个 FIFO 双向队列。
核心原理:
- 获取锁时,通过 CAS 将
state从 0 改为 1,成功则获得锁。 - 获取失败时,线程封装为 Node 加入同步队列尾部,通过
LockSupport.park()阻塞。 - 释放锁时,将
state改回 0,唤醒队列头部的后继节点。
ReentrantLock的可重入性通过state累加实现:同一线程重复获取锁时state加 1,释放时减 1,减到 0 才真正释放锁。
13. CountDownLatch、CyclicBarrier 与 Semaphore
CountDownLatch:计数器减到 0 时放行等待线程,用于一个线程等待多个线程完成。计数器不可复用。
CyclicBarrier:多个线程互相等待,全部到达屏障后同时放行,可循环复用。支持可选的屏障动作。
Semaphore:信号量,控制同时访问资源的线程数量,可用于限流。通过acquire()获取许可,release()释放许可。
区别总结:CountDownLatch是等待事件发生,CyclicBarrier是线程间互相等待,Semaphore是控制并发数量。
14. 并发容器与集合
高频考点:ConcurrentHashMap的实现原理。
JDK 1.7 使用分段锁,将数据分为多个 Segment,每个 Segment 独立加锁,提高并发度。JDK 1.8 改为 CAS +synchronized,锁粒度细化到单个桶,且引入红黑树优化链表过长时的查询性能。
其他并发容器:
CopyOnWriteArrayList:写时复制,读操作无锁,适合读多写少场景。BlockingQueue:阻塞队列,支持生产者-消费者模式,常用实现有ArrayBlockingQueue、LinkedBlockingQueue。ConcurrentLinkedQueue:基于 CAS 的无界非阻塞队列。
15. 线程通信与协作
经典生产者-消费者模型可通过以下方式实现:
synchronized+wait()/notifyAll()。Lock+Condition,支持精准唤醒。BlockingQueue,内部自动处理阻塞和唤醒。
高频面试题:两个线程交替打印 1 到 100。可使用synchronized+wait()/notify(),或ReentrantLock+ 两个Condition实现。
16. 常见多线程面试题汇总
以下为高频面试题及简要思路:
- 线程和进程的区别:进程是资源分配单位,线程是 CPU 调度单位;进程间相互独立,线程共享进程资源。
- 为什么使用线程池:降低资源消耗、提高响应速度、便于统一管理。
- 如何停止一个线程:使用
volatile标志位配合interrupt(),不要使用已废弃的stop()。 - 什么是上下文切换:CPU 从当前线程切换到另一个线程时保存和恢复现场的开销。
- 什么是活锁和饥饿:活锁是线程不断重试但无法推进;饥饿是低优先级线程长期得不到 CPU 时间。
- 什么是伪共享:多个变量位于同一缓存行,一个变量修改导致其他变量缓存失效,可通过
@Contended或填充字节解决。
17. 总结与复习建议
多线程是 Java 面试的高频考点,建议按以下路径复习:
- 先掌握线程基础:创建方式、生命周期、三大特性。
- 再深入锁机制:
synchronized、volatile、Lock、AQS。 - 最后掌握并发工具:线程池、并发容器、原子类、同步器。
面试中不仅要会背概念,还要能结合代码示例说明原理,并主动补充性能对比和适用场景,体现深度思考。