Skip to content

二、Java 并发编程(22 题)

2.1 ★★创建线程有哪几种方式?RunnableCallable 有什么区别?实际开发中推荐使用哪种方式?

📖 点击查看答案

创建线程的方式:

  1. 继承 Thread 类:重写 run()
  2. 实现 Runnable 接口:传给 Thread 构造器
  3. 实现 Callable 接口:配合 FutureTask 获取返回值
  4. 线程池ExecutorService.submit() / execute()
  5. CompletableFutureCompletableFuture.supplyAsync()
java
// 方式1:继承 Thread
class MyThread extends Thread {
    public void run() { System.out.println("run"); }
}
new MyThread().start();

// 方式2:Runnable
new Thread(() -> System.out.println("run")).start();

// 方式3:Callable + FutureTask
FutureTask<String> ft = new FutureTask<>(() -> "result");
new Thread(ft).start();
String result = ft.get();

// 方式4:线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<String> f = pool.submit(() -> "result");

Runnable vs Callable:

维度RunnableCallable
方法run()call()
返回值
异常不能抛出受检异常可以抛出受检异常
引入版本JDK 1.0JDK 1.5

实际推荐:线程池

  • 优点:复用线程、控制并发数、统一管理、避免频繁创建销毁
  • 阿里规约:禁止直接 new Thread / 用 Executors,必须用 ThreadPoolExecutor 显式创建
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    8, 16, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("biz-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
Future<String> f = executor.submit(() -> doSomething());
  • 异步编排用 CompletableFuture.supplyAsync(task, executor)

2.2 ★★★★线程的生命周期(状态)有哪些?各状态之间如何转换?BLOCKEDWAITING 状态有什么区别?

📖 点击查看答案

Java 线程的 6 种状态(Thread.State 枚举):

NEW → RUNNABLE → (TERMINATED)
            ↑↓
        BLOCKED

        WAITING / TIMED_WAITING
状态说明
NEW已创建但未调用 start()
RUNNABLE已调用 start(),可能正在执行或等待 CPU 调度
BLOCKED等待获取 synchronized 锁
WAITING调用了无超时的 wait()/join()/LockSupport.park()
TIMED_WAITING调用了带超时的 sleep(t)/wait(t)/join(t)
TERMINATEDrun() 执行完毕

状态转换:

  • NEW → RUNNABLE:调用 start()
  • RUNNABLE → BLOCKED:进入 synchronized 块但锁被占用
  • RUNNABLE → WAITING:调用 wait() / join() / LockSupport.park()
  • RUNNABLE → TIMED_WAITINGsleep(ms) / wait(ms) / join(ms) / parkNanos
  • BLOCKED/WAITING/TIMED_WAITING → RUNNABLE:被唤醒或获得锁
  • RUNNABLE → TERMINATED:run() 返回

BLOCKED vs WAITING 区别:

维度BLOCKEDWAITING
触发等待 synchronized 锁wait/join/LockSupport.park
唤醒锁释放后自动竞争需要被 notify/notifyAll/unpark
是否持有锁不持有(在等)之前调用 wait() 时已释放锁
何时进入synchronized 入口处调用 wait() 等方法后

重要细节:

  • Java 层没有"运行中"和"就绪"的区分,统一叫 RUNNABLE(因为取决于操作系统调度)
  • 调用 Thread.sleep() 不会释放任何锁
  • 调用 wait() 必须先持有该对象的锁,且会释放锁
  • LockSupport.park() 会阻塞线程但不释放锁(与 wait 不同),用于 AQS 等场景
  • I/O 阻塞(如 socket read)在 Java 层也是 RUNNABLE(操作系统层面是阻塞)

Blocked 场景示例:

java
synchronized (lock) {
    // 线程 A 持有 lock
    // 线程 B 尝试 synchronized(lock) → 进入 BLOCKED
}

Waiting 场景示例:

java
synchronized (lock) {
    lock.wait(); // 释放 lock,进入 WAITING
    // 被 notify 后重新竞争锁,竞争失败进入 BLOCKED
}

2.3 ★★线程中断机制的原理是什么?interrupt()interrupted()isInterrupted() 三个方法的区别是什么?

📖 点击查看答案

中断机制原理:

  • Java 中断是协作式的:interrupt() 只是设置中断标志,不强制停止线程
  • 被中断的线程自己决定如何响应(退出、抛异常、忽略)
  • 设计目的:避免强制停止导致数据不一致、资源泄漏

三个方法对比:

方法所属作用是否清除标志
interrupt()Thread 实例设置中断标志否(设置标志)
isInterrupted()Thread 实例查询中断状态
interrupted()Thread 静态查询当前线程中断状态(清除)
java
Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 执行任务
    }
    // 退出循环,做清理
});
t.start();
t.interrupt(); // 请求中断

特殊情况:阻塞方法响应中断

  • Thread.sleep()Object.wait()Thread.join()LockSupport.park() 等会抛 InterruptedException并清除中断标志
java
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // 此时中断标志已被清除!
    // 若要保留中断状态,需重新设置:
    Thread.currentThread().interrupt();
}

正确处理 InterruptedException 的两种方式:

  1. 向上抛:让调用方决定如何处理
java
public void run() throws InterruptedException {
    Thread.sleep(1000);
}
  1. 恢复中断标志后退出:在不能抛异常时(如 Runnable.run)
java
public void run() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 恢复标志
        return; // 退出
    }
}

错误的处理方式:

java
try { Thread.sleep(1000); }
catch (InterruptedException e) {
    // 吞掉异常是反模式!丢失了中断信号
}

为什么不能用 Thread.stop()

  • 已废弃,强制停止可能导致锁未释放、数据不一致
  • 中断机制是更安全的替代方案

2.4 ★★★★synchronized 的底层实现原理?锁升级过程(偏向锁、轻量级锁、重量级锁)是怎样的?JDK 15 之后偏向锁发生了什么变化?

📖 点击查看答案

synchronized 底层实现:

  • 字节码层:同步代码块用 monitorenter / monitorexit;同步方法在方法表 flags 中标记 ACC_SYNCHRONIZED
  • JVM 层:基于 Monitor 对象实现,每个对象头中存有指向 Monitor 的指针
  • Monitor 本质是 C++ 的 ObjectMonitor,内部维护 _owner、_EntryList、_WaitSet

对象头(Mark Word)布局(JDK 8,64 位):

