03-并发与多线程

前言

本文件汇总专题「03-并发与多线程」,收录该目录下的所有 Markdown 原文,并提供可点击目录便于跳转查阅。

目录

Java面试题合集-20-线程状态与生命周期.md

Java 线程有哪些状态?生命周期怎么流转?

面试官问线程状态,表面是在考概念,实际是在考你能不能把“线程卡住/线程不跑/线程跑不满”这些线上问题讲明白。

这篇用“状态图 + 最小可运行例子”把它讲清楚。


1)一句话结论

Java 线程在 Thread.State 中有 6 种状态:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED;常见流转由 start、获取锁、wait/join/park、sleep、以及执行结束触发。注意:Java 的 RUNNABLE 包含“正在运行”和“就绪等待 CPU”两种含义。


2)六大状态速记(先把图画出来)

 NEW  --start()-->  RUNNABLE  --run结束-->  TERMINATED
                      |
                      |  竞争锁失败
                      v
                   BLOCKED  --拿到锁--> RUNNABLE

 RUNNABLE --wait()/join()/park()--> WAITING --notify/notifyAll/unpark/被join线程结束--> RUNNABLE
 RUNNABLE --sleep(t)/wait(t)/join(t)/parkNanos--> TIMED_WAITING --时间到/唤醒--> RUNNABLE

3)RUNNABLE 为什么看起来“很笼统”?

在 OS 里常见状态会分:

  • Ready(就绪,等 CPU)
  • Running(正在跑)

但在 Java 的 Thread.State 里,这两个通常都映射成 RUNNABLE
所以你看到线程是 RUNNABLE,不代表它此刻一定在占用 CPU,只能说明:它不在 Java 层面的等待/阻塞状态。


4)用代码把每个状态“抓”出来(可在本机跑)

4.1 NEW / RUNNABLE / TERMINATED

public class StateNewRunnableTerminated {
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {});
        System.out.println(t.getState()); // NEW
        t.start();
        t.join();
        System.out.println(t.getState()); // TERMINATED
    }
}

4.2 TIMED_WAITING:sleep

public class StateTimedWaiting {
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {
            try { Thread.sleep(10_000); } catch (InterruptedException ignored) {}
        });
        t.start();
        Thread.sleep(100);
        System.out.println(t.getState()); // TIMED_WAITING
    }
}

4.3 WAITING:wait / join

public class StateWaiting {
    static final Object lock = new Object();

    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {
            synchronized (lock) {
                try { lock.wait(); } catch (InterruptedException ignored) {}
            }
        });
        t.start();
        Thread.sleep(100);
        System.out.println(t.getState()); // WAITING
    }
}

4.4 BLOCKED:争夺同一把锁

public class StateBlocked {
    static final Object lock = new Object();

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                try { Thread.sleep(10_000); } catch (InterruptedException ignored) {}
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2 got lock");
            }
        });

        t1.start();
        Thread.sleep(100); // 确保 t1 先拿到锁
        t2.start();
        Thread.sleep(100);
        System.out.println(t2.getState()); // BLOCKED
    }
}

5)面试追问:WAITING vs BLOCKED 有啥区别?

你可以这样回答(很清晰):

  • BLOCKED等锁(进入 synchronized 但没抢到监视器)
  • WAITING/TIMED_WAITING不等锁,等“某个条件/事件/时间”(比如 wait/join/park/sleep

6)排查建议:看到状态你应该联想到什么?

  • 大量 BLOCKED:锁竞争严重,检查锁粒度/锁范围/是否热点锁
  • 大量 WAITING:线程可能在等任务/等通知/被 join,检查是否正常设计
  • 大量 TIMED_WAITING:可能在 sleep/超时等待,检查重试策略和时间设置

7)一句话收尾(面试可直接用)

Java 线程有 6 种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED;BLOCKED 是等锁,WAITING/TIMED_WAITING 是等条件/事件/时间;RUNNABLE 在 Java 里同时包含“就绪”和“运行中”。

Java面试题合集-21-synchronized原理与锁升级.md

synchronized 的底层原理是什么?锁升级过程(偏向/轻量/重量)?

你可以把 synchronized 想成“门口的一把锁”:

  • 只有一个人用:就贴个“这是我的”标签(偏向锁)
  • 偶尔两个人抢:用更轻的机制协商(轻量级锁)
  • 很多人抢:干脆上“真正的互斥锁”,排队进门(重量级锁)

1)一句话结论

synchronized 基于对象监视器(monitor)实现互斥与内存可见性;JVM 会根据竞争程度进行锁优化与升级:偏向锁(无竞争快速进入)→ 轻量级锁(CAS 自旋)→ 重量级锁(进入 monitor 阻塞),以在不同竞争场景下平衡性能与开销。


2)synchronized 锁的到底是什么?

锁的是“对象的监视器”:

  • synchronized (obj) {}:锁住 obj 关联的 monitor
  • synchronized 实例方法:锁住 this
  • static synchronized:锁住 Class 对象(例如 Foo.class
class Demo {
    public synchronized void f() {}       // 等价于 synchronized(this)
    public static synchronized void g() {} // 等价于 synchronized(Demo.class)
}

3)为什么 synchronized 同时保证“互斥”和“可见性”?

