前言
Java 并发编程一直以来都是面试中的必考内容,尤其是线程、线程锁与线程池这三座大山,几乎覆盖了初中高级 Java 工程师面试的核心考点。很多同学背了大量八股文,却总是被面试官追问到车轮底下:为什么 volatile 能保证可见性?synchronized 锁升级到底经历了什么?线程池的 7 大参数你真的理解了吗?
本文从线程的底层原理讲起,逐步深入到 synchronized、AQS、ReentrantLock、线程池源码,再配合大量可运行代码和面试追问,力求做到「看完这一篇,面试随便问」。全文结构清晰,建议收藏后配合源码一起阅读。
一、线程基础:从进程到线程
1.1 进程与线程的区别
在操作系统层面,进程是资源分配的最小单位,而线程是 CPU 调度的最小单位。一个进程可以包含多个线程,线程之间共享进程的堆内存和方法区资源,但每个线程拥有自己独立的程序计数器、虚拟机栈和本地方法栈。
通俗理解:进程就像一家工厂,拥有独立的厂房、设备和原材料;线程就像工厂里的工人,多个工人共享同一批设备,但每个工人有自己的工作台和操作手册。工厂之间相互独立,工人之间却需要协作与同步。
为什么多线程比多进程更轻量?因为线程共享进程资源,创建和切换线程时不需要重新分配内存空间,切换开销远小于进程切换。但也正因共享资源,线程之间容易出现数据竞争问题,这就引出了后文的线程安全问题。
1.2 创建线程的三种方式
Java 中创建线程主要有三种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口(配合 FutureTask 使用)。实际开发中我们更推荐使用线程池来管理线程,但理解这三种基础方式仍然是面试必考。
public class ThreadDemo { public static void main(String[] args) throws Exception { // 方式一:继承 Thread Thread t1 = new Thread("thread-1") { @Override public void run() { System.out.println(Thread.currentThread().getName() + " 执行了"); } }; t1.start(); // 方式二:实现 Runnable Thread t2 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 执行了"); }, "thread-2"); t2.start(); // 方式三:实现 Callable,可返回结果、可抛异常 FutureTask<String> task = new FutureTask<>(() -> { Thread.sleep(1000); return "callable 返回值"; }); new Thread(task, "thread-3").start(); System.out.println("获取结果:" + task.get()); } }面试高频追问:start() 和 run() 有什么区别?答案是 start() 会启动一个新线程并调用底层 native 方法,真正实现并发执行;而直接调用 run() 只是普通方法调用,仍在当前线程中顺序执行,不会创建新线程。
1.3 线程的优先级与守护线程
Java 线程优先级范围是 1 到 10,默认是 5。但线程优先级只是一个「建议」,实际调度结果依赖操作系统,不同平台的映射规则也不同,因此不要过于依赖优先级来控制执行顺序。
守护线程(Daemon Thread)是为用户线程服务的「后勤」线程,例如 GC 线程就是典型的守护线程。当 JVM 中只剩下守护线程时,虚拟机就会自动退出。通过setDaemon(true)可以在线程启动前将其设置为守护线程,且该设置必须在 start() 之前完成,否则会抛出 IllegalThreadStateException。
二、线程的生命周期与状态转换
2.1 六种线程状态
Java 通过 Thread.State 枚举定义了六种线程状态,注意这里的「状态」指的是 Java 层面的状态,与操作系统的线程状态不完全一致。六种状态分别为:
- NEW(新建):线程对象已创建但尚未调用 start()。
- RUNNABLE(可运行):调用了 start() 后,线程处于可运行状态,包含操作系统中的就绪和运行两种状态。
- BLOCKED(阻塞):线程尝试获取 synchronized 锁但未成功,进入阻塞状态等待锁释放。
- WAITING(无限等待):线程调用 wait()、join() 或 LockSupport.park() 后,无限期等待其他线程唤醒。
- TIMED_WAITING(限时等待):调用带超时参数的方法如 sleep(long)、wait(long)、join(long) 等,限时等待。
- TERMINATED(终止):线程 run() 方法执行完毕或异常退出,线程生命周期结束。
2.2 状态转换路径
一道经典面试题是画出线程状态转换图。核心转换路径如下:NEW 经过 start() 变为 RUNNABLE;RUNNABLE 中因获取不到 synchronized 锁进入 BLOCKED,拿到锁后又回到 RUNNABLE;RUNNABLE 调用 wait()/sleep()/join() 进入 WAITING 或 TIMED_WAITING,被 notify()/notifyAll()/超时唤醒后重新回到 RUNNABLE;线程执行完毕进入 TERMINATED。
需要特别注意:BLOCKED 是针对 synchronized 锁的等待;而调用 LockSupport.park() 或 ReentrantLock.lock() 进入的是 WAITING,并非 BLOCKED。这是很多同学混淆的点。
三、线程安全问题:原子性、可见性、有序性
3.1 什么是线程安全
线程安全是指当多个线程同时访问某个对象或共享数据时,无论操作系统如何调度这些线程,无论这些线程之间如何交替执行,都能得到正确的结果,那么这个对象就是线程安全的。要理解线程安全,必须理解并发编程中的三大特性:原子性、可见性、有序性。
3.2 原子性
原子性是指一个或多个操作要么全部执行且不被中断,要么全部不执行。经典的反例是i++操作,它实际上分为三步:读取 i 的值、将值加 1、将新值写回主内存。在多线程环境下这三步可能被交叉执行,导致结果错误。
public class AtomicityDemo { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[20]; for (int i = 0; i < 20; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 10000; j++) { count++; // 非原子操作,结果会小于 200000 } }); threads[i].start(); } for (Thread t : threads) { t.join(); } System.out.println("最终结果:" + count); } }解决原子性问题的手段包括 synchronized 加锁、CAS 原子类(如 AtomicInteger)、Lock 锁等,后文会详细展开。
3.3 可见性
可见性是指一个线程对共享变量的修改,能够及时被其他线程看到。由于每个线程都有自己的工作内存(CPU 高速缓存或寄存器),共享变量的修改可能只停留在工作内存中,尚未刷新回主内存,其他线程因而读取到旧值。
public class VisibilityDemo { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (flag) { // 忙等,读取 flag } System.out.println("循环结束"); }).start(); Thread.sleep(1000); flag = false; // 主线程修改 flag,但子线程可能永远看不到 System.out.println("flag 已设置为 false"); } }上面的代码在 JIT 编译器优化下,子线程很可能无限循环,因为编译器认为 flag 在循环体内没有被修改,于是将其缓存在寄存器中。解决可见性问题的关键是 volatile、synchronized 和 Lock,它们都能保证修改及时同步回主内存并让其他线程可见。
3.4 有序性
有序性是指代码执行的顺序。为了提升性能,编译器和处理器会对指令进行重排序,在单线程下重排序不会影响最终结果(遵循 as-if-serial 语义),但在多线程下却可能暴露出问题。最典型的例子就是经典的「双重检查锁」单例模式,如果 instance 不是 volatile 修饰,可能因为指令重排序导致其他线程拿到一个尚未初始化完成的对象。
四、JMM 内存模型与 volatile 深入剖析
4.1 什么是 JMM
JMM(Java Memory Model,Java 内存模型)是一套抽象规范,它定义了多线程程序中变量访问的规则。JMM 规定所有共享变量都存储在主内存中,每个线程有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存,变量在工作内存与主内存之间通过读写操作进行同步。
JMM 通过 happens-before(先行发生)原则来保证可见性。如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。happens-before 规则包括:程序次序规则、监视器锁规则、volatile 变量规则、线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性等。
4.2 volatile 的两大作用
volatile 是 Java 提供的一种轻量级同步机制,它有两个核心作用:保证可见性和禁止指令重排序。需要注意的是,volatile 不能保证原子性,这也是它与 synchronized 最本质的区别。
- 保证可见性:被 volatile 修饰的变量,每次读取都会强制从主内存读取最新值,每次写入都会立即刷新回主内存,并通过缓存一致性协议(如 MESI)通知其他 CPU 缓存失效。
- 禁止指令重排序:volatile 通过内存屏障(Memory Barrier)来禁止特定情况下的指令重排序。写 volatile 变量时会在写后加入 StoreLoad 屏障,读 volatile 变量时会在读后加入 LoadLoad 和 LoadStore 屏障。
4.3 双重检查锁单例中的 volatile
public class Singleton { // volatile 防止指令重排序导致的未初始化对象被返回 private static volatile Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 // 对象创建分为三步:分配内存、初始化对象、引用指向内存 // 若 2、3 步重排序,其他线程可能拿到未初始化的对象 instance = new Singleton(); } } } return instance; } }这里的 volatile 之所以必要,是因为new Singleton()并非一个原子操作,它包含分配内存空间、初始化对象、将引用指向内存地址三步,其中第 2、3 步可能被重排序。如果 instance 没有 volatile 修饰,另一个线程在第一次检查时可能看到非 null 的引用,从而获取到一个尚未初始化完成的对象,导致程序出错。
五、synchronized 深入剖析
5.1 synchronized 的三种使用方式
synchronized 是 Java 内置的互斥锁机制,它的三种用法分别锁定不同的对象:
- 修饰实例方法:锁是当前实例对象 this。
- 修饰静态方法:锁是当前类的 Class 对象。
- 修饰代码块:锁是括号中指定的对象,可以是任意对象。
public class SynchronizedDemo { private final Object lock = new Object(); private static int staticCount = 0; private int count = 0; // 锁是 this public synchronized void addInstance() { count++; } // 锁是 SynchronizedDemo.class public static synchronized void addStatic() { staticCount++; } // 锁是 lock 对象 public void addBlock() { synchronized (lock) { count++; } } }5.2 对象头与 Mark Word
要理解 synchronized 的锁升级,必须先了解对象头。在 HotSpot 虚拟机中,对象在内存中的布局分为三部分:对象头、实例数据和对齐填充。对象头又包含 Mark Word 和类型指针(开启压缩指针后为 4 字节)。
Mark Word 中存储了对象的 hashCode、分代年龄、锁标志位、偏向线程 ID 等信息。根据锁状态的不同,Mark Word 的存储内容会动态变化,这正是 synchronized 无锁状态下不引入额外内存开销的原因。
5.3 锁升级过程:无锁、偏向锁、轻量级锁、重量级锁
JDK 6 之后,synchronized 进行了大量优化,引入了「偏向锁、轻量级锁、重量级锁」的锁升级机制。升级过程单向不可逆:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
- 无锁:对象刚创建时没有锁竞争,处于无锁状态。
- 偏向锁:当第一个线程访问同步块时,Mark Word 记录该线程 ID,表示偏向该线程。后续该线程再次进入同步块无需任何同步操作,只需简单判断即可。偏向锁适用于「只有一个线程反复获取锁」的场景。
- 轻量级锁:当出现第二个线程竞争锁但竞争不激烈(没有真正阻塞)时,偏向锁升级为轻量级锁。轻量级锁通过 CAS 原子操作在线程栈帧中复制 Mark Word 并尝试竞争,失败则自旋等待,避免线程挂起和唤醒的开销。
- 重量级锁:当自旋超过一定次数或竞争线程过多时,轻量级锁升级为重量级锁。重量级锁依赖操作系统的互斥量(Mutex),会导致线程从用户态切换到内核态,开销较大,但能保证高竞争下的公平性和稳定性。
面试官常追问:为什么要引入锁升级?答案是为了在「低竞争」场景下用更轻量、更低开销的方式实现同步,避免所有场景都走重量级锁导致的性能浪费。同时 JDK 15 之后逐步废弃偏向锁(JEP 374),因为现代 JVM 中偏向锁带来的收益已经不明显,反而增加了维护成本。
5.4 synchronized 与 Lock 的区别
- synchronized 是 Java 关键字,由 JVM 实现;Lock 是接口,由 JDK 层面的代码实现(核心是 AQS)。
- synchronized 会自动释放锁,即使发生异常也会释放;Lock 必须在 finally 中手动调用 unlock() 释放。
- synchronized 不可中断、不可超时、不可尝试获取;Lock 支持中断、超时、非阻塞尝试获取。
- synchronized 不支持公平锁;Lock(ReentrantLock)支持公平锁和非公平锁。
- synchronized 只能绑定一个条件队列;Lock 配合 Condition 可以使用多个条件队列。
六、CAS 与原子类
6.1 什么是 CAS
CAS(Compare And Swap,比较并交换)是一种无锁算法。它包含三个操作数:内存位置 V、预期原值 A 和新值 B。当且仅当 V 的值等于 A 时,才将 V 更新为 B,否则不做任何操作并返回失败。CAS 是原子操作,由 CPU 通过 cmpxchg 指令保证,期间不会被中断。
import java.util.concurrent.atomic.AtomicInteger; public class CASDemo { public static void main(String[] args) { AtomicInteger atomic = new AtomicInteger(0); // compareAndSet(期望值, 新值) boolean success = atomic.compareAndSet(0, 10); System.out.println("更新结果:" + success + ",当前值:" + atomic.get()); // incrementAndGet 内部就是 CAS 自旋 System.out.println("自增后:" + atomic.incrementAndGet()); } }6.2 CAS 的三大问题
- ABA 问题:一个值从 A 变成 B 又变成 A,CAS 无法察觉这种变化,误以为没有变化。解决办法是使用版本号,Java 提供了 AtomicStampedReference 和 AtomicMarkableReference 来解决。AtomicStampedReference 通过「引用 + 版本号」双重比较避免 ABA 问题。
- 自旋开销问题:CAS 失败时会不断循环重试,如果并发竞争激烈,线程会一直空转占用 CPU,造成性能浪费。JDK 8 的 LongAdder 通过将热点数据分散到多个 Cell 中分段累加,能够有效降低单点竞争,比 AtomicLong 更适合高并发统计场景。
- 只能保证一个共享变量的原子操作:CAS 一次只能保证一个变量的原子更新。当需要同时原子地更新多个变量时,可以把这些变量封装成一个对象,再使用 AtomicReference 进行 CAS;或者改用 synchronized、Lock 来保证复合操作的原子性。
七、AQS 深入剖析
7.1 什么是 AQS
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 JUC 包中锁和同步器的基石,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等工具类的底层都依赖它。AQS 提供了一套模板方法,把「获取锁、释放锁、线程排队、线程唤醒」等复杂逻辑封装好,子类只需要实现少量核心方法,就能快速实现自己的同步器。
为什么 AQS 如此重要?因为并发工具类最困难的部分并不是「共享资源如何保护」,而是「竞争失败后如何安全地排队等待」以及「释放后如何高效地唤醒下一个线程」。AQS 把这两件事统一解决后,开发者就可以专注于业务层面的同步语义,这也是它被称为 JUC 灵魂组件的原因。
7.2 AQS 的两个核心:state 与 CLH 队列
AQS 内部有一个 int 类型的state变量,用 volatile 修饰,用来表示同步状态。不同的同步器对 state 赋予了不同含义:ReentrantLock 用它表示重入次数(0 表示无锁,1 表示第一次加锁,2 表示重入一次);Semaphore 用它表示剩余许可数量;CountDownLatch 用它表示还需要等待多少个任务完成。
当线程竞争锁失败时,AQS 会把该线程封装成一个 Node 节点,放入内部维护的 FIFO 双向等待队列中。这个队列参考了 CLH 锁队列的设计,所以常被称为 CLH 队列。每个 Node 状态包括 CANCELLED、SIGNAL、CONDITION、PROPAGATE 等,其中 SIGNAL 表示当前节点释放锁后需要唤醒它的后继节点。
获取锁的大致流程是:先通过 CAS 尝试修改 state;如果成功则直接获取锁;如果失败,则把当前线程入队,判断前驱节点是否为头节点,若是则再次尝试获取,否则使用 LockSupport.park() 挂起线程。释放锁时,state 归零后唤醒头节点的后继节点,让它重新参与竞争。整个过程通过 CAS 和 park/unpark 实现,没有直接调用操作系统重量级互斥量。
7.3 独占模式与共享模式
AQS 同时支持两种资源共享模式:
- 独占模式(Exclusive):同一时刻只允许一个线程获取锁,例如 ReentrantLock。独占模式只需要实现 tryAcquire、tryRelease。
- 共享模式(Shared):同一时刻允许多个线程同时获取锁,例如 Semaphore、CountDownLatch、ReentrantReadWriteLock 的读锁。共享模式需要实现 tryAcquireShared、tryReleaseShared。
面试中经常追问 ReentrantReadWriteLock 为什么读锁能共享、写锁必须独占。这是因为读操作之间不会互相影响,允许多个线程同时读可以显著提升并发性能;而写操作必须保证互斥,否则会破坏数据一致性。读写锁内部正是通过共享模式实现读锁、独占模式实现写锁。
八、ReentrantLock 深入剖析
8.1 基本使用与可重入特性
ReentrantLock 是 AQS 独占模式最经典的实现,它的名字就表达了核心特性:可重入。同一个线程在持有锁的情况下可以再次获取同一把锁,而不会发生死锁。与之相比,synchronized 同样支持可重入,但 ReentrantLock 在这个基础上还提供了更多灵活能力。
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { lock.lock(); try { System.out.println("第一次获取锁"); method(); } finally { lock.unlock(); } } public static void method() { lock.lock(); try { System.out.println("重入获取同一把锁"); } finally { lock.unlock(); } } }lock()与unlock()必须成对出现。通常建议把unlock()放在 finally 中,确保即使业务逻辑抛出异常也能正常释放锁,否则一旦忘记释放,其他线程将永久阻塞。
8.2 公平锁与非公平锁
ReentrantLock 构造方法中可以传入 boolean 参数指定公平性:new ReentrantLock(true)表示公平锁,new ReentrantLock(false)或默认构造表示非公平锁。这正是 synchronized 不具备的能力。
- 公平锁:线程严格按照等待队列的先后顺序获取锁,先来的线程一定先拿到锁,不会出现插队现象,但上下文切换更频繁,吞吐量相对较低。
- 非公平锁:新来的线程可以直接尝试抢占锁,如果抢占成功就无需排队,从而减少线程切换,吞吐量更高,但可能导致某些线程长时间等待,产生「饥饿」。
源码层面,非公平锁在lock()时先执行一次 CAS 抢占,抢到就直接返回;公平锁则先判断队列中是否已有等待线程,有的话必须乖乖排队。这是二者最核心的区别。
8.3 Condition 实现精确等待唤醒
synchronized 只能配合 wait/notify 使用一个隐式条件队列,而 ReentrantLock 可以通过newCondition()创建多个 Condition 对象,实现更加精细的线程协作。例如生产者消费者模型中,可以使用两个条件队列分别管理「缓冲区非空」和「缓冲区非满」,让生产者和消费者只唤醒真正需要的线程,减少无效唤醒。
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ConditionDemo { private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition(); private final int[] buffer = new int[10]; private int count = 0; public void put(int value) throws InterruptedException { lock.lock(); try { while (count == buffer.length) { notFull.await(); } buffer[count++] = value; System.out.println("生产:" + value); notEmpty.signal(); } finally { lock.unlock(); } } public int take() throws InterruptedException { lock.lock(); try { while (count == 0) { notEmpty.await(); } int value = buffer[--count]; System.out.println("消费:" + value); notFull.signal(); return value; } finally { lock.unlock(); } } }使用 Condition 时有一个重要细节:await()内部会释放锁并挂起当前线程,被signal()唤醒后会重新参与锁竞争;因此等待条件必须写在 while 循环中,而不能用 if,避免醒来后条件已被其他线程改变的情况。
九、线程池深入剖析
9.1 为什么要使用线程池
如果每次任务执行都手动new Thread(),频繁创建和销毁线程会消耗大量系统资源,尤其在高并发场景下,大量线程会占用内存和 CPU,甚至导致系统崩溃。线程池通过「复用线程、统一管理、控制并发度」解决了这些问题,主要优势包括:
- 降低资源消耗:线程复用,避免频繁创建和销毁带来的开销。
- 提高响应速度:任务到达时可以立即使用空闲线程执行,不需要等待线程创建。
- 便于统一管理:可以统一控制最大并发数、队列策略、拒绝策略和线程存活时间。
9.2 线程池的七大参数
ThreadPoolExecutor 的构造方法包含七个参数,这是面试中几乎必问的题目:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // ... }- corePoolSize:核心线程数。线程池会一直保留这些线程,即使它们暂时空闲,除非设置了 allowCoreThreadTimeOut。
- maximumPoolSize:最大线程数。当核心线程繁忙且队列已满时,线程池会创建新线程,直到达到该上限。
- keepAliveTime:非核心线程空闲存活时间。超过该时间仍未接到任务,非核心线程会被回收。
- unit:keepAliveTime 的时间单位,如 TimeUnit.SECONDS。
- workQueue:任务等待队列。常用的有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。
- threadFactory:线程工厂,用于创建线程,通常用它统一设置线程名称、是否守护线程等。
- handler:拒绝策略。当线程数达到最大且队列已满时,如何处理新提交的任务。
9.3 线程池执行流程
当向线程池提交任务时,执行流程严格按以下顺序判断:
- 先判断核心线程:如果当前线程数小于 corePoolSize,直接创建新线程执行,即使已有空闲线程。
- 再判断等待队列:如果核心线程已满,任务会被放入 workQueue 等待空闲线程处理。
- 最后判断最大线程:如果队列也满了,且当前线程数小于 maximumPoolSize,则创建非核心线程执行新任务。
- 触发拒绝策略:如果线程数已达到 maximumPoolSize 且队列还是满的,则根据 RejectedExecutionHandler 处理任务。
这个流程可以概括为「核心线程优先创建,队列优先缓存,队列满了再扩容,扩容不了才拒绝」。面试官经常结合具体队列类型追问:SynchronousQueue 容量为 0,任务不会被实际存储,而是直接交给线程执行,因此它会更快地触发扩容和拒绝策略。
9.4 四种拒绝策略
- AbortPolicy:默认策略,直接抛出 RejectedExecutionException,提醒调用方任务被拒绝。
- CallerRunsPolicy:由提交任务的调用线程直接执行该任务,起到「减速」和缓冲作用。
- DiscardPolicy:直接丢弃新任务,不抛异常。
- DiscardOldestPolicy:丢弃等待队列中最老的任务,然后重新尝试提交当前任务。
实际生产环境中,直接丢弃任务风险较高,通常建议使用 CallerRunsPolicy 或自定义拒绝策略,至少把被拒绝的任务记录下来,方便后续排查和补偿。
十、面试高频追问与总结
10.1 常见面试连环追问
- volatile 能保证原子性吗?不能。它只能保证可见性和禁止指令重排序,适合单线程写、多线程读的场景;复合操作仍需 synchronized、Lock 或原子类。
- synchronized 的锁升级过程是怎样的?无锁 → 偏向锁 → 轻量级锁 → 重量级锁,升级路径单向不可逆;JDK 15 之后偏向锁被逐步废弃。
- AQS 的 state 有什么用?state 是同步状态标记,通过 CAS 修改;不同同步器对其赋予不同语义,如锁重入次数、信号量许可数等。
- ReentrantLock 比 synchronized 强在哪里?支持公平锁、可中断获取、超时获取、tryLock 尝试获取、多条件队列等;但 synchronized 使用更简单且会自动释放锁。
- 线程池的 7 大参数是什么?核心线程数、最大线程数、非核心线程存活时间、时间单位、任务队列、线程工厂、拒绝策略。
- 如何合理设置线程池参数?CPU 密集型任务建议核心线程数约等于 CPU 核数 + 1;IO 密集型任务可以适当增大核心线程数,并结合实际压测不断调整。
10.2 总结
线程、线程锁与线程池是 Java 并发编程的核心主线:理解线程的生命周期和三大特性,是分析并发问题的基础;掌握 JMM、volatile 和 synchronized,能解释清楚「为什么会有线程安全问题」以及「如何保证可见性和有序性」;深入 AQS 和 ReentrantLock,才能真正理解 JUC 锁框架的设计思想;而线程池则是把这些原理落地到工程实践的最佳载体。
并发编程没有捷径,建议读者把本文中的代码逐段运行、逐段验证,再结合源码和真实业务场景分析。面试时不仅要能背出结论,更要能把「为什么这么设计、这样设计解决了什么问题」讲清楚,这样才能真正做到「面试随便问」。