Java 并发面试题

基础

什么是 java 中的线程安全

在 Java 中,线程安全是指一个类或对象在多线程环境下能够正确执行,不会出现数据不一致或其他异常。其核心在于处理共享可变状态的访问。

说说线程的几种创建方式

  1. 继承Thread类:

    • 重写 run() 方法。

    • 创建子类对象并调用 start() 方法启动线程。

  2. 实现Runnable接口

    • 实现 run() 方法。

    • 将实现类对象作为参数传递给 Thread 构造器,调用 start() 启动线程。

  3. 实现 Callable 接口 + FutureTask

    • 实现 Callable<V> 接口,指定返回值类型。
    • 实现 call() 方法,该方法有返回值。
    • 使用 FutureTask<V> 包装 Callable 对象。
    • FutureTask 对象作为参数传递给 Thread 构造器,调用 start() 启动线程。
    • 通过 FutureTask.get() 获取线程执行结果(可能阻塞)。
  4. 使用线程池(ExecutorService)

    • 创建线程池(如 Executors.newFixedThreadPool())。
    • 提交 RunnableCallable 任务到线程池。
    • 通过 Future 获取任务结果(若有)。
    • 关闭线程池。

线程的生命周期

Java 线程的生命周期分为 6 种状态,由 Thread.State 枚举定义:

状态名称 说明
NEW(新建) 线程对象已创建,但未调用 start() 方法(未启动)。
RUNNABLE(可运行) 线程已调用 start() 方法,正在 JVM 中运行,或等待 CPU 调度(包含 “就绪” 和 “运行中” 两种情况)。
BLOCKED(阻塞) 线程因竞争 对象锁 而阻塞(如进入 synchronized 块 / 方法时未获取到锁)。
WAITING(无限等待) 线程无超时地等待其他线程的特定操作(如 wait()join() 等)。
TIMED_WAITING(超时等待) 线程有超时时间地等待(如 sleep(1000)wait(1000) 等,超时后自动唤醒)。
TERMINATED(终止) 线程执行完毕(run() 方法结束)或因异常退出。

什么是线程上下文切换

线程上下文切换是操作系统实现多线程并发的基础,通过保存和恢复线程状态,让 CPU 能在多个线程间快速切换。但它存在固有开销,合理控制线程数量、减少不必要的阻塞(如避免频繁 I/O、优化锁竞争)是降低切换开销、提升并发性能的关键。

线程间有哪些通讯方式

在Java中,线程之间的通讯就是指多个线程协同工作

线程之间传递信息的方式有多种,比如说使用 volatile 和 synchronized 关键字共享对象、使用 wait()notify() 方法实现生产者-消费者模式、使用 Exchanger 进行数据交换、使用 Condition 实现线程间的协调等。

说说 sleep 和 wait 的区别

维度 sleep() wait()
所属类 Thread.sleep()(静态方法) Object.wait()(实例方法)
释放锁 ❌ 不释放持有的锁 ✅ 释放持有的对象锁(需在synchronized中调用)
唤醒方式 时间到期自动唤醒,或被interrupt()中断 需其他线程调用notify()/notifyAll()唤醒
使用场景 单纯暂停线程执行(如模拟耗时操作) 线程间协作(如生产者 - 消费者模型中的条件等待)
调用位置 可在任何代码中调用 必须在synchronized块 / 方法中调用

ThreadLocal

ThreadLocal 是什么?

ThreadLocal 是多线程编程中的一个特殊工具,用于为每个使用它的线程都独立存储一份数据副本。每个线程都可以独立地修改自己的副本,而不会影响其他线程的数据,从而实现线程间的数据隔离。

说说对 ThreadLocal 的理解

  • 每个 Thread 对象内部持有 ThreadLocalMap (哈希表结构)成员变量(threadLocals)。
  • ThreadLocalMapThreadLocal 的一个静态内部类,它内部维护了一个Entry数组,Entry 继承了 WeakReference。
  • KeyThreadLocal 实例(弱引用),Value 为线程变量副本(强引用)。
  • 数据访问
    • 调用 threadLocal.set(value) 时,以当前 ThreadLocal 实例为 Key,存入当前线程的 ThreadLocalMap
    • 调用 threadLocal.get() 时,从当前线程的 Map 中通过 Key 取出 Value。
  • 哈希优化:Key 的哈希值由黄金分割数 0x61c88647 生成,减少哈希冲突。
  • 冲突解决:采用 开放定址法(线性探测),而非链表。

说说 ThreadLocal 的内存泄漏问题

  • 原因:Entry 的 Key(ThreadLocal)是弱引用,但 Value 是强引用。如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null,但 Value 仍被线程强引用。
  • 后果:若线程长期运行(如线程池场景), Value 无法回收,会导致内存泄漏。
  • 解决方案
    1. 每次使用后 **必须调用 remove()**(在 finally 块中);
    2. 将 ThreadLocal 声明为 static final,避免实例被回收。”

如何跨线程传递 ThreadLocal 的值?

ThreadLocal 无法直接跨线程传递值,因为 ThreadLocal 变量存储在每个线程的 ThreadLocalMap 中。

我们可以使用 InheritableThreadLocal来解决这个问题。

InheritableThreadLocal:

  • ThreadLocal 的子类,专门用于在父子线程间传递值。当子线程被创建时,会自动复制父线程中 InheritableThreadLocal 的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class InheritableThreadLocalDemo {
    private static final InheritableThreadLocal<String> context = new InheritableThreadLocal<>();

    public static void main(String[] args) {
    // 在主线程中设置值
    context.set("主线程数据");

    // 创建子线程
    Thread childThread = new Thread(() -> {
    // 子线程可以获取到父线程设置的值
    System.out.println("子线程获取值: " + context.get()); // 输出: 主线程数据
    });

    childThread.start();
    }
    }
  • 局限性

    • 仅适用于直接创建的子线程:如果使用线程池(如 ExecutorService),由于线程是复用的,无法自动复制 InheritableThreadLocal 的值。
    • 值复制是单向的:子线程无法修改父线程中的值,反之亦然。

Java内存模型

说说对Java内存模型的理解

Java 内存模型(Java Memory Model, JMM)是一种抽象规范,用于定义多线程环境下变量的访问规则,解决以下问题:

  • 内存可见性:确保一个线程对共享变量的修改能及时被其他线程看到。
  • 指令重排序:编译器和 CPU 会对指令重排序优化,导致代码执行顺序与编写顺序不一致。
  • 原子性:简单的读写操作(如 i++)在底层可能被拆分为多条指令,多线程同时执行时导致结果错误。

什么是Java中的原子性、可见性和有序性?

  • 原子性(Atomicity):保证操作不被中断(要么全执行,要么全不执行),对应解决 “操作被拆分导致的数据不一致” 问题,由synchronized、原子类等保证。
  • 可见性(Visibility):保证变量修改被其他线程及时看到,对应解决 “工作内存与主内存同步延迟” 问题,由volatilesynchronized等保证。
  • 有序性(Ordering):保证指令执行顺序符合逻辑,对应解决 “指令重排序导致的多线程逻辑错误” 问题,由volatile、Happens-Before 规则等保证。