面试表达可以用这句话:

进入 synchronized 相当于“获取锁”,退出相当于“释放锁”;JMM 规定释放锁前会把工作内存的修改刷新到主内存,获取锁后会使读取到的值保持可见,从而形成 happens-before 关系。

不用背术语也行,抓住:锁释放前写入对之后获得同一把锁的线程可见


4)锁升级:偏向锁 → 轻量级锁 → 重量级锁(讲清“为什么升级”)

4.1 偏向锁(Bias Lock)

适合:大多数时候只有同一线程进入临界区。
做法:在对象头里记录“偏向的线程 ID”,后续同线程进入几乎不用 CAS。

升级触发:出现另一个线程也来抢这把锁(竞争发生),偏向锁会撤销。

4.2 轻量级锁(Lightweight Lock)

适合:短时间竞争,线程很快能拿到锁。
做法:通过 CAS 尝试把对象头替换成指向线程栈中 Lock Record 的指针,并可能自旋等待。

升级触发:竞争激烈/自旋多次仍拿不到 → 升级重量级锁。

4.3 重量级锁(Heavyweight Lock)

适合:竞争激烈或临界区较长。
做法:线程会阻塞挂起,等待唤醒(OS 层面的互斥与调度参与)。

一句话总结升级策略:

能不阻塞就不阻塞:先用“偏向/自旋”赌短临界区,赌输了再阻塞排队。


5)面试追问:偏向锁是不是“必然存在”?

不同 JDK 版本与运行参数会影响偏向锁行为(有的版本默认关闭/逐步弃用方向)。
稳妥说法:

偏向锁是 JVM 的一种优化手段,具体启用与策略受 JDK 版本和参数影响,但锁升级思想依旧成立:根据竞争程度选择不同实现以平衡开销。


6)常见追问:synchronized 和 ReentrantLock 怎么选?

这题下一篇讲更全。这里给一句“不过度承诺”的答法:

synchronized 语义简单、JVM 优化成熟,适合大多数场景;ReentrantLock 提供更丰富能力(可中断、超时、公平锁、多条件队列),需要高级控制时更合适。


7)一句话收尾(面试可直接用)

synchronized 基于 monitor 实现互斥与可见性;JVM 会根据竞争从偏向锁(无竞争快)→ 轻量级锁(CAS 自旋)→ 重量级锁(阻塞等待)逐步升级,在不同场景下兼顾性能与正确性。

Java面试题合集-22-ReentrantLock与synchronized区别.md

ReentrantLocksynchronized 的区别?什么时候选谁?

把这俩比作“门锁”:

  • synchronized:出厂自带的智能门锁,简单耐用,基本够用
  • ReentrantLock:可定制的专业门禁系统,功能多,但你得会配置、会关门

1)一句话结论

synchronized 是 JVM 层面的内置锁,语法简单、自动释放、优化成熟;ReentrantLock 是 JUC 提供的显式锁,支持可中断锁、超时获取、公平锁、多条件队列等高级能力,但需要手动 unlock,更适合需要精细控制的并发场景。


2)最核心区别一览

对比点 synchronized ReentrantLock
获取/释放 进入/退出代码块自动完成 lock()/unlock() 手动控制
异常安全 自动释放(不会忘) 必须 finally 解锁(容易忘)
可中断 不支持(等待锁不可被中断) 支持 lockInterruptibly()
超时 不支持 tryLock(timeout)
公平性 不直接提供公平锁语义 可选公平/非公平
条件队列 只有 wait/notify(monitor 级) Condition(可多个条件队列)
性能优化 JVM 优化成熟(锁升级等) 基于 AQS,性能也很强

3)一个“不会忘记关门”的 ReentrantLock 正确写法

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void incr() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

面试官常抓的坑:忘记 unlock() 会导致其他线程永久卡住。


4)可中断与超时:这就是它“值钱”的地方

4.1 可中断获取锁

lock.lockInterruptibly();

适用:线程等待锁时你希望能响应取消/中断(比如任务被撤销、应用关闭)。

4.2 超时获取锁

if (lock.tryLock(100, java.util.concurrent.TimeUnit.MILLISECONDS)) {
    try {
        // got lock
    } finally {
        lock.unlock();
    }
} else {
    // timeout fallback
}

适用:避免长时间卡死,做降级或快速失败。


5)Condition:一个锁上挂多个“等待室”

synchronizedwait/notify 只有一个隐式条件队列;
ReentrantLock 可以创建多个 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();
}

面试表达:

当你需要“多个条件队列”做精细唤醒时,Condition 比 wait/notify 更清晰。


6)什么时候选谁?(给出清晰选型)

优先 synchronized

  • 临界区简单
  • 不需要超时/可中断/公平性/多条件队列
  • 追求代码简单、异常安全

选择 ReentrantLock

  • 需要 tryLock 超时、可中断获取锁
  • 需要公平锁(避免长时间饥饿)
  • 需要多个 Condition 做更精细的线程协作
  • 需要更灵活的锁结构(比如分段锁思路)

7)一句话收尾(面试可直接用)

synchronized 简单可靠且自动释放,适合大多数场景;ReentrantLock 提供可中断、超时、公平锁与多 Condition 等高级能力,适合需要精细并发控制的场景,但必须 finally 解锁避免死锁。