锁状态25 bit31 bit1 bit4 bit1 bit2 bit
无锁unusedhashCodeunused分代年龄001
偏向锁Thread ID (54) + Epoch (2)分代年龄101
轻量级锁指向栈中 Lock Record 的指针00
重量级锁指向 ObjectMonitor 的指针10
GC 标记-11

锁升级过程:

  1. 无锁:对象刚创建,Mark Word 为无锁状态

  2. 偏向锁

    • 第一个线程访问时,CAS 把 Thread ID 写入 Mark Word
    • 之后同一线程进入同步块,只需对比 Thread ID,无需 CAS
    • 适合单线程反复进入同步块的场景
  3. 轻量级锁

    • 出现两个线程交替竞争(无实质性竞争)时,偏向锁撤销,升级为轻量级锁
    • 在当前线程栈帧中创建 Lock Record,CAS 把 Mark Word 复制到 Lock Record
    • 成功则获取锁,失败则自旋重试
    • 适合临界区很短的场景
  4. 重量级锁

    • 自旋失败(超过阈值)或竞争激烈时升级
    • 申请 ObjectMonitor,未获取锁的线程进入 _EntryList 阻塞(OS 层互斥量)
    • 性能开销大(用户态→内核态切换)

**锁升级是单向的:**无锁 → 偏向锁 → 轻量级锁 → 重量级锁(但偏向锁可批量撤销/重偏向)

JDK 15 之后偏向锁变化:

  • JDK 15 开始废弃偏向锁(JEP 374),默认关闭
  • JDK 18 正式移除相关代码
  • 原因:
    1. 偏向锁的实现复杂度高,维护成本大
    2. 现代应用多线程竞争更频繁,偏向锁收益下降
    3. 偏向锁撤销(safepoint)会引发 STW
    4. CAS 已经足够轻量
  • 参数 -XX:+UseBiasedLocking 已无效

关键点:

  • synchronized 是可重入的(Monitor 计数器)
  • synchronized 是非公平
  • JDK 6 之后 synchronized 性能大幅优化,与 ReentrantLock 差距缩小

2.5 ★★★★synchronizedReentrantLock 的区别是什么?各自的底层实现是什么?什么时候选择 ReentrantLock

📖 点击查看答案

核心区别:

维度synchronizedReentrantLock
性质JVM 关键字JDK 类(java.util.concurrent.locks)
实现Monitor(对象头)AQS(CAS + CLH 队列)
释放自动(出代码块)手动(必须 finally unlock)
公平性非公平可选公平/非公平
可中断不可中断(不响应 interrupt)lockInterruptibly() 可中断
超时不支持tryLock(timeout) 支持
条件变量一个(wait/notify)多个 Condition
锁绑定对象Lock 对象
锁状态无法查询isLocked()getHoldCount()
公平选择不可构造参数选择

底层实现:

  • synchronized:基于对象头的 Mark Word,升级到重量级锁时用 ObjectMonitor(OS Mutex)
  • ReentrantLock:基于 AQS
    • state 变量记录锁被重入次数
    • 抢锁用 CAS 修改 state
    • 抢锁失败封装为 Node 入队(CLH 变种),LockSupport.park() 阻塞

ReentrantLock 的使用模式:

java
ReentrantLock lock = new ReentrantLock(); // 默认非公平
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock(); // 必须在 finally
}

可中断、超时尝试:

java
lock.lockInterruptibly();          // 可被 interrupt 打断
if (lock.tryLock()) { ... }        // 尝试一次
if (lock.tryLock(3, TimeUnit.SECONDS)) { ... } // 超时尝试

多 Condition:

java
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 经典:生产者-消费者(ArrayBlockingQueue 实现)

何时选择 ReentrantLock:

  1. 需要公平锁
  2. 需要可中断的锁等待
  3. 需要超时获取锁
  4. 需要多个条件变量(如生产者/消费者分离等待)
  5. 需要尝试获取锁(避免死锁)
  6. 需要复杂的同步控制(读写锁、StampedLock 底层也是 AQS)

何时用 synchronized:

  • 简单同步,不需要上述高级特性
  • 不会忘记释放锁(避免死锁)
  • JDK 6+ 性能已接近 ReentrantLock,且 JVM 持续优化

2.6 ★★公平锁与非公平锁的区别是什么?ReentrantLock 默认是哪种?底层是如何实现的?

📖 点击查看答案

核心区别:

  • 公平锁:多个线程按申请锁的顺序获取锁(FIFO),先到先得
  • 非公平锁:新线程直接尝试插队抢锁,失败再排队

ReentrantLock 默认是非公平锁:

java
public ReentrantLock() { sync = new NonfairSync(); }          // 默认非公平
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

性能差异:

  • 非公平锁吞吐量更高(默认)
    • 原因:刚释放锁的线程很可能再次获取,避免线程切换开销
  • 公平锁需要维护队列顺序,每次获取锁要检查队列是否有前驱线程,开销大

底层实现(基于 AQS):

  • 非公平锁的 tryAcquire
java
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) { // 直接 CAS,不看队列
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires); // 重入
        return true;
    }
    return false;
}
  • 公平锁的 tryAcquire
java
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && // 多了一步:检查队列前驱
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires);
        return true;
    }
    return false;
}

hasQueuedPredecessors 的作用:

  • 检查 CLH 队列中是否有当前线程之前的等待线程
  • 如果有,当前线程不能插队,必须排队

何时用公平锁:

  • 严格不能饿死某个线程(如交易系统)
  • 响应时间敏感
  • 但吞吐量要求不高

ReentrantReadWriteLock 同样默认非公平:

java
public ReentrantReadWriteLock() { this(false); }

2.7 ★★StampedLock 相比 ReadWriteLock 有什么优势?它的乐观读模式是如何工作的?

📖 点击查看答案

ReadWriteLock 的痛点:

  • 读锁会阻塞写锁(写饥饿问题)
  • 读锁之间不互斥,但读多写少时,写线程可能长时间拿不到锁
  • 读写不能相互升级

StampedLock 的优势:

  • 提供乐观读模式:读操作不加锁,仅做版本校验
  • 写锁和悲观读锁互斥,但乐观读不阻塞写
  • 吞吐量显著高于 ReadWriteLock(读多写少场景)

三种模式:

  1. 写锁(Writing):独占,writeLock() 返回 stamp,unlockWrite(stamp) 释放
  2. 悲观读锁(Reading):共享,readLock() 返回 stamp,unlockRead(stamp) 释放
  3. 乐观读(Optimistic Reading):无锁,tryOptimisticRead() 返回 stamp,validate(stamp) 校验