i++是原子操作吗?

  • i++ 看似是一个简单的操作,但实际上包含三个独立的步骤:

    1. 读取变量值:从主内存或缓存中读取 i 的当前值。
    2. 执行加 1 操作:在 CPU 中对读取的值进行加 1 计算。
    3. 写回新值:将计算结果写回主内存或缓存。

    这三个步骤不是一个不可分割的整体,在多线程环境下可能被其他线程打断。

  • 多线程下会出现错误

    假设有两个线程同时执行 i++,初始值 i = 0,期望结果:两个线程执行后,i应该为2。但可能的执行顺序如下:

    1. 线程 A 读取 i 的值为 0
    2. 线程 B 读取 i 的值为 0(此时线程 A 还未写入新值)。
    3. 线程 A 执行加 1 操作,得到结果 1
    4. 线程 B 执行加 1 操作,得到结果 1(因为 B 读取的是旧值 0)。
    5. 线程 A 将结果 1 写回主内存
    6. 线程 B 将结果 1 写回主内存

    最终结果i 的值为 1,而不是预期的 2。

  • 解决方案

    1. 使用原子类(推荐)

      1
      2
      3
      4
      5
      private AtomicInteger count = new AtomicInteger(0);

      public void increment() {
      count.incrementAndGet(); // 原子操作
      }
      • 原理:通过 CPU 的 CAS(Compare-And-Swap) 指令实现原子性。
    2. 加锁(synchronized/Lock)

      1
      2
      3
      public synchronized void increment() {
      count++;
      }
      • 性能可能会降低,牺牲性能换取安全。
    3. volatile 无法解决此类问题(仅解决可见性,不解决原子性)

说说什么是指令重排

指令重排(Instruction Reordering) 是计算机系统中为提升执行效率而对指令执行顺序进行优化的技术。

  1. 为什么会发生指令重排?

    指令重排的核心目的是提高程序运行效率

    • 编译器优化:Java 的 JIT(即时编译器)在编译字节码为机器码时,可能会调整指令顺序,比如将没有依赖关系的指令重新排序,减少 CPU 的等待时间。
    • 处理器优化:现代 CPU 支持 “乱序执行”(Out-of-Order Execution),如果前一条指令需要等待内存读写(耗时操作),CPU 会先执行后面没有依赖关系的指令,充分利用 CPU 资源。
  2. 多线程下的问题:重排导致逻辑错误

    多线程环境中,指令重排可能打破线程间的逻辑依赖,导致错误。因为重排只保证单线程结果正确,不考虑多线程间的交互。

    最经典的例子是双重检查锁定(DCL)的单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Singleton {
    private static Singleton instance; // 未用volatile

    public static Singleton getInstance() {
    if (instance == null) { // 第一次检查
    synchronized (Singleton.class) {
    if (instance == null) { // 第二次检查
    instance = new Singleton(); // 可能被重排序
    }
    }
    }
    return instance;
    }
    }

    instance = new Singleton()可拆分为三步:

    1. 分配内存空间(memory = allocate ())
    2. 初始化对象(ctorInstance (memory))
    3. 把 instance 指向内存空间(instance = memory)

    编译器可能重排序为 1→3→2。若线程 A 执行到 3(未执行 2)时,线程 B 进入第一次检查,发现instance != null,直接返回未初始化的对象,导致错误。给 instance 变量加上 volatile 关键字,可以禁止指令重排。

    1
    private static volatile Singleton instance; // 使用volatile
  3. 如何禁止指令重排?

    • volatile关键字:通过插入内存屏障(禁止特定类型的重排序)阻止指令重排序。例如,对volatile变量的写操作前插入 StoreStore 屏障,写操作后插入 StoreLoad 屏障,避免其前后的指令被重排序。
    • **synchronized/Lock**:同一时间只有一个线程执行同步代码块,天然保证了代码块内的指令按顺序执行。
    • happens-before 规则:JMM 定义的一套有序性规则,例如 “程序顺序规则”(单线程内指令按代码顺序执行)、“volatile 规则”(volatile 写先行发生于 volatile 读)、“锁规则”(解锁先行发生于加锁)等,通过这些规则保证多线程环境下的有序性。

as-if-serial了解吗

单线程环境中,指令重排不会影响程序的最终结果,这是因为编译器和处理器遵循as-if-serial 语义—— 即 “不管怎么重排,单线程程序的执行结果都要和按代码顺序执行的结果一致”。

举个栗子:

1
2
3
int a = 1;    // 指令1
int b = 2; // 指令2
int c = a + b; // 指令3

指令 1 和指令 2 没有依赖关系(互不影响结果),编译器可能先执行指令 2 再执行指令 1,但指令 3 必须在 1 和 2 之后(依赖它们的结果),因此最终c的结果一定是 3,单线程下完全没问题。

说说对volatile的理解

volatile 是 Java 中用于修饰变量的关键字,是一种轻量级的同步机制,主要用于解决多线程环境下变量的可见性有序性问题,但不保证原子性。它的设计目标是提供比 synchronized 更低开销的同步方案,适用于特定场景。

volatile 的核心作用

  1. 保证可见性

    可见性指:当一个线程修改了 volatile 变量的值后,其他线程能立即看到该变量的最新值。

    • 原理
      未被 volatile 修饰的变量,线程会从 “工作内存”(CPU 缓存)读取和修改,修改后不会立即同步到 “主内存”;其他线程可能一直使用旧的工作内存值,导致数据不一致。
      volatile 变量的读写会强制通过 “主内存” 进行:
      • 写操作:线程修改 volatile 变量后,会立即将新值刷新到主内存。
      • 读操作:线程读取 volatile 变量时,会先清空工作内存,再从主内存重新加载最新值。
  2. 禁止指令重排

    指令重排是编译器或 CPU 为优化性能,对代码执行顺序的调整(不影响单线程结果,但可能破坏多线程正确性)。volatile 通过内存屏障(Memory Barrier)禁止指令重排,保证代码执行顺序与预期一致。

    • 原理
      JVM 会在 volatile 变量的读写操作前后插入内存屏障,限制重排范围:
      • 禁止 volatile 变量写操作前的代码被重排到写操作之后;
      • 禁止 volatile 变量读操作后的代码被重排到读操作之前。
    • 典型场景:单例模式的双重检查锁
  3. 不保证原子性

    volatile 无法保证复合操作的原子性(即多线程下的 “读 - 改 - 写” 步骤可能被打断)。

volatile 的实现原理:内存屏障

JVM 通过插入内存屏障(特殊指令)保证 volatile 的可见性和有序性:

  • StoreStore 屏障:在 volatile 写操作前插入,确保之前的普通写操作已刷新到主内存。
  • StoreLoad 屏障:在 volatile 写操作后插入,防止写操作与后续的读操作重排。
  • LoadLoad 屏障:在 volatile 读操作后插入,防止后续读操作与该读操作重排。
  • LoadStore 屏障:在 volatile 读操作后插入,防止后续写操作与该读操作重排。