Java面试题合集-23-volatile能解决什么问题.md

volatile 能解决什么问题?能保证原子性吗?

volatile 经常被误解成“轻量版 synchronized”。
实际上,它更像一个“公告栏”:

我写的东西必须立刻贴出来让大家看到;读的人也必须去公告栏看最新的。

但它不负责“多人同时改同一份账本时谁先谁后”的互斥问题。


1)一句话结论

volatile 主要保证可见性与禁止特定重排序(提供 happens-before 关系),不保证复合操作的原子性;适合做状态标记、停止开关、单例双检锁的关键字段等,但计数自增这类读-改-写必须用锁或原子类。


2)volatile 解决的第一个问题:可见性

没有 volatile 时,一个线程可能把变量缓存在寄存器/工作内存里,另一个线程看不到最新值。

最经典例子:停止线程的开关

public class VolatileStopDemo {
    static volatile boolean running = true;

    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {
            while (running) {
                // busy work
            }
            System.out.println("stopped");
        });
        t.start();

        Thread.sleep(1000);
        running = false;
    }
}

如果 running 不是 volatile,循环可能“停不下来”(取决于 JVM 优化与运行时条件)。


3)volatile 解决的第二个问题:有序性(禁止重排序)

JIT/CPU 可能为了性能对指令重排。volatile 会在读写处插入内存屏障,限制某些重排,从而建立更稳定的先后关系(happens-before)。

面试不需要深入屏障细节,能说清:

volatile 写对之后的 volatile 读可见,并建立 happens-before。

就很加分。


4)volatile 不解决:原子性(最常见坑)

下面这个计数器即使 count 是 volatile,也不安全:

public class VolatileNotAtomic {
    static volatile int count = 0;

    static void incr() {
        count++; // 读-改-写
    }
}

因为 count++ 是三步:
1)读 count
2)+1
3)写回 count

两个线程可能同时读到同一个旧值,最后写回覆盖,导致丢增量。

正确做法 1:AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    static final AtomicInteger count = new AtomicInteger();
    static void incr() { count.incrementAndGet(); }
}

正确做法 2:加锁

public class SyncCounter {
    static int count = 0;
    static synchronized void incr() { count++; }
}

5)volatile 的典型使用场景(说这几个很稳)

  • 状态标记:停止开关、配置开关、是否初始化完成
  • 单例 DCL 的关键字段:volatile 防止指令重排导致“半初始化对象被读到”
  • 读多写少的配置刷新:写线程更新,读线程实时可见

6)一句话收尾(面试可直接用)

volatile 保证可见性与一定的有序性(happens-before),但不保证原子性;适合做状态标记与发布-订阅式的可见性需求,自增计数等复合操作要用锁或原子类。

Java面试题合集-24-CAS与ABA问题.md

CAS 是什么?ABA 问题怎么解决?

CAS 是并发世界的“抢座位”规则:

你想坐下(更新值),必须先确认座位还和你上次看的一样(期望值一致),否则你就得重新排队再看一眼。

它让很多并发操作避免了“先上锁再干活”的重量流程,但也带来一些经典问题,比如 ABA。


1)一句话结论

CAS(Compare-And-Swap)通过比较内存中的当前值与期望值,相等则原子更新为新值,否则失败重试;它是无锁并发的基础(Atomic 类、AQS 等大量使用)。ABA 问题指值从 A 变 B 又回到 A 导致 CAS 误判“没变”,常用版本号(AtomicStampedReference)或标记位(AtomicMarkableReference)解决。


2)CAS 的核心伪代码

boolean cas(addr, expected, newValue) {
    if (*addr == expected) {
        *addr = newValue;  // 原子
        return true;
    }
    return false;
}

Java 里的 AtomicInteger.incrementAndGet() 本质就是 CAS 自旋重试:

while(true){
  old = get()
  new = old + 1
  if (cas(old, new)) return new
}

3)CAS 的优点与代价(面试讲这两点很稳)

优点:

  • 低竞争下性能好(避免线程阻塞与唤醒)
  • 适合短临界区、高并发读写

代价/问题:

  • 高竞争下自旋重试会浪费 CPU
  • 只能保证“一个变量”的原子更新(多变量需要更复杂方案或加锁)
  • ABA 问题

4)什么是 ABA?为什么是问题?

场景:
1)线程 T1 读到值 A,准备 CAS(A → C)
2)线程 T2 把值 A → B → A(中间发生过变化)
3)线程 T1 再 CAS,发现还是 A,于是成功

问题:T1 不知道中间发生过变化,这在某些业务里会产生严重逻辑错误(比如基于指针/节点的无锁结构)。

用图表示:

T1: 读到 A -------------------> CAS 看到还是 A(成功)
T2:        A -> B -> A

5)ABA 怎么解决?——给“值”加一个版本号

5.1 AtomicStampedReference:值 + stamp(版本号)

思路:

  • 每次更新不仅改值,还把 stamp +1
  • CAS 时同时比较值和 stamp,避免“回到 A 但版本变了”被误判

你可以这样描述(面试够用):

用带版本号的 CAS:从 (A,1) 变成 (B,2) 再变成 (A,3),虽然值回到 A,但版本不同,CAS 会失败。