乐观读工作流程:

java
StampedLock sl = new StampedLock();
double x, y;

public double distanceFromOrigin() {
    long stamp = sl.tryOptimisticRead(); // 1. 乐观读,返回版本号
    double curX = x, curY = y;            // 2. 读取数据(可能不一致)
    if (!sl.validate(stamp)) {            // 3. 校验:读期间是否有写操作
        // 4. 校验失败:升级为悲观读锁,重新读取
        stamp = sl.readLock();
        try {
            curX = x; curY = y;
        } finally {
            sl.unlockRead(stamp);
        }
    }
    return Math.sqrt(curX * curX + curY * curY);
}

乐观读原理:

  • tryOptimisticRead() 返回当前 stamp(非 0),并保证此时没有写锁
  • validate(stamp) 检查 stamp 是否还有效(即读期间没有发生写)
  • 底层用 volatile 的 state 字段 + 位运算,无锁操作

StampedLock 的限制和注意:

  1. 不可重入:同一线程不能重复获取读锁,否则死锁
  2. 不支持 Condition:无法用 newCondition()
  3. 不要调用 interrupt():在 readLock() 阻塞时调用 interrupt 会导致 CPU 飙升(已知 bug)
  4. 谨慎使用:复杂度高于 ReentrantReadWriteLock

适用场景:

  • 读多写少,且能容忍短期数据不一致
  • 高并发场景下的高性能读
  • 典型:缓存、坐标点等数据

2.8 ★★★★volatile 关键字的两大作用是什么?它能保证原子性吗?为什么?底层是通过什么机制实现的?

📖 点击查看答案

volatile 的两大作用:

  1. 保证可见性:一个线程修改 volatile 变量,其他线程立即可见
  2. 禁止指令重排序:编译器、CPU 不会对 volatile 变量的读写进行重排序
  3. 保证有序性:通过内存屏障实现 happens-before

volatile 不能保证原子性:

  • i++ 不是原子操作(读-改-写三步),volatile 修饰的 i++ 在多线程下仍然不安全
java
volatile int i = 0;
// 多线程执行 i++ 仍然会丢失更新
// 因为 i++ 等价于:temp = i; temp = temp + 1; i = temp;
  • 想保证原子性:用 AtomicIntegersynchronizedLongAdder

为什么不能保证原子性:

  • volatile 只保证单次读/写的原子性(如 i = 5 是原子的)
  • 复合操作(i++i += 1)由多步组成,每步原子但整体不原子
  • 线程 A 读 i=0,线程 B 读 i=0,A 写 i=1,B 写 i=1 → 丢失更新

典型使用场景:

java
class Singleton {
    private static volatile Singleton instance;  // 必须 volatile!
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton(); // 防止指令重排
            }
        }
        return instance;
    }
}
  • 双重检查锁定的 instance 必须 volatile,否则新建对象的过程可能被重排序:
    • new Singleton() 实际是:① 分配内存 ② 初始化对象 ③ 引用赋值
    • 重排序可能变成 ①③②,其他线程在 ③ 后看到非 null 但未初始化的对象

状态标志位场景:

java
private volatile boolean running = true;
public void run() {
    while (running) { /* 工作 */ }
}
public void shutdown() { running = false; }

底层实现机制:

  • 基于**内存屏障(Memory Barrier)**实现
  • x86 架构:
    • volatile 写之前插入 StoreStore 屏障,之后插入 StoreLoad 屏障
    • volatile 读之后插入 LoadLoadLoadStore 屏障
  • 汇编层面:volatile 写会生成 lock addl $0,0,(%rsp) 指令(lock 前缀)
    • lock 前缀会:
      1. 锁定缓存行(或总线)
      2. 强制将缓存行数据写回主存
      3. 通过 MESI 缓存一致性协议使其他 CPU 的缓存失效

JMM 视角:

  • volatile 写 happens-before volatile 读
  • 配合 happens-before 规则的传递性,可实现线程间通信

2.9 ★★★★CAS(Compare And Swap)的原理是什么?ABA 问题是什么?Java 中如何解决 ABA 问题?

📖 点击查看答案