这些屏障强制 CPU 缓存与主内存同步,禁止指令重排,从而保证可见性和有序性。

volatilesynchronized 的区别

特性 volatile synchronized
原子性 不保证(仅可见性 / 有序性) 保证(同步块内操作原子)
可见性 保证 保证(解锁时刷新主内存)
有序性 保证(禁止重排) 保证(同步块内顺序执行)
开销 轻量(无锁) 较重(可能阻塞线程)
适用场景 状态标记、禁止重排 复杂同步逻辑(需原子性)

说说happens-before 规则

在 Java 内存模型(JMM)中,Happens-Before 规则是一组用于确定跨线程操作之间可见性的核心规则。它定义了 前一个操作的结果对后一个操作可见 的条件,是 JMM 保证多线程有序性和可见性的基础。

Happens-Before 的核心规则

  1. 程序顺序规则(Program Order Rule)

    单线程内,每个操作Happens-Before后续的所有操作。

    1
    2
    3
    int a = 1;  // 操作1
    int b = 2; // 操作2
    int sum = a + b; // 操作3

    单线程下,操作 1 Happens-Before 操作 2,操作 2 Happens-Before 操作 3。但编译器可能重排操作 1 和 2 的顺序(只要不影响最终结果),因此多线程下不能依赖此规则

  2. 监视器锁规则(Monitor Lock Rule)

    对一个锁的解锁操作 Happens-Before 后续对同一锁加锁操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class MonitorExample {
    private int x = 0;

    public void writer() {
    synchronized (this) { // 加锁
    x = 1; // 操作1
    } // 解锁
    } // 操作1 Happens-Before 后续其他线程的加锁

    public void reader() {
    synchronized (this) { // 加锁(必须等待前面的解锁)
    int y = x; // 操作2
    } // 解锁
    }
    }

    线程 A 解锁后,线程 B 加锁时能看到 A 对 x 的修改(x=1)。

  3. volatile 变量规则(Volatile Variable Rule)

    对一个volatile变量的写操作 Happens-Before 后续对同一变量读操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private volatile boolean flag = false;

    // 线程A
    public void writer() {
    flag = true; // 写操作
    } // 写操作Happens-Before 线程B的读操作

    // 线程B
    public void reader() {
    while (!flag) { // 读操作(保证看到最新值)
    // 循环等待
    }
    }

    volatile通过内存屏障禁止重排,确保写操作的结果对读操作可见。

  4. 线程启动规则(Thread Start Rule)

    线程的start()方法 Happens-Before 此线程的任意操作

    1
    2
    3
    4
    5
    6
    7
    Thread t = new Thread(() -> {
    // 线程t的操作
    System.out.println("线程t启动"); // 操作1
    });

    t.start(); // 主线程调用start()
    // start() Happens-Before 操作1

    主线程调用start()后,线程 t 的操作能看到主线程在start()前的所有修改。

  5. 线程终止规则(Thread Termination Rule)

    线程的所有操作 Happens-Before 其他线程检测到该线程已终止(如通过Thread.join()返回、Thread.isAlive()返回false)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Thread t = new Thread(() -> {
    // 线程t的操作
    System.out.println("线程t即将结束"); // 操作1
    });

    t.start();
    t.join(); // 主线程等待t终止
    // 操作1 Happens-Before t.join()返回
    System.out.println("线程t已结束");

    主线程能看到线程 t 的所有操作结果。

  6. 中断规则(Interruption Rule)

    线程 A 调用threadB.interrupt() Happens-Before 线程 B 检测到中断(如通过Thread.interrupted()Thread.isInterrupted())。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
    // 循环
    }
    System.out.println("线程t被中断"); // 操作1
    });

    t.start();
    t.interrupt(); // 主线程中断t
    // interrupt() Happens-Before 操作1

    线程 t 能看到主线程调用interrupt()的结果。

  7. 对象终结规则(Finalizer Rule)

    对象的初始化完成(构造函数执行结束) Happens-Before 它的finalize()方法开始。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Resource {
    public Resource() {
    // 初始化操作
    System.out.println("Resource初始化完成"); // 操作1
    }

    @Override
    protected void finalize() throws Throwable {
    // 对象销毁前的清理
    System.out.println("Resource finalize()开始"); // 操作2
    }
    }
    // 操作1 Happens-Before 操作2

    确保对象初始化完成后才会执行finalize()

  8. 传递性规则 (Transitivity)

    如果操作 A Happens-Before 操作 B,操作 B Happens-Before 操作 C,则操作 A Happens-Before 操作 C。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 线程A
    public void writer() {
    a = 1; // 操作1
    flag = true; // 操作2(volatile写)
    }

    // 线程B
    public void reader() {
    while (!flag) { // 操作3(volatile读)
    // 循环
    }
    int b = a; // 操作4
    }

    根据规则:

    1. 操作 1 Happens-Before 操作 2(程序顺序规则)。
    2. 操作 2 Happens-Before 操作 3(volatile 变量规则)。
    3. 操作 3 Happens-Before 操作 4(程序顺序规则)。
      通过传递性,操作 1 Happens-Before 操作 4,因此线程 B 能看到线程 A 对a的修改(a=1)。

什么是乐观锁和悲观锁

在并发编程中,乐观锁悲观锁是两种不同的锁策略,用于解决多线程环境下对共享资源的访问冲突。它们的核心区别在于对数据冲突的预期态度处理方式

特性 悲观锁 乐观锁
核心假设 假设冲突概率高 假设冲突概率低
加锁时机 先加锁,再操作 先操作,后验证
实现方式 synchronizedReentrantLock、数据库行锁(SELECT … FOR UPDATE) CAS、版本号机制
性能 写操作多时性能较差(锁竞争开销大) 读操作多时性能较好(无锁开销)
冲突处理 线程阻塞等待 重试或放弃操作
典型场景 库存扣减、金融交易 缓存更新、统计数据

什么是Java的CAS?

在 Java 里,CASCompare-And-Swap(比较并交换),它是一种无锁的原子操作,作用是实现多线程环境下的变量同步。CAS操作主要依赖于底层硬件(像 CPU 指令)来保证操作的原子性,避免了使用传统锁(例如synchronized)带来的线程阻塞和上下文切换开销。

CAS 操作包含三个参数:

  • V:要更新的变量(var)
  • E:预期值(expected)
  • N:新值(new)

如果变量 V 的值等于 E ,则将 V 的值变为 N;如果不相等则说明有其他线程在操作,放弃更新。整个过程由 CPU 保证原子性。

Java 中的实现