5.2 AtomicMarkableReference:值 + mark(标记位)

适合只需要一个布尔标记的场景(比如逻辑删除)。


6)一句话收尾(面试可直接用)

CAS 通过“比较期望值再原子更新”实现无锁并发,低竞争下高效但高竞争会自旋浪费 CPU;ABA 指值 A→B→A 导致 CAS 误判未变化,常用版本号(AtomicStampedReference)或标记位(AtomicMarkableReference)解决。

Java面试题合集-25-AQS原理.md

AQS 是什么?它在并发工具类中扮演什么角色?

如果把并发工具类比作“各种交通工具”(锁、信号量、倒计时器……),那 AQS 就是它们共用的“发动机底盘”:

你想造一把锁、一把门禁、一套排队系统?AQS 提供了排队、唤醒、状态管理这套通用骨架。


1)一句话结论

AQS(AbstractQueuedSynchronizer)是 JUC 中实现锁与同步器的基础框架,通过一个 state 表示同步状态,并用 CLH 变体队列管理等待线程;ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 等大量组件都基于 AQS 实现。


2)AQS 解决了什么“通用难题”?

实现一个同步器,你总会遇到这些问题:

  • 怎么表示“资源是否可用”?(状态)
  • 多线程抢资源失败后怎么办?(排队)
  • 谁该被唤醒?什么时候唤醒?(阻塞/唤醒)
  • 取消、超时、中断怎么处理?(边界条件)

AQS 把这些“通用部分”封装起来,你只要实现很少的核心方法即可。


3)AQS 的三个核心概念

3.1 state:同步状态(一个 int)

可以理解成“资源剩余量/锁占用次数”:

  • ReentrantLock:state 表示重入次数
  • Semaphore:state 表示剩余许可数
  • CountDownLatch:state 表示剩余计数

3.2 等待队列:抢不到就排队(CLH 变体)

抢失败的线程会被封装成节点进入队列:

Head  ->  Node(T1)  ->  Node(T2)  ->  Node(T3)  ->  Tail

唤醒一般从队头开始,遵循一定的公平/非公平策略。

3.3 acquire/release:获取与释放的统一流程

你可以把 AQS 的 acquire 理解成:

1)先 tryAcquire(自己尝试一次)
2)失败则入队
3)挂起等待(park)
4)被唤醒后再尝试 tryAcquire

release 则会在释放成功后唤醒队列中的下一个合适线程。


4)你需要实现什么?(面试讲到这里很加分)

AQS 的设计是模板方法:

  • 框架负责排队/阻塞/唤醒
  • 子类只负责“业务语义”的获取/释放条件

常见需要覆盖的方法:

  • tryAcquire(int)
  • tryRelease(int)
  • 共享模式下:tryAcquireShared(int)tryReleaseShared(int)

5)独占 vs 共享:AQS 的两种模式

独占(Exclusive)

同一时刻只允许一个线程成功获取:

  • ReentrantLock

共享(Shared)

同一时刻允许多个线程成功获取:

  • Semaphore(许可>1)
  • CountDownLatch(等待计数归零后所有线程通过)
  • ReadWriteLock 的读锁(多读共享)

面试一句话:

独占像“单人洗手间”,共享像“多座位地铁”。


6)面试追问:AQS 为什么用队列?不用忙等行不行?

忙等(自旋)在高竞争下会浪费 CPU。
AQS 的队列 + park/unpark 能让线程“睡着”等唤醒,更适合锁等待时间不确定的场景。


7)一句话收尾(面试可直接用)

AQS 是并发同步器的基础框架:用 state 表示同步状态,用队列管理等待线程,封装了 acquire/release 的排队与唤醒流程;各类锁与同步器通过实现 tryAcquire/tryRelease(或 shared 版本)复用这套通用骨架。

Java面试题合集-26-线程池参数与设置.md

线程池核心参数怎么设置?(core/max/queue/keepAlive)

线程池不是“越大越好”的仓库,它更像一家餐厅:

  • corePoolSize:常驻服务员数量
  • maximumPoolSize:高峰期临时加人的上限
  • workQueue:排队等位区
  • keepAliveTime:临时员工闲着多久就下班

参数设置的本质是:用可控的方式把并发压住,避免线程无限增长把机器拖死。


1)一句话结论

线程池参数要围绕任务类型(CPU 密集/IO 密集)、延迟目标和资源上限来定:核心线程数通常接近 CPU 核心数(CPU 密集)或略大(IO 密集),队列容量用于吸收瞬时流量,最大线程数用于短时峰值兜底,keepAlive 控制临时线程回收;生产环境还必须配置合理的拒绝策略与命名线程工厂。


2)先把 ThreadPoolExecutor 的“决策流程”讲清楚

提交任务时大致规则(很适合面试描述):

1)线程数 < core:创建核心线程执行
2)否则:尝试入队
3)如果队列满 且 线程数 < max:创建非核心线程执行
4)如果队列满 且 线程数 == max:执行拒绝策略

所以参数的本质是:你在控制“先扩线程还是先排队”。


3)CPU 密集 vs IO 密集:核心线程数怎么估?

3.1 CPU 密集(计算为主)

特点:线程大多在跑 CPU,不怎么等待。
建议:

corePoolSize ≈ CPU 核心数 或 CPU 核心数 + 1