CAS 原理:

  • CAS(V, Expected, New):如果 V 的当前值等于 Expected,则更新为 New,返回 true;否则什么都不做,返回 false
  • 整个操作是原子的,由 CPU 指令保证(x86 的 cmpxchg
  • 是乐观锁的核心实现
java
AtomicInteger ai = new AtomicInteger(0);
boolean success = ai.compareAndSet(0, 1); // 期望0,更新为1

CAS 底层(Unsafe 类):

java
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
// HotSpot 实现最终调用 CPU 的 cmpxchg 指令
  • JDK 9+ 提供 VarHandle 替代 Unsafe

ABA 问题:

  • 线程1 读取值 A
  • 线程2 把 A 改成 B,又改回 A
  • 线程1 CAS(A → C) 成功,但中间的变化被忽略了

ABA 问题的危害:

  • 对于纯数值更新(如计数器)通常无害
  • 对于基于对象状态的操作(如链表节点复用)可能导致逻辑错误:
    • 线程1 准备从链表弹出 A(A.next=B),CAS(head, A, B)
    • 线程2 弹出 A、B,又把 A 重新压入(head=A, A.next=C)
    • 线程1 CAS 成功,head=B,但 B 已被弹出,链表损坏

解决 ABA:AtomicStampedReference / AtomicMarkableReference

java
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
// 初始值 100,初始版本号 0

int[] stampHolder = new int[1];
Integer value = ref.get(stampHolder);  // 获取值和版本号
int stamp = stampHolder[0];

// 线程2 即使把值改回 100,版本号也变了
ref.compareAndSet(100, 200, stamp, stamp + 1); // 必须传版本号
  • AtomicMarkableReference:用 boolean 标记代替版本号

CAS 的其他问题:

  1. 自旋开销大:竞争激烈时 CAS 反复失败,CPU 空转
  2. 只能保证单个变量原子:多个变量需用锁或 AtomicReference 包装成对象

CAS 的应用:

  • AtomicIntegerAtomicLongAtomicReference 等原子类
  • ConcurrentHashMap 等并发容器
  • AQS 中的 state 修改
  • LongAdder 用分段 CAS 减少竞争

2.10 ★★乐观锁和悲观锁的区别是什么?各自的应用场景有哪些?

📖 点击查看答案

核心思想:

  • 悲观锁:假定会发生冲突,访问数据前先加锁
  • 乐观锁:假定不会冲突,操作时不加锁,提交时检查冲突

对比:

维度悲观锁乐观锁
思想先加锁再操作操作后校验
并发性低(串行)高(无锁)
性能冲突多时好冲突少时好
实现synchronized、ReentrantLock、数据库行锁CAS、版本号机制
适用写多读少、冲突频繁读多写少、冲突少

悲观锁场景:

  1. 数据库行锁select ... for update
  2. Java 锁:synchronized、ReentrantLock
  3. 临界区竞争激烈:如秒杀扣库存
  4. 数据一致性要求高:金融账户
sql
-- 悲观锁:扣库存
SELECT stock FROM goods WHERE id=1 FOR UPDATE;
-- 业务判断
UPDATE goods SET stock = stock - 1 WHERE id=1;

乐观锁场景:

  1. CAS 原子类:AtomicInteger、LongAdder
  2. 数据库版本号/时间戳:UPDATE 时校验版本
sql
-- 乐观锁:扣库存
SELECT stock, version FROM goods WHERE id=1;
-- 业务判断
UPDATE goods SET stock = stock - 1, version = version + 1
WHERE id=1 AND version = #{version};
-- affected rows = 0 → 重试
  1. 无锁数据结构:ConcurrentHashMap、Disruptor
  2. 读多写少:缓存更新

CAS 实现乐观锁(Java):

java
AtomicInteger stock = new AtomicInteger(100);
public boolean deduct() {
    int current;
    do {
        current = stock.get();
        if (current <= 0) return false;
    } while (!stock.compareAndSet(current, current - 1));
    return true;
}

版本号机制(业务):

java
// 实体类
class User {
    Long id; String name; Integer version;
}
// 更新
userMapper.update(id, name, version); // WHERE id=? AND version=?
// 失败重试

选择建议:

  • 冲突少:乐观锁性能高
  • 冲突多:悲观锁避免反复重试
  • 临界区短:乐观锁更优
  • 临界区长:悲观锁更稳

2.11 ★★★★AQS 的核心原理是什么?它的 state 变量和 CLH 队列分别起什么作用?如何实现独占锁与共享锁?

📖 点击查看答案

AQS(AbstractQueuedSynchronizer)概述:

  • 是 J.U.C 的核心基类,为各种同步器提供框架
  • 子类:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier 等
  • 模板方法模式:AQS 提供骨架,子类实现 tryAcquire/tryRelease 等

核心组成:

  1. state 变量(volatile int):
    • 不同同步器赋予不同语义
    • ReentrantLock:state 表示锁被重入的次数
    • Semaphore:state 表示剩余许可数
    • CountDownLatch:state 表示还剩多少个计数
    • ReentrantReadWriteLock:state 高 16 位为读锁数量,低 16 位为写锁数量
    • 修改 state 用 CAS 保证原子性
java
private volatile int state;
protected final int getState() { return state; }
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
  1. CLH 队列变种(FIFO 双向队列)
    • 等待锁的线程封装为 Node 入队
    • Node 维护前驱、后继指针、等待状态、线程引用
    • 每个节点通过前驱节点的状态判断自己是否应该被唤醒
    • 抢锁失败的线程被 LockSupport.park() 阻塞,被前驱唤醒时 unpark()

Node 状态:

  • CANCELLED(1):节点已取消
  • SIGNAL(-1):后继节点需要被唤醒
  • CONDITION(-2):在 Condition 队列中
  • PROPAGATE(-3):共享模式下传播唤醒

独占锁实现流程:

java
// acquire 模板
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&                          // 1. 子类实现:尝试获取
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 入队 + 阻塞
        selfInterrupt();                              // 3. 补上中断
}

// ReentrantLock 的 tryAcquire
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        setState(c + acquires); // 重入
        return true;
    }
    return false;
}

共享锁实现流程:

java
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)   // 子类实现:< 0 表示获取失败
        doAcquireShared(arg);        // 入队 + 阻塞
}

// Semaphore 的 tryAcquireShared
protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining; // >= 0 成功,< 0 失败
    }
}
  • 共享锁唤醒后会传播唤醒:被唤醒的线程获取成功后,继续唤醒后继共享节点

独占 vs 共享:

维度独占共享
同时持有者1 个多个
方法tryAcquire/tryReleasetryAcquireShared/tryReleaseShared
典型ReentrantLockSemaphore、CountDownLatch、读锁
唤醒只唤醒一个后继传播唤醒

2.12 ★★★★什么是 Java 内存模型(JMM)?它和 JVM 内存结构有什么区别?happens-before 规则有哪些?

📖 点击查看答案

JMM(Java Memory Model):

  • JSR-133 规范,定义多线程下变量的访问规则
  • 解决:可见性、原子性、有序性问题
  • 抽象模型:每个线程有自己的本地内存(CPU 缓存/寄存器抽象),共享变量存储在主内存
线程 A                      线程 B
  │                          │
本地内存(副本)          本地内存(副本)
  │                          │
  └────── 主内存(共享) ─────┘

JMM vs JVM 内存结构:

维度JMMJVM 内存结构
关注点线程间通信、可见性内存区域划分
抽象层逻辑模型物理区域
内容happens-before、volatile、synchronized 语义堆、栈、方法区
用途解决并发问题描述 JVM 运行时数据区

JMM 解决的三大问题:

  1. 可见性:volatile、synchronized、final
  2. 原子性:synchronized、Lock、Atomic 类
  3. 有序性:volatile(禁止重排)、synchronized(互斥)

happens-before 规则(核心):

如果 A happens-before B,那么 A 的操作对 B 可见,且 A 的执行顺序先于 B。

8 大规则:

  1. 程序顺序规则:同一线程中,前面的操作 happens-before 后面的操作
  2. 监视器锁规则:unlock 操作 happens-before 后续对同一锁的 lock
  3. volatile 变量规则:volatile 写 happens-before 后续 volatile 读
  4. 线程启动规则Thread.start() happens-before 该线程的所有操作
  5. 线程终止规则:线程的所有操作 happens-before Thread.join() 返回
  6. 线程中断规则interrupt() 调用 happens-before 被中断线程检测到中断
  7. 对象初始化规则:对象初始化完成 happens-before 其 finalize 方法
  8. 传递性:A happens-before B,B happens-before C → A happens-before C

示例:

java
// 线程 A
int x = 1;            // (1)
volatile boolean flag = true; // (2) volatile 写