Java 通过Unsafe类和java.util.concurrent.atomic包提供 CAS 支持。

  • Unsafe类(底层实现)

    Unsafe类提供了硬件级别的原子操作,例如:

    1
    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
    • o:需要修改的对象。
    • offset:对象中变量的内存偏移量。
    • expected:预期值。
    • x:新值。
  • ② 原子类(Atomic Classes)

    基于Unsafe封装的原子类,如AtomicIntegerAtomicLong等:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import java.util.concurrent.atomic.AtomicInteger;

    public class CASDemo {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void increment() {
    // 自增操作 = getAndAdd(1) = CAS循环
    counter.incrementAndGet();
    }
    }

优点

  • 无锁性能:避免了线程阻塞和上下文切换,在竞争不激烈时性能远高于synchronized
  • 原子性保障:硬件级别的原子操作,无需依赖操作系统的锁机制。

CAS会有什么问题

  • ABA 问题

    若变量 V 的值从 A 变为 B,再变回 A,CAS 会认为值未变化,但实际已被修改。
    解决方法:使用AtomicStampedReference(带版本号的原子引用)。他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。

  • 自旋开销

    竞争激烈时,线程频繁重试 CAS 会消耗大量 CPU 资源。
    解决方法:限制重试次数或使用锁(如ReentrantLock)。

  • 只能保证单个变量的原子性

    CAS 只能对单个变量操作,如需原子操作多个变量,需将它们封装成一个对象(使用AtomicReference)。

什么是Java的 AQS?

AQS 是 Java 并发编程的基础框架,它通过一个volatile int类型的state变量表示锁状态,并用 CLH 队列管理等待线程。例如,ReentrantLockstate记录重入次数,Semaphorestate表示可用许可数。子类需要通过 CAS 操作修改state,并实现tryAcquiretryRelease方法来控制锁的获取与释放。

在 JUC 中的典型应用

  • **ReentrantLock**:公平锁和非公平锁的差异在于入队策略。非公平锁会先尝试 CAS 获取锁,失败后才入队;公平锁则直接入队等待。
  • **Semaphore**:当线程调用acquire()时,若state大于 0,则获取许可并减少state;否则阻塞。release()增加state并唤醒等待线程。
  • **CountDownLatch**:初始化时state为指定计数,线程调用countDown()时通过 CAS 减少state,当state减为 0 时,唤醒所有等待线程。
  • **ReentrantReadWriteLock**:使用两个 AQS 子类分别管理读锁和写锁,支持多读单写。

优缺点

  • 优点
    • 代码复用:避免重复实现队列管理和线程阻塞 / 唤醒逻辑。
    • 灵活性:通过模板方法支持多种锁语义(如公平锁、读写锁)。
    • 高性能:CAS 和 CLH 队列在无竞争时效率高。
  • 缺点
    • 实现复杂:需要深入理解 AQS 的模板方法和状态管理。
    • 只能支持独占或共享模式中的一种(子类需选择其一)。

AQS的核心原理

AQS 的核心是通过state变量和 CLH 队列实现线程同步。当线程竞争锁时:

  1. 获取锁:线程调用acquire()方法,尝试通过 CAS 将state从 0 改为 1。若成功,则获取锁;否则放入队列。
  2. 入队等待:线程被封装成Node,通过 CAS 插入队尾,并通过LockSupport.park()阻塞。
  3. 释放锁:持有锁的线程调用release()方法,将state置为 0,并通过LockSupport.unpark()唤醒后继节点。

AQS 支持两种同步方式:

  • 独占模式:如ReentrantLock,同一时间只有一个线程能持有锁,state表示重入次数。获取锁时,tryAcquire需判断state是否为 0(未锁定)或当前线程是否已持有锁(可重入)。

    核心方法:

    • acquire:获取锁,失败进入等待队列
    • release:释放锁,唤醒等待队列中的线程
  • 共享模式:如 Semaphore 和 CountDownLatchstate表示可用许可数。调用acquireShared时,若state大于 0,则 CAS 减少state并获取许可;否则入队等待。释放时,releaseShared增加state并唤醒后续节点。

    核心方法:

    • acquireShared:共享模式获取锁
    • releaseShared:共享模式释放锁

AQS 如何处理线程中断和超时?

  • 线程中断:
    • acquireInterruptibly():可中断获取锁,被中断时抛出InterruptedException
    • lockInterruptibly():如ReentrantLock的可中断锁实现。
  • 超时机制:
    • tryAcquireNanos():指定时间内尝试获取锁,超时则返回false

说说Java中ReentrantLock的实现原理?

  1. 在 Java 中,ReentrantLock是一个可重入的互斥锁,它位于java.util.concurrent.locks包下,实现了 Lock 接口,功能类似于synchronized关键字,但更加灵活。例如:

    • 可重入:线程可多次调用 lock() 而不阻塞自己(需对应次数的 unlock())。
    • 公平锁:按请求顺序获取锁,避免 “线程饥饿”(但性能开销大)。
    • 可中断锁:通过 lockInterruptibly() 允许线程在等待锁时响应中断。
  2. ReentrantLock的实现基于AQS(AbstractQueuedSynchronizer)CAS(Compare-And-Swap) 操作:

    • AQS:作为同步器的基础框架,线程队列和管理锁状态(state)记录锁的持有次数。
      • state = 0:锁未被持有。
      • state > 0:锁被持有,数值表示当前线程的重入次数。
        线程通过 CAS 操作尝试修改 state,成功则获取锁,失败则进入 AQS 队列等待。
    • CAS:用于原子性地更新锁状态,避免使用传统锁的开销。
  3. 公平锁 vs 非公平锁(如何实现?)

    特性 公平锁 非公平锁
    获取顺序 遵循请求顺序,先到先得(FIFO)。 允许插队,新请求可能比等待中的线程优先获取锁。
    是随机或者按照其他优先级排序
    调度开销 较高,因为需要维护严格的队列顺序。 较低,因为减少了线程切换的开销。
    吞吐量 可能较低,尤其在锁持有时间短的场景。 通常较高,因为减少了线程挂起和唤醒的次数。
    饥饿可能性 几乎不会出现,每个线程都能按序获得锁。 存在可能,等待中的线程可能长时间无法获取锁。
    • 公平锁:适用于需要保证线程执行顺序的场景,比如资源分配、定时任务执行等。
    • 非公平锁:适合对吞吐量要求高、锁持有时间短的场景,像大多数的 RPC 调用、缓存更新操作等。

    为什么默认是非公平锁?

    非公平锁性能更高,因为省去了检查队列的开销。大多数场景下,线程的执行顺序对结果无影响,此时非公平锁的吞吐量更好。

  4. 可重入性实现

    ReentrantLock通过 AQS 的state变量记录锁的重入次数:

    • state为 0 时,表示锁未被任何线程持有。

    • state大于 0 时,表示锁已被持有,且数值等于当前线程的重入次数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      protected final boolean tryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      if (c == 0) {
      // 锁未被持有,尝试获取
      if (compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current);
      return true;
      }
      }
      else if (current == getExclusiveOwnerThread()) {
      // 当前线程已持有锁,增加重入次数
      int nextc = c + acquires;
      if (nextc < 0) // overflow
      throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
      }
      return false;
      }
  5. 锁释放流程

    释放锁时,每次调用unlock()会减少state的值:

    • state减为 0 时,表示锁完全释放,唤醒队列中的下一个线程。
    • 如果state仍大于 0,表示锁仍被当前线程持有(重入未完全释放)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
    throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
    free = true;
    setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
    }