理由:过多线程会造成频繁上下文切换,反而变慢。

3.2 IO 密集(等待为主:网络/DB/磁盘)

特点:线程经常阻塞等待 IO。
建议:

corePoolSize 可以显著大于 CPU 核心数(比如 2~4 倍起步,再压测调整)

更“面试范”的表达:

IO 密集时,线程大部分时间在等待,适当增加线程数能提高 CPU 利用率,但上限要受连接数、内存与下游承载能力约束。


4)队列怎么选?(这比“线程数”更关键)

4.1 无界队列(LinkedBlockingQueue 默认无界)

风险:任务堆积会吃光内存,延迟越来越大,还不容易触发扩线程。
适用:非常稳定的负载、明确可控的生产者速度(但生产环境一般不建议“无界”)。

4.2 有界队列(ArrayBlockingQueue / 指定容量的 LinkedBlockingQueue)

优点:能对系统施加背压,防止无穷堆积。
缺点:容量太小会更早触发拒绝。

面试建议:

生产优先用有界队列:让“系统过载时失败得体面”,而不是把机器拖死。

4.3 SynchronousQueue(不存任务,直接移交)

特点:提交任务不排队,要么立刻被线程接手,要么扩线程/拒绝。
适用:希望尽量不排队、低延迟、允许扩线程承压的场景(例如某些短任务)。


5)最大线程数与 keepAlive:峰值兜底与回收策略

  • maximumPoolSize:用于短时峰值,别设成“无限”,要考虑:
    • 每个线程栈内存
    • 上下文切换
    • 下游(DB/Redis)连接数
  • keepAliveTime:控制非核心线程空闲回收速度
  • allowCoreThreadTimeOut(true):让核心线程也可回收(用于波峰波谷明显的场景)

6)线程工厂与命名:排障��备(强烈建议)

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class NamedThreadFactoryDemo {
    public static void main(String[] args) {
        ThreadFactory factory = r -> {
            Thread t = Executors.defaultThreadFactory().newThread(r);
            t.setName("order-pool-" + t.getId());
            return t;
        };
    }
}

有了名字,jstack/日志里一眼就能定位是哪一类任务把线程打满了。


7)一个相对“生产友好”的线程池示例

import java.util.concurrent.*;

public class PoolExample {
    public static ExecutorService pool() {
        int core = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
                core,
                core * 2,
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(500),
                r -> {
                    Thread t = new Thread(r);
                    t.setName("biz-pool-" + t.getId());
                    return t;
                },
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

注意:这是示例模板,最终值必须压测并结合下游承载能力调整。


8)一句话收尾(面试可直接用)

线程池参数的核心是“线程扩容与排队策略”:core 控制常驻并发,queue 吸收突发,max 兜底峰值,keepAlive 回收临时线程;CPU 密集 core≈核数,IO 密集可更大但受下游与内存约束;生产建议有界队列 + 明确拒绝策略 + 命名线程工厂。

Java面试题合集-27-线程池拒绝策略.md

线程池拒绝策略有哪些?生产上如何兜底?

线程池的拒绝策略,本质是系统过载时的“态度”:

任务太多了,我是直接拒绝?丢掉一些?还是让提交者自己干?

选错策略的后果通常不是“报个错”,而是:

  • 请求堆积把系统拖死
  • 下游雪崩
  • 或者悄悄丢任务导致业务账对不上

1)一句话结论

当线程数达到 maximumPoolSize 且队列已满时会触发拒绝;JDK 提供 4 种内置拒绝策略:Abort(抛异常)、CallerRuns(调用者执行)、Discard(直接丢)、DiscardOldest(丢最旧);生产选型要结合“能否丢任务/是否允许降速/是否必须告警”,并配合监控与限流做兜底。


2)什么时候会拒绝?(先把触发条件讲清楚)

触发拒绝需要同时满足:

线程池线程数 == maximumPoolSize
且 workQueue 已满

所以拒绝不是“偶发”,是你参数策略的自然结果。


3)四种内置拒绝策略(面试必背但要会解释)

3.1 AbortPolicy(默认)

  • 行为:抛 RejectedExecutionException
  • 适用:不能默默丢任务,必须让调用方感知失败(例如关键业务)

3.2 CallerRunsPolicy

  • 行为:由提交任务的线程自己执行任务
  • 适用:用“降速”做背压(提交者变慢,流量自然被压住)
  • 注意:如果提交者是 IO 线程(比如 Netty 事件循环),会拖慢整个 IO 处理链

3.3 DiscardPolicy

  • 行为:直接丢弃任务,不抛异常
  • 适用:允许丢弃且对一致性要求低(比如某些非关键采样、低价值日志)

3.4 DiscardOldestPolicy

  • 行为:丢弃队列中最旧的任务,然后尝试重新提交当前任务
  • 适用:更关心“最新任务”(例如某些状态刷新),但要非常小心业务语义

4)生产兜底怎么做?(高质量回答)

4.1 先明确:任务能不能丢?

把任务分三类(面试表达很工程):

  • 绝不能丢:交易、支付、扣库存(必须失败可见或落库重试)
  • 可以降级:推荐、画像、异步刷新
  • 可以丢:低价值统计、非关键日志(但最好可采样、可观测)