// 线程 B
if (flag) {           // (3) volatile 读
    int y = x;        // (4) y == 1 一定成立
}
// 依据:(1) HB (2) [程序顺序],(2) HB (3) [volatile 规则],(3) HB (4) [程序顺序]
// 传递性:(1) HB (4),所以 x=1 对 y 可见

内存屏障:

  • LoadLoad:确保 Load1 完成后再 Load2
  • LoadStore:确保 Load1 完成后再 Store2
  • StoreStore:确保 Store1 完成后再 Store2
  • StoreLoad:确保 Store 完成后再 Load(开销最大)

2.13 ★★★什么是死锁?产生死锁的四个必要条件是什么?如何排查和避免死锁?

📖 点击查看答案

死锁定义:

  • 两个或多个线程互相持有对方需要的资源,导致永久阻塞
java
// 经典死锁
Object lock1 = new Object(), lock2 = new Object();
new Thread(() -> {
    synchronized (lock1) {
        try { Thread.sleep(100); } catch (Exception e) {}
        synchronized (lock2) { } // 等待 lock2
    }
}).start();
new Thread(() -> {
    synchronized (lock2) {
        try { Thread.sleep(100); } catch (Exception e) {}
        synchronized (lock1) { } // 等待 lock1
    }
}).start();

死锁的四个必要条件(缺一不可):

  1. 互斥条件:资源同一时刻只能被一个线程占用
  2. 请求与保持:线程持有资源 A 的同时请求资源 B,不释放 A
  3. 不剥夺条件:资源不能被强制剥夺,只能由持有者主动释放
  4. 循环等待:多个线程形成头尾相接的循环等待关系

排查死锁的方法:

  1. jstack:打印线程堆栈,会自动检测死锁
bash
jstack <pid>
# 输出 "Found one Java-level deadlock"
  1. jconsole / VisualVM:图形化工具,可视化线程状态
  2. Arthasthread -b 查找阻塞线程,thread 查看所有线程
  3. JMXThreadMXBean.findDeadlockedThreads()
java
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] ids = bean.findDeadlockedThreads();
  1. 日志和堆栈:业务日志 + thread dump

避免死锁的策略:

  1. 破坏请求与保持:一次性申请所有资源

    java
    // 一次性获取多个锁(tryLock)
    if (lock1.tryLock() && lock2.tryLock()) {
        try { ... } finally { lock1.unlock(); lock2.unlock(); }
    }
  2. 破坏不剥夺:使用 tryLock(timeout) 超时获取,失败则释放已有锁重试

  3. 破坏循环等待按固定顺序获取锁(最常用)

    java
    // 所有线程按 lock1 → lock2 顺序加锁
  4. 避免嵌套锁:尽量减少锁的层级

  5. 使用更高级的并发工具

    • ConcurrentHashMap 替代 synchronized Map + 其他锁
    • BlockingQueue 实现生产者-消费者(替代 wait/notify + 锁)

预防与检测:

  • 代码评审:检查锁的顺序
  • 单元测试:并发场景测试
  • 上线监控:检测线程阻塞、响应超时
  • 使用 tryLock(timeout) 替代 lock() 防止永久阻塞

2.14 ★★★★线程池的核心参数有哪些?每个参数的含义是什么?任务提交后的执行流程是怎样的?

📖 点击查看答案

ThreadPoolExecutor 的 7 个核心参数:

java
public ThreadPoolExecutor(
    int corePoolSize,                  // 1. 核心线程数
    int maximumPoolSize,               // 2. 最大线程数
    long keepAliveTime,                // 3. 空闲存活时间
    TimeUnit unit,                     // 4. 时间单位
    BlockingQueue<Runnable> workQueue, // 5. 任务队列
    ThreadFactory threadFactory,       // 6. 线程工厂
    RejectedExecutionHandler handler   // 7. 拒绝策略
);

参数详解:

参数含义
corePoolSize核心线程数,即使空闲也不会回收(除非 allowCoreThreadTimeOut)
maximumPoolSize线程池最大线程数
keepAliveTime非核心线程的空闲存活时间
unit时间单位
workQueue任务队列,保存等待执行的任务
threadFactory创建线程的工厂,可自定义线程名
handler饱和时的拒绝策略

常用的 workQueue:

  • LinkedBlockingQueue:无界(默认 Integer.MAX_VALUE),FIFO
  • ArrayBlockingQueue:有界,FIFO
  • SynchronousQueue:无缓冲,直接交付
  • PriorityBlockingQueue:优先级队列

四种拒绝策略:

  • AbortPolicy(默认):抛 RejectedExecutionException
  • CallerRunsPolicy:由提交任务的线程执行
  • DiscardPolicy:直接丢弃
  • DiscardOldestPolicy:丢弃队列最老的任务,再尝试提交

任务执行流程(关键):

  1. 提交任务到线程池
  2. 判断当前线程数是否小于 corePoolSize:
    • 是:创建核心线程执行任务
    • 否:进入下一步
  3. 尝试将任务加入 workQueue:
    • 成功:等待执行
    • 失败(队列满):进入下一步
  4. 判断当前线程数是否小于 maximumPoolSize:
    • 是:创建非核心线程执行任务
    • 否:执行拒绝策略
提交任务


线程数 < corePoolSize? ──Y──> 创建核心线程执行
   │ N

队列未满? ──Y──> 入队等待
   │ N

线程数 < maxPoolSize? ──Y──> 创建非核心线程执行
   │ N

执行拒绝策略

重要细节:

  • 核心线程在 JDK 7+ 也是延迟创建(首次提交任务才创建)
  • 非核心线程空闲超过 keepAliveTime 会被回收
  • 任务队列满后才会创建非核心线程(很多人误以为是先到最大线程数才入队)
  • prestartAllCoreThreads() 可预热核心线程

常见设置:

java
new ThreadPoolExecutor(
    8, 16, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

2.15 ★★ThreadPoolExecutor 任务执行的完整流程是怎样的?

📖 点击查看答案

execute() 方法源码:

java
public void execute(Runnable command) {
    int c = ctl.get();
    // 1. 当前线程数 < corePoolSize,新增 worker
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) return;
        c = ctl.get();
    }
    // 2. 队列未满,入队
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false); // 防止所有线程已死,任务在队列中无人执行
    }
    // 3. 队列满,尝试新增非核心线程
    else if (!addWorker(command, false))
        // 4. 失败(达到 maxPoolSize),拒绝
        reject(command);
}