说说Java中synchronized的实现原理

在 Java 里,synchronized关键字的主要作用是实现线程同步,保证同一时刻只有一个线程能够访问被它修饰的代码块或者方法。

synchronized 是基于Java对象头**(Object Header)**和 **Monitor**(监视器,也称为管程)实现的线程同步机制。使用的时候不用手动去 lock 和 unlock,JVM 会自动加锁和解锁。

  • 对象头(Mark Word):在对象的头部,有一部分空间用于存储锁的状态信息,比如偏向锁、轻量级锁、重量级锁这些标记。
  • Monitor(监视器):这是 Java 对象所拥有的一个内置锁,其实现依赖于底层操作系统的互斥量(Mutex)。
  1. Monitor(监视器)机制

    Monitor 是 Java 中实现同步的基础,每个对象在内存中都有一个对象头——Mark Word,用于存储锁的状态,以及 Monitor 对象的指针。当一个线程尝试访问被 synchronized 修饰的代码块或方法时:

    • 获取锁:线程必须先获得对象的 Monitor。如果 Monitor 已被其他线程持有,则当前线程会被阻塞,进入 Monitor 的等待队列。
    • 释放锁:持有 Monitor 的线程执行完同步代码后释放锁,唤醒等待队列中的其他线程竞争锁。

    底层实现
    Monitor 依赖于操作系统的互斥量(Mutex)实现,这涉及用户态与内核态的切换,因此重量级锁的性能开销较大。

    在 Hotspot 虚拟机中,MonitorObjectMonitor 实现,其核心结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ObjectMonitor {
    _header = NULL; // Mark Word 副本
    _count = 0; // 重入次数
    _waiters = 0; // 等待线程数
    _recursions = 0; // 锁重入次数
    _owner = NULL; // 持有锁的线程
    _WaitSet = NULL; // 等待队列(wait() 的线程)
    _cxq = NULL; // 竞争队列
    _EntryList = NULL; // 阻塞队列
    }
  2. 对象头(Object Header):锁的存储基础

    Java 对象在内存中分为三部分:

    • 对象头(Header):存储锁状态、GC 分代年龄、哈希码等。
    • 实例数据(Instance Data):对象的字段值。
    • 对齐填充(Padding):补齐内存对齐。

    其中,对象头中的 Mark Word 存储了锁的状态信息。

    Mark Word 结构

    • Mark Word 是一个动态数据结构,根据锁状态不同存储不同信息。以 64 位 JVM 为例:
      • 无锁状态Mark Word存储对象哈希码、CG分代信息。
      • 偏向锁Mark Word存储偏向线程 ID、时间戳等。
      • 轻量级锁Mark Word存储线程栈帧中的锁记录指针。
      • 重量级锁Mark Word存储指向 Monitor 对象的指针。
  3. 锁升级过程

    JDK 6 之后引入锁升级机制,避免直接使用重量级锁,优化了性能:

    偏向锁(Biased Locking)

    • 适用场景

      只有一个线程访问同步块。

    • 原理
      当第一个线程获取锁时,会在 Mark Word 中记录该线程的 ID(偏向锁状态)。此后该线程再次进入同步块时,无需任何同步操作,直接获取锁。

    • 优点

      无竞争下几乎无开销。

    • 升级触发

      当其他线程尝试竞争该锁时,偏向锁升级为轻量级锁。

    轻量级锁(Lightweight Locking)

    • 适用场景

      多个线程交替访问同步块(无竞争)。

    • 原理:

      • 线程在进入同步块前,会在栈帧中创建用于存储锁记录(Lock Record)的空间。
      • 然后通过 CAS(Compare-and-Swap)操作,尝试将对象头的 Mark Word 复制到锁记录中,并将对象头指向锁记录的地址。
      • 如果 CAS 操作成功,线程就获取到了锁;若失败,则表示有其他线程正在持有锁,当前线程会进行自旋等待,避免上下文切换。
    • 升级触发

      当多个线程同时竞争锁时(CAS 失败多次),轻量级锁升级为重量级锁。

    重量级锁(Heavyweight Lock)

    • 适用场景

      多个线程同时竞争锁

    • 原理
      Mark Word 指向操作系统的 Monitor 对象,竞争失败的线程会被阻塞(进入内核态),锁释放后需唤醒线程(从内核态恢复)。

    • 缺点

      线程阻塞和唤醒的开销大。

总结
synchronized 的核心原理是通过对象头的 Mark Word 标记锁状态,并利用 Monitor 实现线程同步。锁升级机制(偏向锁 → 轻量级锁 → 重量级锁)在不同竞争程度下平衡了性能和安全性,使得它在大多数场景下成为首选的同步方式。

说说synchronizedReentrantLock的区别

特性 synchronized ReentrantLock
实现方式 JVM 内置关键字 JUC 包下的类
锁的释放 自动释放 手动释放
公平性 非公平锁 支持公平 / 非公平锁
可中断性 不可中断 可中断
超时机制 不支持 支持
锁状态判断 不支持 支持
性能 优化后接近 ReentrantLock 竞争激烈时更灵活
  • 优先使用 synchronized,因为它简洁易用,且 JVM 对其进行了优化。
  • 当需要高级锁特性(如可中断锁、公平锁、超时机制)时,选择 ReentrantLock

什么是线程死锁

两个或多个线程在执行过程中,由于互相持有对方需要的资源(通常是锁),并无限期地等待对方释放资源,从而导致所有线程都无法继续执行下去的一种永久性阻塞状态。

死锁发生的条件是什么?如何解决?

  • 死锁产生的 4 个必要条件(缺一不可)

    1. 互斥条件:资源(如锁)具有排他性,同一时间只能被一个线程持有(例如synchronized锁、ReentrantLock都是互斥的)。
    2. 持有并等待条件:线程持有至少一个资源,同时又在等待获取其他线程持有的资源。
    3. 不可剥夺条件:线程已持有的资源不能被其他线程强制剥夺,只能由线程主动释放(例如锁只能由持有者主动释放)。
    4. 循环等待条件:多个线程形成闭环等待链,每个线程都在等待下一个线程持有的资源(例如线程 1 等线程 2 的资源,线程 2 等线程 1 的资源)。
  • 如何避免死锁?

    • 破坏 “循环等待” 条件

      给所有资源定义统一的获取顺序,所有线程必须按顺序获取资源。例如:规定 “必须先获取 LOCK_A,再获取 LOCK_B”,避免交叉获取。

    • 破坏 “持有并等待” 条件

      线程获取资源时,一次性获取所有所需资源(若获取不全则释放已持有的资源,重新尝试)。

    • 破坏 “不可剥夺” 条件

      使用可中断的锁(如ReentrantLocktryLock(timeout)),若超时未获取到锁,则主动释放已持有的资源。

    • 减少锁的持有时间

      同步块只包含必要的代码,避免长时间持有锁,降低交叉等待的概率。