4.2 关键业务:拒绝要“可见 + 可重试”

常见做法:

  • AbortPolicy:让调用方失败并告警
  • 或自定义 RejectedExecutionHandler:落库/入 MQ/写本地队列做补偿

自定义策略示例(简化版):

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class LogAndAbortPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.err.println("reject: queue=" + executor.getQueue().size());
        throw new RuntimeException("系统繁忙,请稍后重试");
    }
}

4.3 背压思路:CallerRuns 让上游慢下来

如果你的提交者本身就是业务线程(比如 Controller 线程),CallerRuns 往往能起到“自动限流”的效果。


5)配套三件套:没有它们拒绝策略等于裸奔

1)监控:活跃线程数、队列长度、拒绝次数、任务耗时分位数
2)限流:入口限流/令牌桶/漏桶,提前在入口削峰
3)降级:非核心功能在高峰自动关闭或走缓存兜底


6)一句话收尾(面试可直接用)

线程池拒绝发生在“线程达到 max 且队列满”时;Abort 抛异常、CallerRuns 让提交者执行形成背压、Discard 直接丢、DiscardOldest 丢旧保新。生产选型要围绕业务是否允许丢、是否需要降速和可观测补偿,并配合监控、限流与降级兜底。

Java面试题合集-28-Future与CompletableFuture.md

FutureCompletableFuture 的区别?如何组合异步任务?

把异步任务想象成“点外卖”:

  • Future:你下单后只能反复问“到了没?”(get 阻塞)
  • CompletableFuture:你可以设置“到了给我打电话/顺便帮我拿餐具/两份到了再一起吃”(回调与组合)

1)一句话结论

Future 主要用于获取异步结果,但组合能力弱,常需要阻塞 get()CompletableFutureFuture 基础上提供了丰富的回调与编排能力(thenApply/thenCompose/allOf/anyOf 等),支持非阻塞式组合、异常链路处理与自定义线程池,更适合复杂异步流程。


2)Future 的典型用法(简单但“等得难受”)

import java.util.concurrent.*;

public class FutureDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<Integer> f = pool.submit(() -> {
            Thread.sleep(500);
            return 42;
        });

        System.out.println("do other work");
        System.out.println(f.get()); // 阻塞等待结果
        pool.shutdown();
    }
}

Future 的主要痛点:

  • 结果只能 get(阻塞)
  • 组合多个 Future 往往要自己写等待逻辑
  • 异常处理不够链式

3)CompletableFuture 的核心优势:可组合、可回调、可链式异常处理

3.1 thenApply:对结果做变换

import java.util.concurrent.CompletableFuture;

public class ThenApplyDemo {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> 21)
                .thenApply(x -> x * 2)
                .thenAccept(System.out::println)
                .join();
    }
}

3.2 thenCompose:扁平化串联(避免嵌套 Future)

import java.util.concurrent.CompletableFuture;

public class ThenComposeDemo {
    static CompletableFuture<String> queryUser() {
        return CompletableFuture.supplyAsync(() -> "Tom");
    }

    static CompletableFuture<Integer> queryScore(String user) {
        return CompletableFuture.supplyAsync(() -> user.length() * 10);
    }

    public static void main(String[] args) {
        queryUser()
                .thenCompose(ThenComposeDemo::queryScore)
                .thenAccept(System.out::println)
                .join();
    }
}

串联时优先 thenCompose(“一个任务依赖上一个结果”)。


4)并行组合:allOf / anyOf(面试最常问)

4.1 allOf:都完成再继续

import java.util.concurrent.CompletableFuture;

public class AllOfDemo {
    public static void main(String[] args) {
        CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> "A");
        CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> "B");

        CompletableFuture<Void> all = CompletableFuture.allOf(a, b);
        all.thenRun(() -> System.out.println(a.join() + b.join()))
           .join();
    }
}

4.2 anyOf:谁先完成用谁

import java.util.concurrent.CompletableFuture;

public class AnyOfDemo {
    public static void main(String[] args) {
        CompletableFuture<Object> any = CompletableFuture.anyOf(
                CompletableFuture.supplyAsync(() -> "fast"),
                CompletableFuture.supplyAsync(() -> "slow")
        );
        System.out.println(any.join());
    }
}

5)异常处理:handle / exceptionally(写出来就很加分)

import java.util.concurrent.CompletableFuture;

public class ExceptionallyDemo {
    public static void main(String[] args) {
        int v = CompletableFuture.supplyAsync(() -> 1 / 0)
                .exceptionally(e -> 0)
                .join();
        System.out.println(v);
    }
}

6)线程池:不要默认“全用 commonPool”(生产建议)

supplyAsync 默认用 ForkJoinPool.commonPool()。在生产环境建议为业务异步指定线程池,避免相互影响:

import java.util.concurrent.*;

public class CustomPoolDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(4);
        CompletableFuture.supplyAsync(() -> "x", pool)
                .thenAccept(System.out::println)
                .join();
        pool.shutdown();
    }
}

7)一句话收尾(面试可直接用)

Future 适合“提交任务-阻塞取结果”的简单场景;CompletableFuture 支持链式回调与任务编排(串联 thenCompose、并行 allOf/anyOf、异常 exceptionally/handle),更适合复杂异步流程,且生产环境应尽量指定业务线程池避免共用 commonPool 带来的干扰。