addWorker 流程:

  1. CAS 增加 workerCount
  2. 创建 Worker 对象(封装 firstTask、ThreadFactory 创建线程)
  3. 加锁后将 Worker 加入 workers 集合
  4. 调用 thread.start() 启动线程

Worker 线程的 run 流程:

java
final void runWorker(Worker w) {
    Runnable task = w.firstTask;
    while (task != null || (task = getTask()) != null) {
        w.lock(); // Worker 自己有锁,用于 shutdown 时判断是否空闲
        try {
            beforeExecute(w.thread, task); // 钩子
            try {
                task.run();
                afterExecute(task, null);
            } catch (Throwable ex) {
                afterExecute(task, ex);
            }
        } finally {
            task = null;
            w.completedTasks++;
            w.unlock();
        }
    }
    processWorkerExit(w, false); // 线程退出处理
}

getTask()(核心线程的阻塞获取):

java
Runnable getTask() {
    boolean timedOut = false;
    for (;;) {
        // 是否允许超时回收(核心线程也回收需要 allowCoreThreadTimeOut)
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        if (wc > maximumPoolSize && workQueue.isEmpty()) return null; // 退出
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take(); // 核心线程永久阻塞
            if (r != null) return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

submit() 与 execute() 的区别:

  • execute(Runnable):无返回值
  • submit(Callable/Runnable):返回 Future,内部包装成 FutureTask 提交

关键细节:

  • 工作线程通过 Worker 的 AQS 实现(不可重入)实现"是否在执行任务"的判断
  • shutdown 时通过 tryLock 判断线程是否空闲,空闲则中断
  • 线程异常退出后会自动新增一个 worker(除非 shutdown 中)

2.16 ★★★★Executors 工厂方法创建的几种线程池各自有什么缺陷?为什么阿里巴巴开发规范不推荐使用 Executors

📖 点击查看答案

Executors 提供的几种线程池:

  1. newFixedThreadPool(int):固定大小线程池
  2. newSingleThreadExecutor():单线程池
  3. newCachedThreadPool():可缓存线程池
  4. newScheduledThreadPool(int):定时任务线程池
  5. newWorkStealingPool(int):JDK 8 引入,ForkJoinPool

各线程池的缺陷:

1. newFixedThreadPool

java
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 缺陷:队列无界(LinkedBlockingQueue 默认 Integer.MAX_VALUE)
  • 风险:任务堆积导致 OOM

2. newSingleThreadExecutor

java
public static ExecutorService newSingleThreadExecutor() {
    return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 缺陷:同上,队列无界
  • 风险:任务堆积导致 OOM

3. newCachedThreadPool

java
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • 缺陷:最大线程数无界(Integer.MAX_VALUE)
  • 风险:高并发下创建大量线程,导致 OOM(无法创建 native 线程)

4. newScheduledThreadPool

java
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
// 内部用 DelayedWorkQueue,也是无界
  • 缺陷:队列无界,最大线程数 Integer.MAX_VALUE
  • 风险:任务堆积或线程爆炸

为什么不推荐用 Executors(阿里规约):

  1. 无界队列/无界线程导致 OOM 风险
  2. 无法自定义拒绝策略:默认 AbortPolicy 抛异常可能影响业务
  3. 无法自定义线程名:默认 pool-1-thread-1,排查问题困难
  4. 无法精细控制参数:corePoolSize、maxPoolSize、queue 容量都是固定的

正确做法:用 ThreadPoolExecutor 显式创建

java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    8,                                  // 核心线程数
    16,                                 // 最大线程数
    60L, TimeUnit.SECONDS,              // 空闲时间
    new ArrayBlockingQueue<>(1000),     // 有界队列
    new ThreadFactoryBuilder()
        .setNameFormat("order-pool-%d") // 业务可识别的线程名
        .setUncaughtExceptionHandler(...)
        .build(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 自定义拒绝策略
);

线程数计算建议(参见 2.18):

  • CPU 密集型:N + 1
  • IO 密集型:2N 或 N × (1 + 等待时间/计算时间)

2.17 ★★线程池的四种拒绝策略分别是什么?在实际业务中如何选择或自定义拒绝策略?

📖 点击查看答案

四种内置拒绝策略:

策略行为适用场景
AbortPolicy(默认)抛 RejectedExecutionException关键任务,必须感知到失败
CallerRunsPolicy由提交任务的线程执行不希望丢任务,能接受降速
DiscardPolicy静默丢弃允许丢失(如日志)
DiscardOldestPolicy丢弃队列最老的任务,重新提交新任务优先级高(如最新数据)

源码:

java
public static class AbortPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
    }
}

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) r.run(); // 提交者执行
    }
}

public static class DiscardPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } // 啥也不做
}

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll(); // 丢最老的
            e.execute(r);        // 重试
        }
    }
}

业务选择建议:

  1. 关键业务(订单、支付):自定义策略,记录日志 + 持久化 + 报警
java
public class PersistAndAlertPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        log.error("线程池拒绝任务: {}", r);
        // 1. 持久化到 DB/MQ,后续重试
        taskRepository.save((Task) r);
        // 2. 触发告警
        alertService.send("线程池饱和");
    }
}
  1. 非关键业务(日志、埋点):DiscardPolicy,直接丢弃

  2. 希望降速保护系统:CallerRunsPolicy,让调用方减速

  3. 实时数据(最新值优先):DiscardOldestPolicy,丢弃旧数据

自定义策略的常见模式:

java
// 1. 阻塞提交者(同步阻塞)
new RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        try {
            e.getQueue().put(r); // 阻塞直到队列有位置
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
    }
};

// 2. 转移到 MQ
new RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        mqProducer.send(r); // 转为异步处理
    }
};

注意:

  • CallerRunsPolicy 会阻塞调用者,可能导致调用方雪崩
  • DiscardPolicy/DiscardOldestPolicy 没有任何日志,慎用(业务无感知)
  • 自定义策略务必记录日志,便于排查

2.18 ★★如何合理设置线程池大小?CPU 密集型与 IO 密集型有何区别?allowCoreThreadTimeOut 的作用是什么?

📖 点击查看答案

CPU 密集型 vs IO 密集型:

维度CPU 密集型IO 密集型
瓶颈CPU 计算等待 IO(磁盘/网络/数据库)
CPU 利用率
线程数少(=N+1)多(=2N 或更多)
典型加密、压缩、序列化、计算HTTP 调用、DB 查询、文件 IO