如何检测死锁?

  • 在 Linux 生产环境中

    可以先使用 top ps 等命令查看进程状态,看看是否有进程占用了过多的资源。

  • 使用 JDK 自带工具(生产环境常用)

    使用 jps -l 查看当前进程,然后使用 jstack 进程号 查看当前进程的线程堆栈信息,看看是否有线程在等待锁资源。

  • 图形化工具:jconsoleVisualVM

    可视化工具,查看线程状态和死锁。

并发工具类

什么是Java的CountDownLatch?

CountDownLatch 是 Java 并发包(java.util.concurrent)中的一个同步工具类,用于让一个或多个线程等待其他线程完成操作后再继续执行。它通过一个计数器实现,初始时设置计数器值,线程完成操作后调用 countDown() 方法递减计数器,等待线程通过 await() 方法阻塞,直到计数器值为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
	int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount);

// 创建并启动3个工作线程
for (int i = 0; i < workerCount; i++) {
final int taskId = i;
new Thread(() -> {
try {
System.out.println("任务" + taskId + "开始执行");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("任务" + taskId + "执行完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 任务完成,计数器减1
}
}).start();
}

// 主线程等待所有工作线程完成
latch.await();
System.out.println("所有任务已完成,主线程继续执行");
//执行结果
//任务0开始执行
//任务1开始执行
//任务2开始执行
//任务0执行完成
//任务2执行完成
//任务1执行完成
//所有任务已完成,主线程继续执行

什么是Java的CyclicBarrier

CyclicBarrier 是 Java 并发包(java.util.concurrent)中的一个同步工具类,用于让一组线程在到达某个屏障点(Barrier)时相互等待,直到所有线程都到达该点后,再继续执行后续操作。与 CountDownLatch 不同,CyclicBarrier 的计数器可以循环使用(重置后可再次等待),因此适用于需要多轮协作的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 创建一个屏障,等待3个线程
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有人都到齐了,一起出发!");
});

// 启动3个线程(模拟3个人)
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+"到餐厅了,等待其他人...");
barrier.await(); // 等待其他线程

System.out.println(Thread.currentThread().getName()+"到电影院了,等待其他人...");
barrier.await(); // 可以循环使用,等待下一轮
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
//执行结果
//Thread-0到餐厅了,等待其他人...
//Thread-2到餐厅了,等待其他人...
//Thread-1到餐厅了,等待其他人...
//所有人都到齐了,一起出发!
//Thread-2到电影院了,等待其他人...
//Thread-0到电影院了,等待其他人...
//Thread-1到电影院了,等待其他人...
//所有人都到齐了,一起出发!

CountDownLatchCyclicBarrier的区别

特性 CountDownLatch CyclicBarrier
使用次数 计数器减到 0 后无法重置,只能用一次。 支持循环使用(通过 reset() 方法)。
计数方向 递减(countDown) 递增(await)
等待方向 主线程等待子线程 线程互相等待
重置功能 ❌ 不可重置 ✅ 可重置
回调功能 ❌ 无 ✅ 全员到齐后执行指定任务
适用场景 启动前初始化,结束前收尾 计算任务拆分,所有线程都到达后才能继续
异常处理 计数不受影响,需要处理 InterruptedException 需要处理 BrokenBarrierException(如某个线程中断导致屏障破坏)。
  • CyclicBarrier:像团队爬山,所有人在每个山顶等待,到齐后一起出发下一段路,可重复使用。
  • CountDownLatch:像火箭发射倒计时,各部门完成准备后计数减 1,倒计时结束后发射(且只发射一次)。

什么是Java的Semaphore?

Semaphore(信号量)是 Java 并发包(java.util.concurrent)中的一个同步工具类,用于控制同时访问某个资源的线程数量。它通过维护一个许可证(permit)计数器,线程在访问资源前必须先获取许可证,使用完后释放许可证,从而限制并发访问的线程上限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
// 先获取许可证
semaphore.acquire();
// 成功获取后再打印日志
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println(Thread.currentThread().getName() + "离开车位");
semaphore.release();
}
}, String.valueOf(i)).start();
}
//执行结果
//1抢到车位
//3抢到车位
//2抢到车位
//2离开车位
//1离开车位
//3离开车位
//4抢到车位
//5抢到车位
//6抢到车位
//4离开车位
//5离开车位
//6离开车位

什么是Java的Exchanger

Exchanger 是 Java 并发包(java.util.concurrent)中的一个同步工具类,用于两个线程之间交换数据。它允许两个线程在某个同步点互相交换各自持有的数据,当一个线程到达 exchange() 方法时,会阻塞直到另一个线程也到达该方法,然后两者交换数据并继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.Exchanger;