Java面试题合集-29-ThreadLocal原理与内存泄漏.md

ThreadLocal 的原理是什么?为什么会引发内存泄漏?

ThreadLocal 很像“每个线程一个抽屉”:

  • 你把东西放进抽屉(set)
  • 只要还是同一个线程,随时能从抽屉拿出来(get)
  • 别的线程看不到你的抽屉

它很方便(比如存用户信息、traceId),但也很容易踩坑:线程池 + 忘记清理 = 内存泄漏。


1)一句话���论

ThreadLocal 并不是把变量存到 ThreadLocal 对象里,而是存到当前线程的 ThreadLocalMap 中;map 的 key 是 ThreadLocal 的弱引用,value 是强引用。当线程长期存活(线程池)且不 remove,即使 key 被回收,value 仍可能被强引用挂住形成泄漏(更准确说是“value 泄漏/滞留”)。


2)ThreadLocal 的“存放位置”到底在哪?

常见误解:ThreadLocal 里存了值。
真实结构更像这样:

Thread
  └─ ThreadLocalMap
       ├─ Entry(key=ThreadLocal弱引用, value=对象)
       ├─ Entry(...)
       └─ ...

所以:

  • ThreadLocal 只是“钥匙”
  • 真正的“抽屉柜”在 Thread 里

3)为什么 key 用弱引用?为什么还会泄漏?

3.1 key 用弱引用的目的

避免你把 ThreadLocal 变量丢了(没有强引用了)后,key 还能被 GC 回收。

3.2 泄漏发生的关键:value 是强引用 + 线程长期存活

当 key 被 GC 回收后,会出现:

Entry(key=null, value=大对象还在)

如果线程一直不结束(线程池线程会长期复用),这些 value 就会一直挂在这个线程的 map 里,直到:

  • 你显式 remove
  • 或下一次 set/get 触发清理(但不可靠)
  • 或线程结束(线程池里通常很久不会结束)

这就是“ThreadLocal 内存泄漏”的常见来源。


4)最典型的事故现场:线程池复用线程

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalLeakDemo {
    static final ThreadLocal<byte[]> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        for (int i = 0; i < 1000; i++) {
            pool.execute(() -> {
                tl.set(new byte[1024 * 1024]); // 1MB
                // 忘记 remove
            });
        }
        pool.shutdown();
    }
}

这段代码在真实环境里非常容易把内存顶满(或出现 value 长期滞留)。


5)正确姿势:一定要 finally remove(面试必答)

public class ThreadLocalBestPractice {
    static final ThreadLocal<String> tl = new ThreadLocal<>();

    static void handle() {
        tl.set("traceId-xxx");
        try {
            // do work
        } finally {
            tl.remove();
        }
    }
}

面试表达:

ThreadLocal 适合传递线程上下文,但在线程池里必须在 finally 里 remove,避免线程复用导致上下文串线和内存滞留。


6)除了内存泄漏,还有一个更隐蔽的问题:上下文串线

如果你不清理,下一次任务复用同一线程时可能读到上一个请求的用户信息/traceId,产生严重安全问题。

所以 remove 不只是为了内存,更是为了数据隔离。


7)一句话收尾(面试可直接用)

ThreadLocal 的值存在线程的 ThreadLocalMap 中,key 是 ThreadLocal 的弱引用,value 是强引用;线程池线程长期存活时如果不 remove,可能出现 key 被回收但 value 仍滞留导致内存泄漏和上下文串线,所以必须在 finally 中 remove()

Java面试题合集-30-死锁定位与避免.md

如何识别与排查死锁?如何避免死锁?

死锁就像两个人互相礼让:

  • A 抱着“锁1”不放,等“锁2”
  • B 抱着“锁2”不放,等“锁1”

结果就是:两个人都很有礼貌,但事情永远办不完。


1)一句话结论

死锁是多个线程形成环形等待:每个线程都持有部分资源并等待对方释放;排查通常用 jstack/线程 dump 直接定位“Found one Java-level deadlock”,结合业务代码找锁顺序;避免死锁的核心是统一加锁顺序、缩小锁范围、避免嵌套锁和在持锁时做阻塞操作,必要时使用 tryLock 超时失败策略。


2)死锁的四个必要条件(面试官爱问)

经典“��要素”:

  • 互斥:资源一次只能被一个线程占用
  • 占有且等待:持有资源还要再等其他资源
  • 不可剥夺:资源不能被强行抢走
  • 循环等待:形成环

面试表达技巧:你不必死背术语,能讲出“环形等待”就已经抓住核心。


3)最小复现:两把锁 + 反着拿

public class DeadlockDemo {
    static final Object L1 = new Object();
    static final Object L2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (L1) {
                sleep(100);
                synchronized (L2) {
                    System.out.println("t1 done");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (L2) {
                sleep(100);
                synchronized (L1) {
                    System.out.println("t2 done");
                }
            }
        }, "t2");

        t1.start();
        t2.start();
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

这段代码的“坏心思”在于:t1 先拿 L1 后拿 L2,t2 反过来拿。


4)怎么定位死锁?(生产最实用)

4.1 jstack 一把梭