线程数计算公式:

  1. CPU 密集型N + 1(N = CPU 核数)

    • 多一个线程用于在偶发的页面错误等情况下不阻塞 CPU
  2. IO 密集型

    • 经验公式:2N
    • 精确公式(Brian Goetz):N × (1 + 等待时间/计算时间)
    • N × (1 + WT/CT),WT/CT 越大线程越多
java
// 获取 CPU 核数
int n = Runtime.getRuntime().availableProcessors();
// CPU 密集型
int cpuIntensiveSize = n + 1;
// IO 密集型
int ioIntensiveSize = 2 * n;

实际调优建议:

  1. 不同任务用不同线程池:避免相互影响
  2. 先按公式估算,再压测调整:观察 CPU 利用率、吞吐量、响应时间
  3. 有界队列容量:根据任务平均处理时间和期望响应时间估算
    • 队列容量 ≈ 线程数 × 单任务处理时间 × 期望等待时间因子
  4. 监控线程池指标:活跃线程数、队列大小、拒绝次数、任务平均耗时

混合型任务的处理:

  • 拆分为 CPU 密集和 IO 密集两类,分别用不同线程池
  • 或用 ForkJoinPool 让任务自己决定并行度

allowCoreThreadTimeOut 的作用:

java
public void allowCoreThreadTimeOut(boolean value);
  • 默认 false:核心线程即使空闲也不会被回收(一直阻塞在 workQueue.take()
  • 设为 true:核心线程空闲超过 keepAliveTime 也会被回收
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(8, 16, 60L, TimeUnit.SECONDS, queue);
pool.allowCoreThreadTimeOut(true); // 允许核心线程超时回收

使用场景:

  • 任务有明显波峰波谷(白天忙、晚上闲),希望低谷期释放线程资源
  • 长期运行的服务,避免不必要的线程占用
  • 突发流量场景,避免线程恢复慢

注意:

  • 设为 true 后,新任务到来时需要重新创建线程,会有短暂延迟
  • 设置过小的核心线程数可能影响响应时间

2.19 ★★★★CountDownLatchCyclicBarrierSemaphore 三者的区别和使用场景是什么?

📖 点击查看答案

三者对比:

维度CountDownLatchCyclicBarrierSemaphore
作用等待 N 个事件完成N 个线程互相等待控制并发访问数
计数减法加法增减
重置不可重置可重置(cyclic)可增可减
基于AQS 共享模式ReentrantLock + ConditionAQS 共享模式
场景主线程等待子任务多线程阶段性同步限流、资源池

CountDownLatch(一次性倒计时):

java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try { doWork(); } finally { latch.countDown(); }
    }).start();
}
latch.await(); // 阻塞到 count = 0
System.out.println("所有子任务完成");
  • 不可重置,计数到 0 后无法再用
  • 典型场景:
    • 主线程等待多个子任务并行执行
    • 服务启动时检查多个依赖(DB、缓存、外部服务)
    • 多线程初始化数据

CyclicBarrier(可循环屏障):

java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到达屏障,执行汇总");
});
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        doWork();
        barrier.await(); // 等待其他线程
        doNextPhase();
    }).start();
}
  • 可重置,可重复使用(cyclic)
  • 所有线程都到达屏障后,可选执行一个 barrier action
  • 典型场景:
    • 多线程分阶段计算(如多阶段归并)
    • 并行测试,所有线程准备好后同时开始
    • 多线程对大数据分片处理,每阶段同步一次

Semaphore(信号量):

java
Semaphore permits = new Semaphore(5); // 5 个许可
for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        try {
            permits.acquire(); // 获取许可
            doWork();
        } finally {
            permits.release(); // 释放许可
        }
    }).start();
}
  • 用于限流(限制同时访问某资源的线程数)
  • 典型场景:
    • 数据库连接池
    • 限流器(如接口 QPS 限制)
    • 资源池

区别细节:

  1. CountDownLatch 是"一个等多个"或"多个等一个",单向等待
  2. CyclicBarrier 是"多个互相等",对称等待
  3. CountDownLatch 计数到 0 后失效;CyclicBarrier 可以 reset 重新使用
  4. CountDownLatch 的 await 不要求所有线程都 await;CyclicBarrier 必须所有线程都 await

2.20 ★★★★ThreadLocal 的实现原理是什么?为什么会导致内存泄漏?如何正确使用 ThreadLocal

📖 点击查看答案

ThreadLocal 的实现原理:

  • 每个 Thread 对象内部有一个 ThreadLocalMap 字段
  • ThreadLocalMap 的 key 是 ThreadLocal 对象(弱引用),value 是用户数据
  • 调用 threadLocal.get() 时,实际是从当前线程的 ThreadLocalMap 中查找
java
// Thread 类
ThreadLocal.ThreadLocalMap threadLocals = null;

// ThreadLocalMap.Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k); // key 是弱引用
        value = v;
    }
}

set/get 流程:

java
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) map.set(this, value);
    else createMap(t, value);
}

public T get() {
    ThreadLocalMap map = getMap(Thread.currentThread());
    if (map != null) {
        Entry e = map.getEntry(this);
        if (e != null) return (T) e.value;
    }
    return setInitialValue();
}

为什么导致内存泄漏:

  1. ThreadLocal 的 key 是弱引用:ThreadLocal 外部强引用断开后,key 会被 GC 回收 → key=null
  2. 但 value 是强引用:Entry.value 一直引用着 value 对象
  3. 线程长期存活(线程池),ThreadLocalMap 也长期存活,导致 value 无法回收
Thread ──强──> ThreadLocalMap ──> Entry[]

                       key(弱) ──> ThreadLocal(被GC)
                       value(强) ──> 大对象(泄漏!)

实际场景:

  • 线程池中的线程长期存活
  • 使用 ThreadLocal 存储大对象(如 Connection、UserContext)
  • 业务代码忘记调用 remove()
  • key 被 GC 但 value 还在,无法访问也无法回收

正确使用 ThreadLocal:

  1. 使用完务必 remove(最重要)
java
ThreadLocal<User> ctx = new ThreadLocal<>();
try {
    ctx.set(currentUser);
    // 业务逻辑
} finally {
    ctx.remove(); // 必须!
}
  1. 不要存储大对象
  2. 避免共享 ThreadLocal 实例(每个线程应有独立副本)
  3. 使用 static final 修饰 ThreadLocal:避免被 GC 导致 key 失效
java
private static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();
  1. 使用 InheritableThreadLocal 实现父子线程传递(或 TransmittableThreadLocal 用于线程池场景)