public class ProducerConsumer {
private static final Exchanger<String> EXCHANGER = new Exchanger<>();

public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
try {
String data = "生产的数据";
System.out.println("生产者:准备交换数据 " + data);
String receivedData = EXCHANGER.exchange(data);
System.out.println("生产者:收到消费者的数据 " + receivedData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

// 消费者线程
Thread consumer = new Thread(() -> {
try {
String data = "反馈信息";
System.out.println("消费者:准备交换数据 " + data);
String receivedData = EXCHANGER.exchange(data);
System.out.println("消费者:收到生产者的数据 " + receivedData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

producer.start();
consumer.start();
}
}

线程池

什么是线程池

线程池是一种 线程复用机制,通过预先创建/管理线程集合,避免频繁线程创建销毁带来的性能开销,实现任务的并发执行。

如何创建线程池

  • 手动创建 ThreadPoolExecutor(推荐,更安全可控)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    2,
    5,
    2,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.DiscardPolicy()
    );

    try {
    for (int i = 0; i < 9; i++) {
    threadPool.execute(()->{
    System.out.println(Thread.currentThread().getName()+"=>执行业务");
    });
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    threadPool.shutdown();
    }
  • 通过 Executors 工厂类创建(简单快捷)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建一个固定大小的线程池
    try {
    for (int i = 0; i < 4; i++) {
    threadPool.execute(()->{
    System.out.println(Thread.currentThread().getName()+"=>执行业务");
    });
    }
    } catch (Exception e) {
    throw new RuntimeException(e);
    } finally {
    threadPool.shutdown();
    }

线程池的原理

graph TD
  A[提交任务] --> B(核心线程<br>是否空闲?):::decision
  B -->|是| C[立即执行]:::process
  B -->|否| D(任务队列<br>是否已满?):::decision
  D -->|否| E[加入队列等待]:::process
  D -->|是| F(线程数<br><最大线程数?):::decision
  F -->|是| G[创建临时线程执行]:::process
  F -->|否| H[触发拒绝策略]:::process
  
  classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
  classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

线程池常见的参数有哪些?

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
  1. corePoolSize(核心线程数)

    • 作用:线程池长期保持的最小线程数(即使空闲也不会被销毁,除非开启 allowCoreThreadTimeOut)。
    • 配置建议
      • CPU 密集型任务(如计算):设为 CPU 核心数 + 1(减少线程切换开销)。
      • IO 密集型任务(如网络 / DB 操作):设为 2 × CPU 核心数(利用等待时间并行处理)。
      • 混合型任务:(线程等待时间/线程CPU时间 + 1) * CPU核数
  2. maximumPoolSize(最大线程数)

    • 作用:线程池允许创建的最大线程数量(包括核心线程和临时线程)
    • 关键规则
      • 任务数 > corePoolSize工作队列已满 时,才会创建临时线程
      • 临时线程数量 = maximumPoolSize - corePoolSize
    • 配置建议:通常设为 corePoolSize * 2,但需考虑系统资源上限
  3. keepAliveTime(空闲线程存活时间)

    • 作用临时线程空闲时的存活时间,超时后会被回收,需配合 TimeUnit 参数使用
    • 默认值60 秒。
  4. unit(时间单位)

    • 作用:指定 keepAliveTime 的时间单位

    • 常用值

      1
      2
      3
      TimeUnit.SECONDS  // 秒
      TimeUnit.MILLISECONDS // 毫秒
      TimeUnit.MINUTES // 分钟
  5. workQueue(任务队列)

    • 作用:缓存待执行任务的队列,核心线程忙时任务进入队列等待。

    • 常见实现类

      队列类型 容量特性 适用场景 线程池默认使用
      ArrayBlockingQueue 有界 任务量可控、需限制内存 手动配置(如自定义线程池)
      LinkedBlockingQueue 无界(默认) 任务处理快、需避免任务被拒 FixedThreadPoolSingleThreadExecutor
      SynchronousQueue 容量 0 任务处理极快、需动态线程数 CachedThreadPool
      PriorityBlockingQueue 无界 任务需优先级排序 需手动配置(如自定义线程池)
      DelayQueue 无界 定时任务、延迟执行 ScheduledThreadPool
  6. threadFactory(线程工厂)

    • 作用:自定义线程创建逻辑(如命名线程、设置优先级、守护线程等)。
    • 默认值Executors.defaultThreadFactory()(创建的线程名为 pool-1-thread-1 格式)。
    • 场景:建议自定义工厂(如 new ThreadFactory() { ... }),便于日志追踪和问题排查。
  7. handler(拒绝策略)

    • 作用:当线程数=maxPoolSize且队列已满时,对新任务的拒绝策略。

    • 默认值AbortPolicy(抛异常 RejectedExecutionException)。

    • 常见策略:

      策略类 行为
      AbortPolicy(默认) 抛出 RejectedExecutionException 异常
      CallerRunsPolicy 由提交任务的线程直接执行该任务(同步执行)
      DiscardPolicy 静默丢弃新任务,不抛异常
      DiscardOldestPolicy 丢弃队列中最旧的任务,然后重试提交

线程池的常用的阻塞队列有哪些?

  • **ArrayBlockingQueue**(有界阻塞队列)

    • 底层由数组实现,容量一旦创建,就不能修改。
    • 适用场景:需要严格控制队列长度,防止资源耗尽(如高并发限流)。
    • 在线程池中: 当队列满时,提交新任务会触发线程池的拒绝策略。
  • **LinkedBlockingQueue**(默认无界,也可指定容量)

    • 底层由链表实现,默认大小为 Integer.MAX_VALUE (无界易引发OOM),可指定为有界队列
    • 适用场景:
      • 有界: 需要控制任务数量,但任务量波动较大。
      • 无界: 任务到达速度通常不会长时间远高于处理速度,且能容忍队列暂时增长(需警惕 OOM)。newFixedThreadPoolnewSingleThreadExecutor 默认使用无界 LinkedBlockingQueue
    • 在线程池中:
      • 有界: 队列满时触发拒绝策略。
      • 无界: 理论上队列永远不会满(除非 OOM),因此提交任务通常不会触发拒绝策略(除非线程池关闭或饱和策略有其他限制)。
  • **SynchronousQueue**(同步队列)

    • 没有容量的阻塞队列是一种没有容量的阻塞队列,生产者插入任务时必须等待消费者取出。它直接传递任务,避免任务排队,适合快速响应的场景,但对线程池的扩缩容更敏感。
    • 任务处理速度快:适合需要快速响应的场景。
    • 创建线程无限制:如**Executors.newCachedThreadPool()**默认使用此队列,配合 Integer.MAX_VALUE 最大线程数。
    • 在线程池中:
      • 如果没有空闲工作线程且当前线程数未达核心线程数,会创建新线程。
      • 如果没有空闲工作线程且当前线程数已达到核心线程数(但小于最大线程数),会创建新线程。
      • 如果没有空闲工作线程且当前线程数已达最大线程数,则提交任务会立即触发拒绝策略(因为队列无法存放任何任务)。这使得 SynchronousQueue 能非常快速地响应系统过载。
  • **DelayedWorkQueue**(延迟队列)

    • 实现: 基于 PriorityQueue 实现的延时近似无界阻塞队列。

    • 特性:

    • 延时获取: 元素必须实现 Delayed 接口。只有在其指定的延迟时间到期后,元素才能被取出。

      • 近似无界: 容量自动增长。
    • 无界风险: OOM 风险。

    • 适用场景: 需要延迟执行任务的场景,如定时任务调度、缓存过期失效等。

  • 在线程池中:

    • 通常需要配合自定义的线程池使用(如 ScheduledThreadPoolExecutor 内部使用了类似 DelayQueue 的机制)。提交任务时,即使队列未满,任务也不会立即被执行,而是等到其延迟到期。队列满不是主要问题(近似无界),但需注意 OOM。
  • **PriorityBlockingQueue**(支持优先级排序)

    • 实现: 支持优先级排序的近似无界阻塞队列。
    • 特性:
      • 优先级: 元素必须实现 Comparable 接口,或者在构造队列时提供 Comparator。队列按照优先级顺序出队(优先级最高的先出队)。
      • 近似无界: 容量自动增长(受限于内存),所以 put 操作通常不会阻塞(除非资源耗尽)。
      • 无界风险: 同样有 OOM 风险。
      • 非 FIFO: 出队顺序由优先级决定。
    • 适用场景: 需要根据任务优先级而非提交顺序来执行任务的场景。
    • 在线程池中: 由于近似无界,提交任务通常不会因队列满而触发拒绝策略(优先级低的可能会长时间等待)。

线程池的拒绝策略有哪些?

当线程池无法接受新的任务时,也就是线程数达到 maximumPoolSize,任务队列也满了的时候,就会触发拒绝策略。

  • AbortPolicy(默认):直接抛出 RejectedExecutionException 异常,拒绝新任务。

  • CallerRunsPolicy:由提交任务的线程(如主线程)直接执行被拒任务。

  • DiscardPolicy:静默丢弃被拒任务,无异常、无日志。

  • DiscardOldestPolicy :移除队列中最旧的任务(队列头部),重试提交新任务。

  • 自定义拒绝策略:默认策略不能满足需求,可以通过实现 RejectedExecutionHandler 接口来定义自己的淘汰策略。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class MyRejectedExecution implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    System.out.println("Task " + r.toString() + " rejected. Queue size: " + executor.getQueue().size());
    }
    }

    public static void main(String[] args) {

    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    2,
    5,
    2,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),
    Executors.defaultThreadFactory(),
    new MyRejectedExecution()
    );

    try {
    for (int i = 0; i < 9; i++) {
    threadPool.execute(()->{
    System.out.println(Thread.currentThread().getName()+"=>办理业务");
    });
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    threadPool.shutdown();
    }
    }

如何选择拒绝策略

需要结合队列来选择

  • 有界队列:常用 AbortPolicy(抛异常)或 CallerRunsPolicy(让提交线程执行任务)。
  • 无界队列:理论上不会触发拒绝策略(但需防OOM)。
  • SynchronousQueue:建议用 DiscardPolicy 或自定义策略避免阻塞提交线程。

有哪几种常见的线程池

  • FixedThreadPool(固定线程数线程池)
    • 特点
      核心线程数 = 最大线程数(通过Executors.newFixedThreadPool(n)n指定),线程池中的线程数量固定不变。
      使用无界阻塞队列LinkedBlockingQueue 存储等待任务。
      线程空闲时不会被回收(核心线程数 = 最大线程数,且无超时机制)。
    • 适用场景
      适用于任务数量已知、任务执行时间较长且稳定的场景(如服务器接收固定数量的并发请求)。
  • CachedThreadPool(可缓存线程池)
    • 特点
      核心线程数 = 0,最大线程数 =Integer.MAX_VALUE(理论上无限)。
      使用**同步队列SynchronousQueue**(不存储任务,直接传递给线程)。
      线程空闲 60 秒后会被自动回收,因此空闲时几乎不占用资源。
    • 适用场景
      适用于任务数量多但执行时间短、突发且临时的任务(如临时批量处理短任务)。
      缺点:最大线程数过大,若任务执行时间过长,可能创建大量线程导致资源耗尽。
  • SingleThreadExecutor(单线程线程池)
    • 特点
      核心线程数 = 1,最大线程数 = 1,仅用一个线程执行所有任务。
      使用**无界阻塞队列LinkedBlockingQueue**,任务按提交顺序串行执行。
      若唯一线程因异常终止,会自动创建新线程替代。
    • 适用场景
      适用于需要任务串行执行(如日志写入、顺序处理数据)或避免并发冲突的场景。
  • ScheduledThreadPool(定时任务线程池)
    • 特点
      核心线程数固定(通过Executors.newScheduledThreadPool(n)n指定),最大线程数 = Integer.MAX_VALUE
      使用**延迟队列DelayedWorkQueue**(支持定时 / 周期性执行)。
      支持定时(schedule())或周期性(scheduleAtFixedRate())执行任务。
    • 适用场景
      适用于需要定时执行(如延迟 3 秒后执行)或周期性执行(如每隔 1 分钟执行一次)的任务(如定时备份、心跳检测)。

为什么不建议使用Executors去创建线程池

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

  • 1)newFixedThreadPool(n)newSingleThreadExecutor():队列无界,可能导致 OOM