步骤(说给面试官听就够):
1)jps 找到 Java 进程 pid
2)jstack pid > dump.txt
3)在 dump 里搜 deadlockFound one Java-level deadlock

你会看到类似信息:

  • 哪些线程死锁
  • 分别在等待哪把锁、持有哪些锁
  • 以及堆栈位置(精确到类与行号)

4.2 更通俗的排查思路

  • 看到线程大量 BLOCKED:优先怀疑锁竞争或死锁
  • 如果线程长期不变化、互相等待:高度怀疑死锁

5)怎么避免死锁?(给出可执行的工程策略)

5.1 统一加锁顺序(最有效)

只要所有地方都按同一个顺序拿锁,就不会形成环。

例如规定:

永远先拿 L1 再拿 L2

5.2 减少锁嵌套,缩小锁范围

把锁只包住“共享数据的最小操作”,不要把网络/IO/远程调用包在锁里。

5.3 使用 tryLock + 超时(宁可失败也不永久卡死)

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockAvoidDeadlock {
    static final ReentrantLock A = new ReentrantLock();
    static final ReentrantLock B = new ReentrantLock();

    static void work() throws InterruptedException {
        if (A.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (B.tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        // do work
                    } finally {
                        B.unlock();
                    }
                } else {
                    // fallback / retry
                }
            } finally {
                A.unlock();
            }
        }
    }
}

5.4 分层拆锁:用更粗的“单锁”替代多把锁(有时更安全)

当业务允许时,用一把锁保护一组资源,虽然并发度下降,但能显著降低死锁风险。


6)一句话收尾(面试可直接用)

死锁本质是环形等待;排查用 jstack 线程 dump 可直接定位等待关系和代码行;避免死锁的关键是统一锁顺序、减少嵌套锁与持锁时间,避免持锁做阻塞操作,必要时用 tryLock 超时让系统能失败并恢复。

Java面试题合集-31-常用并发容器.md

常用并发容器有哪些?各自适合什么场景?

并发容器就像“专为多人同时操作设计的收纳盒”:

  • 普通容器(ArrayList/HashMap)像“单人桌面”,你硬要多人一起用,就容易翻车
  • 并发容器像“多人协作工位”,规则清晰、冲突可控

1)一句话结论

常用并发容器包括 ConcurrentHashMapConcurrentLinkedQueueCopyOnWriteArrayListBlockingQueue 家族等;选型关键看读写比例、是否需要阻塞协作、是否需要有界背压与是否需要强一致遍历:Map 通常优先 CHM,读多写少 List 用 COW,线程间生产消费用阻塞队列,纯队列化传递可用无锁队列。


2)Map:ConcurrentHashMap(最常用)

适合:

  • 高并发读写 map
  • 需要原子复合操作:computeIfAbsentmergecompute

典型示例:并发计数(比 get+put 安全)

import java.util.concurrent.ConcurrentHashMap;

public class ChmMergeDemo {
    static final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    static void incr(String k) {
        map.merge(k, 1, Integer::sum);
    }
}

注意:CHM 不允许 key/value 为 null。


3)List:CopyOnWriteArrayList(读多写少)

适合:

  • 读远多于写
  • 需要遍历稳定、迭代期间不抛并发修改异常(快照语义)

不适合:

  • 写很频繁(每次写都会复制底层数组)

4)Queue:无锁队列 vs 阻塞队列

4.1 ConcurrentLinkedQueue(非阻塞、无界)

适合:

  • 高并发入队/出队
  • 不需要阻塞等待(拿不到就返回空)

4.2 BlockingQueue(生产者-消费者必选)

常见:

  • ArrayBlockingQueue:有界、数组实现,内存更紧凑
  • LinkedBlockingQueue:链表实现,可配置容量(注意默认可能很大)
  • PriorityBlockingQueue:带优先级(无界),适合任务优先级调度
  • DelayQueue:延迟任务(定时/超时处理)
  • SynchronousQueue:不存储元素,直接移交(适合低延迟、扩线程/拒绝策略配合)

面试表达:

需要“阻塞协作”和“背压控制”就用 BlockingQueue;只需要高性能队列化传递就用 ConcurrentLinkedQueue。


5)Set:ConcurrentSkipListSet / CopyOnWriteArraySet

5.1 ConcurrentSkipListSet

特点:有序、并发安全(基于跳表),操作 O(log n)。
适合:需要有序集合且并发读写。

5.2 CopyOnWriteArraySet

特点:读多写少、快照遍历(内部基于 CopyOnWriteArrayList)。
适合:监听器列表、白名单等低频变更集合。


6)面试加分:并发容器也不是“万能”

常见误区:

  • “用了并发容器就线程安全了”:复合操作仍要用原子 API(如 merge/compute),否则还是竞态
  • “无界队列更稳”:无界队列更容易造成堆积与 OOM,生产一般更推荐有界背压

7)一句话收尾(面试可直接用)

并发容器要按场景选:Map 选 ConcurrentHashMap 并使用 compute/merge 做原子复合操作;读多写少 List 选 CopyOnWriteArrayList;线程间协作与背压用 BlockingQueue(有界优先);高性能无锁队列用 ConcurrentLinkedQueue;需要并发有序集合可用 ConcurrentSkipListSet/Map

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