ThreadLocal 的常见应用:

  • 用户上下文(登录用户)
  • 数据库连接、事务管理
  • 日期格式化(SimpleDateFormat 非线程安全)
  • 链路追踪 traceId
  • MDC 日志上下文

最佳实践:

java
try {
    UserContext.set(user);
    log.info("处理用户 {}", UserContext.get().getId());
    // 业务...
} finally {
    UserContext.remove(); // 一定要 remove
}

2.21 ★★CompletableFuture 有哪些核心方法?如何优雅地编排多个异步任务?thenApplythenComposethenCombine 的区别?

📖 点击查看答案

CompletableFuture 概述:

  • JDK 8 引入的异步编程工具,实现了 Future 和 CompletionStage
  • 支持链式调用、组合、异常处理
  • 比 Future 更强大:可主动完成、回调通知、组合多个任务

创建 CompletableFuture:

java
// 1. 异步执行(默认 ForkJoinPool)
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> doSomething());

// 2. 异步执行带返回值
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "result");

// 3. 指定线程池
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> "result", executor);

// 4. 已完成
CompletableFuture.completedFuture("done");

核心方法分类:

类型方法说明
转换thenApply / thenApplyAsyncT → R
消费thenAcceptT → void
执行thenRun() → void
组合thenComposeflatMap
合并thenCombine合并两个 CF
多任务allOf / anyOf全部/任一完成
异常exceptionally / handle / whenComplete异常处理

thenApply vs thenCompose vs thenCombine:

  1. thenApply(fn):把当前结果转为另一个值(同步,类似 map)
java
CompletableFuture<Integer> f = CompletableFuture.supplyAsync(() -> "123")
    .thenApply(Integer::parseInt); // String → Integer
  1. thenCompose(fn):当前结果 → 另一个 CompletableFuture(异步,类似 flatMap)
java
CompletableFuture<String> f = CompletableFuture.supplyAsync(() -> getUser(1))
    .thenCompose(user -> CompletableFuture.supplyAsync(() -> getOrders(user)));
// 返回 CompletableFuture<String>,而不是 CompletableFuture<CompletableFuture<String>>
  1. thenCombine(other, biFn):合并两个独立的 CompletableFuture 的结果
java
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "price=100");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "user=A");
CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + "&" + b);

优雅编排示例:

java
// 查用户 + 查订单(并行)
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.findById(1), executor);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.findByUserId(1), executor);

// 合并结果
CompletableFuture<UserDTO> result = userFuture.thenCombine(orderFuture, (user, orders) -> {
    UserDTO dto = new UserDTO();
    dto.setUser(user);
    dto.setOrders(orders);
    return dto;
}).exceptionally(ex -> {
    log.error("查询失败", ex);
    return UserDTO.empty();
});

// 链式:先查用户,再查订单,再查商品
CompletableFuture<List<Product>> future = CompletableFuture
    .supplyAsync(() -> userService.findById(1), executor)
    .thenComposeAsync(user -> CompletableFuture.supplyAsync(() -> orderService.findByUser(user), executor))
    .thenComposeAsync(orders -> CompletableFuture.supplyAsync(() -> productService.findByOrders(orders), executor));

// 等待多个任务全部完成
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.join();

异步 vs 同步方法:

  • thenApply:默认在前一个任务完成的线程执行

  • thenApplyAsync:提交到 ForkJoinPool(或指定线程池)异步执行

  • 推荐业务场景下显式传入业务线程池,避免占用 ForkJoinPool

异常处理:

java
future
    .thenApply(...)
    .exceptionally(ex -> fallback) // 仅异常时
    .handle((result, ex) -> ex != null ? fallback : result) // 异常和正常都处理
    .whenComplete((result, ex) -> log.info("完成: {}, 异常: {}", result, ex)); // 不修改结果

2.22 ★★什么是伪共享(False Sharing)?在 Java 中如何解决?@Contended 注解的原理是什么?

📖 点击查看答案

伪共享定义:

  • CPU 缓存以**缓存行(Cache Line)**为单位加载,通常 64 字节
  • 多个变量位于同一缓存行时,一个线程修改变量 A 会让另一 CPU 上修改变量 B 的缓存行失效
  • 导致频繁的缓存同步,性能急剧下降

示例:

java
class Counter {
    volatile long x;  // 8 字节
    volatile long y;  // 8 字节
}
// x 和 y 大概率位于同一缓存行
// 线程 A 修改 x,线程 B 修改 y,会互相让对方的缓存行失效

为什么是"伪"共享:

  • 两个变量逻辑上没有共享,但物理上位于同一缓存行
  • CPU 缓存一致性协议(MESI)让缓存失效,造成不必要的总线通信

解决方法:

  1. 缓存行填充(Padding):在变量之间填充无用字段,确保不同变量在不同缓存行
java
class PaddedCounter {
    volatile long x;
    public long p1, p2, p3, p4, p5, p6, p7; // 填充 56 字节
    volatile long y;
}
  1. @Contended 注解(JDK 8+):JVM 自动填充
java
@jdk.internal.vm.annotation.Contended
class Counter {
    volatile long x;
    volatile long y;
}
  • 需要 JVM 参数 -XX:-RestrictContended 才能生效(默认仅 JDK 内部可用)
  • 字段级别也可以用:
java
class Counter {
    @Contended volatile long x;
    @Contended volatile long y;
}
  1. 使用 @Contended 的实际场景
    • JDK 内部:AtomicLongLongAdder 的 Cell 类、Thread 类的随机数种子
    • Disruptor 框架的 RingBuffer
    • 高性能计数器

@Contended 原理:

  • 编译期:注解本身只是元数据
  • 运行期:JVM 在对象布局时,为 @Contended 字段前后填充 padding(默认 128 字节)
  • 字段分组:同组的字段会填充到一起,不同组之间填充
java
// 字段分组示例
class Example {
    @Contended("group1") int a;
    @Contended("group1") int b;
    @Contended("group2") int c;
}
// a 和 b 在一起,c 单独填充

LongAdder 的应用:

java
// Striped64.Cell 源码
@jdk.internal.vm.annotation.Contended
static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // ...
}
  • LongAdder 用 Cell[] 分散计数,避免单个变量竞争
  • 每个 Cell 用 @Contended 避免伪共享,性能远超 AtomicLong(高并发下)

何时关注伪共享:

  • 高并发场景下对共享变量的频繁修改
  • 性能优化已经做到极致
  • 一般业务代码不需要关注

基于 4 份主流面试题库整合精选,共 200 题