    这两种线程池的核心线程数 = 最大线程数(固定大小),工作队列使用 LinkedBlockingQueue,而该队列的默认容量是 Integer.MAX_VALUE(约 20 亿)。当任务提交速度远快于线程处理速度时,任务会在队列中无限堆积,占用大量内存,最终导致 OOM(内存溢出)

  • 2)newCachedThreadPool():线程数无界,可能导致资源耗尽

    该线程池的核心线程数为 0,最大线程数为 Integer.MAX_VALUE,线程空闲 60 秒后销毁,工作队列使用 SynchronousQueue(不存储任务,直接提交给线程)。

    当任务提交量激增时,线程池会无限创建新线程(理论上可达 20 亿)。过多的线程会导致:

    • 大量 CPU 时间用于线程上下文切换,系统吞吐量骤降;
    • 线程占用的内存(如栈空间)急剧增加,最终触发 OOM。
  • 3) newScheduledThreadPool(n):存在线程数或队列的潜在问题

    本质是支持定时任务的线程池,其核心线程数固定,但工作队列类似 DelayQueue,若任务调度不当(如大量长期未执行的定时任务),也可能导致队列堆积和资源耗尽。

线程池的生命周期

  • RUNNING(运行中)

    • 初始状态:线程池创建后默认进入此状态。

    • 行为:

      • 能接受新任务(execute() 方法正常提交)。
      • 能处理阻塞队列中已存在的任务。
  • SHUTDOWN(关闭中)

    • 触发条件:调用 shutdown() 方法。

    • 行为:

      • 不接受新任务(新提交的任务会被拒绝策略处理)。
      • 继续处理阻塞队列中已存在的任务,直到队列清空。
    • 转换:当队列中所有任务处理完毕,且工作线程数量为 0 时,进入 TIDYING 状态。

  • STOP(停止中)

    • 触发条件:调用 shutdownNow() 方法。

    • 行为:

      • 不接受新任务
      • 立即中断正在执行的任务(通过 Thread.interrupt())。
      • 清空阻塞队列(未执行的任务被移除并返回)。
    • 转换:当所有工作线程都已中断(数量为 0)时,进入 TIDYING 状态。

  • TIDYING(整理中)

    • 触发条件:

      • SHUTDOWN 转换:队列清空 + 工作线程数量为 0。
      • STOP 转换:工作线程数量为 0。
    • 行为:

      • 线程池进入 “整理” 状态,此时会执行 terminated() 钩子方法(默认空实现,可重写用于资源清理)。
  • TERMINATED(已终止)

    • 触发条件terminated() 方法执行完毕。
    • 行为:线程池生命周期结束,无法再执行任何任务。
方法 作用 状态转换触发
shutdown() 平缓关闭:不接受新任务,处理完队列任务 RUNNING → SHUTDOWN
shutdownNow() 强制关闭:中断任务,清空队列 RUNNING → STOP
isShutdown() 判断是否处于 SHUTDOWN/STOP 状态 -
isTerminated() 判断是否处于 TERMINATED 状态 -
awaitTermination() 阻塞等待线程池进入 TERMINATED 状态 -

线程池中shutdown (),shutdownNow()有什么区别

方法 任务处理 线程状态 响应中断
shutdown() 已提交的任务会继续执行(包括队列中等待的任务) 不再接受新任务,线程池状态变为 SHUTDOWN 不中断正在执行的线程
shutdownNow() 尝试终止所有活跃线程,未执行的任务返回 立即终止,线程池状态变为 STOP 中断正在执行的线程(通过 Thread.interrupt()