八股文笔记 #3 Java 并发

再好的项目,也敌不过 HashMap 的 resize 过程没讲清楚

多线程

1# Java 的线程和操作系统的线程一样吗?

是的,Java 中的线程本质上与操作系统的线程是一致的

Java 线程

在现代 JVM(如 HotSpot)中,Java 使用的是 一对一的线程模型,即每创建一个 Java 线程,JVM 底层都会调用操作系统的 API(如 Linux 下的 pthread_create)来创建一个 原生线程

因此,Java 线程与操作系统线程是 一一对应 的,拥有独立的调用栈、程序计数器等资源,由操作系统负责调度和管理

这种设计使得 Java 能够充分利用多核 CPU 的并发能力,同时也意味着 Java 线程的性能和行为,受到操作系统线程调度策略的影响

2# 使用多线程要注意哪些问题?

在使用多线程编程时,最关键的是确保线程安全,避免由于数据竞争导致的数据不一致、程序行为异常等问题

Java 中的线程安全主要体现在以下三个方面:

  1. 原子性:原子性保证某个操作在执行过程中不会被其他线程打断,即同一时刻只能有一个线程对共享数据进行修改。可以通过以下方式保障原子性
    • 使用 synchronized 关键字对代码块或方法加锁
    • 使用 java.util.concurrent.atomic 包下的原子类(如 AtomicIntegerAtomicReference)来进行原子操作
  2. 可见性:可见性指的是一个线程对共享变量的修改能够及时地被其他线程看到。 Java 中通过以下机制保证可见性
    • synchronized 隐含地保证了进入和退出临界区时的内存刷新
    • volatile 关键字用于标记变量,确保对该变量的读写不会被线程本地缓存,从而强制线程从主内存读取
  3. 有序性:在多线程环境下,Java 编译器和 CPU 可能对指令进行重排序以优化性能,导致线程间看到的执行顺序不一致。Java 使用happens-before 原则来建立线程间操作的执行顺序关系,确保必要的有序性

3# 保证数据的一致性有哪些方案呢?

在并发编程或分布式系统中,确保数据一致性是至关重要的。以下是常见的几种实现方案:

  1. 事务管理:事务遵循 ACID 原则,通过数据库事务机制,确保一组操作要么全部执行成功并提交,要么全部失败并回滚,从而保证数据在操作过程中的一致性

    • 原子性:事务中的所有操作要么全部执行,要么全部不执行

    • 一致性:事务执行前后,数据库始终处于一致的状态

    • 隔离性:多个事务之间的操作互不干扰

    • 持久性:事务一旦提交,数据变更就是永久性的

  2. 锁机制:锁机制是一种悲观并发控制策略,适合冲突概率较高的场景。通过加锁可以控制多个线程对共享资源的访问,避免并发读写导致的数据不一致问题。可以使用 Java 的 synchronized 关键字、ReentrantLock 等显示锁或者读写锁(ReadWriteLock)等

  3. 版本控制(乐观锁):乐观锁适用于读多写少、冲突较少的场景,可以有效提高系统的吞吐量。通过乐观锁的方式,在更新数据时记录数据的版本信息,从而避免同时对同一数据进行修改,进而保证数据的一致性

4# 线程的创建方式有哪些?

太长不看版

方式 关键点 优点 局限
继承 Thread 重写 run(),调用 start() 编写简单;this 即当前线程 已继承 Thread,无法再继承其他类,线程与任务代码强耦合
实现 Runnable 实现 run(),包装进 Thread 可继承其它父类;多个线程可共享同一任务对象 需用 Thread.currentThread() 取当前线程;无返回值
实现 Callable + FutureTask 实现 call(),包装成 FutureTask 再交给 Thread 任务可返回结果,可抛异常;多个线程可共享同一任务对象 代码略复杂;同样需 Thread.currentThread()
线程池(ExecutorService 任务提交给池;池内部复用线程 复用线程、限制并发、便于管理 参数配置不当易导致资源耗尽或死锁;调优、排错复杂
  1. 继承 Thread:这是最直接的一种方式,用户自定义类继承 java.lang.Thread 类,需要重写 run() 方法,run() 方法中定义了线程执行的具体任务。创建该类的实例后,通过调用 start() 方法启动线程

    1
    2
    3
    4
    5
    6
    7
    class MyThread extends Thread {
    @Override public void run() { /* 任务逻辑 */ }
    }

    public static void main(String[] args) {
    new MyThread().start();
    }
    • 优点:编写简单,如果需要访问当前线程,无需使用 Thread.currentThread() 方法,直接使用 this,即可获得当前线程
    • 缺点:因为线程类已经继承了 Thread 类,所以不能再继承其他的父类
  2. 实现 Runnable:如果一个类已经继承了其他类,就不能再继承 Thread 类,此时可以实现 java.lang.Runnable 接口,需要重写 run() 方法,然后将此 Runnable 对象作为参数传递给 Thread 类的构造器,调用 start() 方法启动线程

    1
    2
    3
    4
    5
    6
    7
    class MyTask implements Runnable {
    @Override public void run() { /* 任务逻辑 */ }
    }

    public static void main(String[] args) {
    new Thread(new MyTask()).start();
    }
    • 优点:线程类只是实现了 Runable 接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU 代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想
    • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用 Thread.currentThread() 方法
  3. 实现 Callable 接口 + FutureTaskjava.util.concurrent.Callable 接口类似于 Runnable,但 Callablecall() 方法可以有返回值并且可以抛出异常。要执行 Callable 任务,需将它包装进一个 FutureTask,因为 Thread 类的构造器只接受 Runnable 参数,而 FutureTask 实现了 Runnable 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyCall implements Callable<Integer> {
    @Override public Integer call() throws Exception { return 1; }
    }

    FutureTask<Integer> ft = new FutureTask<>(new MyCall());
    new Thread(ft).start();
    try {
    Integer result = ft.get(); // 阻塞拿结果
    } catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
    }
    • 缺点:编程稍微复杂,如果需要访问当前线程,必须调用 Thread.currentThread() 方法
    • 优点:线程只是实现 Runnable 或实现 Callable 接口,还可以继承其他类。这种方式下,多个线程可以共享一个 target 对象,非常适合多线程处理同一份资源的情形
  4. 使用线程池(Executor 框架):从 Java 5 开始引入的 java.util.concurrent.ExecutorService 和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销。可以通过 Executors 类的静态方法创建不同类型的线程池

    1
    2
    3
    4
    5
    ExecutorService pool = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 100; i++) {
    pool.submit(() -> {/* 任务逻辑 */});
    }
    pool.shutdown();
    • 缺点:程池增加了程序的复杂度,特别是当涉及线程池参数调整和故障排查时。错误的配置可能导致死锁、资源耗尽等问题,这些问题的诊断和修复可能较为复杂

    • 优点:线程池可以重用预先创建的线程,避免了线程创建和销毁的开销,显著提高了程序的性能。对于需要快速响应的并发请求,线程池可以迅速提供线程来处理任务,减少等待时间。并且,线程池能够有效控制运行的线程数量,防止因创建过多线程导致的系统资源耗尽(如内存溢出)。通过合理配置线程池大小,可以最大化 CPU 利用率和系统吞吐量

5# 如何启动线程 ?

在 Java 中,启动线程的标准方式是调用 Thread 类的 start() 方法。该方法会通知 JVM 去创建一个新的线程,并由系统调度线程执行 run() 方法中定义的任务逻辑

1
2
3
4
5
6
// 创建两个线程,并使用 start() 启动它们
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

myThread1.start(); // 启动线程1
myThread2.start(); // 启动线程2

6# 如何停止线程?

停止线程应遵循 协作式 原则,既让线程 自行结束,又确保资源得到正确释放

太长不看版

# 停止方式 简要说明 场景适配性
1 volatile 标志位控制循环 主线程控制标志,工作线程轮询判断是否退出 简单计算任务
2 使用 interrupt() + 判断中断状态 发送中断信号,由线程检查 isInterrupted() 响应退出 阻塞/非阻塞任务
3 可中断阻塞操作(sleep() / wait() / join() 阻塞时响应中断,捕获异常后安全退出 阻塞型任务
4 使用 return 提前结束 run() 方法 可配合中断或标志位,在逻辑判断处直接返回退出 控制逻辑明确
5 使用线程池 + Future.cancel(true) 在线程池中通过 Future 中断任务 线程池管理的任务
6 关闭底层资源解除不可中断阻塞 关闭资源触发异常,中断无法生效的情况专用 I/O、Socket 阻塞
7 Thread.stop()(已废弃) 强制终止,风险极高,不建议使用 禁止使用
  1. 使用 volatile 标志位控制循环退出:通过定义共享的 volatile 布尔变量,线程在 run() 中轮询该变量判断是否继续执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class SafeStopFlag implements Runnable {
    private volatile boolean running = true;

    @Override public void run() {
    while (running) {
    // 执行任务
    }
    System.out.println("Thread exiting safely.");
    }

    public void stop() { running = false; }
    }
  2. 使用 interrupt() + 判断中断状态:主线程调用 interrupt() 设置中断标志,线程通过 isInterrupted() 判断是否终止

    1
    2
    3
    4
    5
    6
    7
    8
    class InterruptCheck implements Runnable {
    @Override public void run() {
    while (!Thread.currentThread().isInterrupted()) {
    // 正常执行逻辑
    }
    System.out.println("Thread interrupted, exiting.");
    }
    }
  3. 处理中断异常:可中断阻塞操作(如 sleep/wait/join):调用 interrupt() 后,线程在阻塞方法中抛出 InterruptedException,在异常处理中结束线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class SleepInterruptDemo extends Thread {
    @Override public void run() {
    try {
    Thread.sleep(5000); // 阻塞
    } catch (InterruptedException e) {
    System.out.println("Interrupted during sleep.");
    Thread.currentThread().interrupt(); // 恢复中断状态
    }
    }
    }
  4. 使用 return 提前结束 run() 方法:配合中断标志或其他条件判断,在满足某条件时通过 return 直接结束 run() 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class ReturnEarlyDemo implements Runnable {
    @Override public void run() {
    while (true) {
    if (Thread.currentThread().isInterrupted()) {
    return; // 提前退出
    }
    // 执行任务逻辑
    }
    }
    }
  5. 线程池 + Future.cancel(true) 中断任务:将任务提交给线程池,通过 Future.cancel(true) 向线程发送中断信号来终止执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<?> future = executor.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
    // 执行任务
    }
    });
    Thread.sleep(2000);
    future.cancel(true); // 触发中断
    executor.shutdown();
  6. 关闭资源解除不可中断阻塞(如 Socket.accept()):对于不能响应中断的阻塞方法,通过关闭相关底层资源(如 ServerSocket)使其抛出异常退出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class SocketTask implements Runnable {
    private final ServerSocket server;

    SocketTask(ServerSocket server) { this.server = server; }

    @Override public void run() {
    try {
    while (!Thread.currentThread().isInterrupted()) {
    Socket s = server.accept(); // 阻塞不可中断
    // 处理 socket
    }
    } catch (IOException e) {
    if (Thread.currentThread().isInterrupted()) {
    System.out.println("Interrupted while blocked.");
    }
    }
    }

    public void stop() throws IOException {
    server.close(); // 触发 accept() 抛异常
    }
    }
  7. 不推荐:使用 Thread.stop() 强制终止线程(已废弃):直接停止线程执行,不会进行资源清理,存在严重安全隐患

    1
    2
    3
    4
    5
    Thread t = new Thread(() -> {
    while (true) { /* 永久循环 */ }
    });
    t.start();
    t.stop(); // 已废弃,可能导致数据不一致

7# interrupt() 是如何让线程抛出异常的?

在 Java 中,每个线程都关联一个布尔类型的中断标志位,用于表示线程是否被中断。该标志的初始值为 false,可以通过调用线程的 interrupt() 方法将其设为 true

当其他线程调用某个线程的 interrupt() 方法时,具体表现分为两种情况:

  • 如果线程处于阻塞状态:如果调用了 Thread.sleep()Thread.join()Object.wait() 等方法,则调用 interrupt() 会立即中断阻塞状态,并在该线程中抛出 InterruptedException 异常。此机制允许线程及时响应中断请求,跳出阻塞操作并执行善后处理

  • 如果线程没有处于阻塞状态:此时调用 interrupt() 并不会抛出异常,仅仅是将中断标志设置为 true。线程可以在运行过程中自行检查中断状态,例如通过

    1
    Thread.currentThread().isInterrupted();

    1
    if (Thread.interrupted()) { ... }

    来自行决定是否退出当前任务,进行资源释放或中断处理

8# Java 线程的状态有哪些?

Java 线程状态变迁

Java 中的线程状态由 java.lang.Thread.State 枚举类定义,总共有六种状态。你可以通过线程对象的 getState() 方法来获取当前线程所处的状态

线程状态 描述
NEW 新建状态:线程对象已创建,但尚未调用 start() 方法,尚未启动执行
RUNNABLE 可运行状态:线程已调用 start(),处于就绪状态正在运行
BLOCKED 阻塞状态:线程尝试获取某个对象的监视器锁时被阻塞,等待锁的释放
WAITING 等待状态:线程无限期等待另一个线程执行某个操作(如 notify()
TIMED_WAITING 超时等待状态:线程在指定时间内等待另一个线程的操作(如 sleep()join(long)
TERMINATED 终止状态:线程执行完毕或被异常中止,生命周期结束

补充说明

  • RUNNABLE 包含两种实际状态
    • 就绪(Ready):已准备好等待 CPU 分配时间片
    • 运行中(Running):正在由 CPU 执行
  • BLOCKEDWAITINGTIMED_WAITING 都表示线程处于非运行态,但它们的等待条件不同,需结合具体方法(如 synchronizedwait()sleep() 等)来区分

9# sleep() 和 wait() 的区别是什么?

特性 sleep() wait()
所属类 Thread(静态方法) Object(实例方法)
是否释放锁 不释放锁 会释放锁
使用前提 可在任何线程上下文中调用 必须在同步块或同步方法中调用(即持有对象锁)
唤醒方式 到达指定时间自动唤醒 需要其他线程调用 notify() / notifyAll() 或超时
设计目的 使线程暂停指定时间,不涉及线程协作 用于线程之间通信与协作,主动释放锁进入等待队列

一、类归属不同

  • sleep()Thread 类的静态方法,通过 Thread.sleep() 调用,无需依赖对象实例
  • wait()Object 类的实例方法,必须通过对象调用,例如 obj.wait()

二、是否释放锁

  • sleep() 不释放任何锁,即使在同步代码块中调用,线程休眠期间仍然占有锁,其他线程无法访问同步资源
  • wait() 会释放当前线程持有的对象锁,使其他线程可以获得该锁并继续执行

三、使用条件

  • sleep() 可在任意代码位置调用,无需持有任何锁
  • wait() 只能在同步块或同步方法中调用(即必须先获取该对象的监视器锁),否则会抛出 IllegalMonitorStateException 异常

四、唤醒机制

  • sleep() 会在指定时间后自动唤醒,回到可运行状态等待 CPU 调度
  • wait() 必须依赖 notify()notifyAll() 显式唤醒,或设置超时时间(如 wait(5000))后自动唤醒

10# sleep() 会释放 CPU 吗?

是的,调用 Thread.sleep() 时,线程会释放 CPU 使用权,但不会释放它所持有的锁

当线程执行 Thread.sleep() 方法时,它会进入 TIMED_WAITING(计时等待)状态,暂停执行指定的时间;在这段时间内,线程主动让出 CPU 时间片,操作系统会进行线程调度,将 CPU 分配给其他处于可运行状态的线程;这使得其他线程可以获得 CPU 并执行任务,从而提高系统的并发性能

虽然线程暂停执行,但不会释放它已持有的同步锁(例如通过 synchronized 获取的锁)。如果其他线程尝试获取这些锁,仍会被阻塞,直到当前线程恢复执行并释放锁

11# BLOCKED 和 WAITING 有什么区别?

Java 中线程的 BLOCKEDWAITING 状态都表示线程当前没有运行,但两者产生的原因和恢复机制完全不同,主要区别如下:

线程竞争锁后的状态
  1. 触发条件
    • BLOCKED(阻塞状态):当线程尝试获取某个对象的锁(例如进入一个 synchronized 块或方法)而该锁已被其他线程占用时,线程会进入 BLOCKED 状态。此状态下线程在等待锁的释放,无法参与锁竞争,也不会执行任何指令
    • WAITING(等待状态):当线程主动调用Object.wait()Thread.join()LockSupport.park() 方法时,会进入 WAITING 状态。此时线程进入无限期等待,线程将不会消耗 CPU 资源,并且不会参与锁的竞争
  2. 唤醒机制
    • BLOCKED 状态的线程会在持有锁的线程释放锁后,自动进入就绪(RUNNABLE)状态,并参与下一轮的锁竞争
    • WAITING 状态的线程不会自动唤醒,必须由其他线程显式地调用 notify()notifyAll()unpark() 等方法,或者使用 join() 等触发条件满足后,才会恢复运行

总结

特性 BLOCKED 状态 WAITING 状态
触发方式 被动触发,等待锁的释放 主动调用等待方法(如 wait()join()
是否释放锁 ❌ 不释放 ✅ 调用 wait() 时会释放当前锁
唤醒机制 自动唤醒(当锁可用时) 必须显式唤醒(如 notify())或条件满足
是否参与锁竞争 ❌ 不参与 ❌ 不参与,直到被唤醒
  • BLOCKED 是因锁竞争失败被动进入的状态,WAITING 是线程主动进入等待的状态
  • BLOCKED 的唤醒是自动的WAITING 的唤醒是手动触发的

12# 线程处于 WAITING 状态时,如何恢复为 RUNNING 状态?

核心机制是通过外部事件触发或资源可用性变化,比如其他线程调用 notify()notifyAll() 唤醒它

1
2
3
4
5
6
7
8
9
10
// 等待线程
synchronized (lock) {
lock.wait(); // 当前线程进入 WAITING 状态,并释放锁
}

// 唤醒线程
synchronized (lock) {
lock.notify(); // 唤醒一个等待该锁的线程
// lock.notifyAll(); // 唤醒所有在该锁上等待的线程
}

说明

  • notify() 会随机唤醒一个正在该对象上等待的线程
  • notifyAll() 会唤醒所有在该对象上等待的线程(通常用于多个线程等待同一条件的场景)
  • 被唤醒的线程会重新尝试获取锁,一旦成功获取锁,就会从 WAITING 状态转为 RUNNABLE,最终进入 RUNNING 状态继续执行
  • 如果唤醒后竞争锁失败,线程会暂时进入 BLOCKED 状态,直到成功获取锁

13# notify() 和 notifyAll() 的区别?

两者都用于唤醒因调用 wait() 而处于等待状态的线程,但在唤醒数量和行为上存在显著差异:

  • 共同点
    • 都只能在同步代码块或同步方法中调用
    • 都会将等待中的线程从 WAITING 状态变为 BLOCKED,接着尝试竞争锁资源
    • 被唤醒的线程只有在重新获得锁后才能继续执行
  • 区别
    • notify()只唤醒一个正在等待当前对象锁的线程,具体唤醒哪个线程不可控。如果被唤醒的线程没有在运行过程中再次调用 notify()notifyAll(),其他线程将继续处于等待状态,可能导致线程 “永久等待” 的问题(死锁风险)
    • notifyAll()唤醒所有等待当前对象锁的线程。所有被唤醒的线程将同时进入阻塞状态(BLOCKED),并竞争锁资源。最终只有一个线程获得锁并进入运行状态,其余线程继续等待锁,避免了遗漏唤醒的情况
  • 推荐使用场景
    • 当只有一个线程需要被唤醒时,可使用 notify()
    • 当可能有多个线程等待,且不能确定具体唤醒哪一个为最佳时,应使用 notifyAll(),以防遗漏唤醒导致系统阻塞

14# notify() 唤醒的是哪个线程?

从语义上讲,notify() 唤醒的是任意一个正在等待该对象锁的线程,具体唤醒哪一个线程,不在 Java 语言层面定义,而是由 JVM 的实现来决定的

虽然规范中声明唤醒是 “非确定性” 的,但主流 JVM 实现(如 HotSpot)在内部的实际处理方式却并非真正随机:在 HotSpot JVM 中,notify() 通常按照**等待队列中的先后顺序(FIFO)**来唤醒线程,也就是说,先进入等待队列的线程,通常会先被唤醒

15# 线程间通信方式有哪些?

太长不看版

方案 适用场景 优点 注意点 / 限制
volatile / 共享变量 极简标志位、写少读多 语法简单 无法做复杂同步;需防 busy‑wait
wait/notify 经典线程协作 JDK 原生;释放锁 必须同步块;易误用
Lock + Condition 多条件、更高灵活度 可多队列;响应中断 需显式加/解锁
BlockingQueue 生产‑消费模型、任务队列 封装好并发控制;易用 需选合适容量及实现
  1. 共享变量(volatile / synchronized:多线程读写同一变量完成信息传递

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class SharedVarDemo {
    private static volatile boolean flag = false; // 保证可见性

    public static void main(String[] args) {
    new Thread(() -> { // 生产者
    sleep(2);
    flag = true;
    System.out.println("Producer → flag=true");
    }).start();

    new Thread(() -> { // 消费者
    while (!flag) { /* busy‑wait */ }
    System.out.println("Consumer ← flag=true");
    }).start();
    }
    private static void sleep(int s) { try { Thread.sleep(s * 1000); } catch (InterruptedException ignored) {} }
    }
    • volatile → 可见性;若需原子更新,可配合 synchronized 或原子类
    • 缺点:忙等浪费 CPU;适合极简单的状态标志
  2. wait/notify:线程在对象监视器上等待并释放锁,唤醒后再次竞争锁

    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
    class WaitNotifyDemo {
    private static final Object lock = new Object();
    private static boolean ready = false;

    public static void main(String[] args) {
    // 消费者
    new Thread(() -> {
    synchronized (lock) {
    while (!ready) { // 避免虚假唤醒
    try { lock.wait(); } catch (InterruptedException ignored) {}
    }
    System.out.println("Consumer ← ready");
    }
    }).start();

    // 生产者
    new Thread(() -> {
    sleep(2);
    synchronized (lock) {
    ready = true;
    lock.notify(); // 或 notifyAll()
    System.out.println("Producer → notify");
    }
    }).start();
    }
    private static void sleep(int s) { try { Thread.sleep(s * 1000); } catch (InterruptedException ignored) {} }
    }
    • 必须在同步块/同步方法内调用,否则抛 IllegalMonitorStateException

    • 建议 while 循环重判条件,防止虚假唤醒

  3. Lock + ConditionCondition 提供与 wait/notify 等价但更灵活的 API,可配多条件队列

    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
    class LockConditionDemo {
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition done = lock.newCondition();
    private static boolean ready = false;

    public static void main(String[] args) {
    // 消费者
    new Thread(() -> {
    lock.lock();
    try {
    while (!ready) done.await();
    System.out.println("Consumer ← ready");
    } catch (InterruptedException ignored) {
    } finally { lock.unlock(); }
    }).start();

    // 生产者
    new Thread(() -> {
    sleep(2);
    lock.lock();
    try {
    ready = true;
    done.signal(); // 或 signalAll()
    System.out.println("Producer → signal");
    } finally { lock.unlock(); }
    }).start();
    }
    private static void sleep(int s) { try { Thread.sleep(s * 1000); } catch (InterruptedException ignored) {} }
    }
    • wait/notify 相比,可针对不同条件建多条等待队列,更易管理
  4. BlockingQueue:使用 线程安全 的阻塞队列负责生产/消费,完全无需显式加锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class BlockingQueueDemo {
    private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);

    public static void main(String[] args) {
    // 消费者
    new Thread(() -> {
    try {
    int item = queue.take(); // 阻塞直到有元素
    System.out.println("Consumer ← " + item);
    } catch (InterruptedException ignored) {}
    }).start();

    // 生产者
    new Thread(() -> {
    sleep(2);
    try {
    queue.put(1); // 队列满则阻塞
    System.out.println("Producer → 1");
    } catch (InterruptedException ignored) {}
    }).start();
    }
    private static void sleep(int s) { try { Thread.sleep(s * 1000); } catch (InterruptedException ignored) {} }
    }
    • 阻塞队列内部已处理好并发与等待逻辑,是最推荐的生产‑消费实现。

    • 常见实现:ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue

线程安全

1# juc 包下你常用的类有哪些?

一、线程池相关

  • ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求
  • Executors:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如 newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)等,方便开发者快速创建线程池

二、并发集合类

  • ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。它采用了分段锁等技术,允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的 Hashtable 性能更好
  • CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景

三、同步工具类

  • CountDownLatch:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用 countDown() 方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景
  • CyclicBarrier:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与 CountDownLatch 不同的是,CyclicBarrier 可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景
  • Semaphore:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等

四、原子类

  • AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便
  • AtomicReference:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景

2# 怎么保证多线程安全?

  1. synchronized 关键字:通过内置锁实现互斥访问,Synchronized 可用于修饰方法或代码块,确保同一时刻只有一个线程能访问同步区域。其本质是基于对象的监视器锁(monitor)机制
  2. volatile 关键字:确保变量的可见性,使用 volatile 修饰的变量不会被线程缓存,所有线程访问的都是主内存中的最新值。适用于状态标志等无需原子性但需要可见性的场景
  3. 显式锁 Lock 接口与 ReentrantLock:更灵活的同步控制,ReentrantLock 是一种可重入互斥锁,提供了 tryLock()lockInterruptibly() 等高级控制方式,相较 synchronized 更灵活
  4. 原子类(AtomicInteger 等):原子性操作无需加锁,java.util.concurrent.atomic 包提供了多种原子类,支持无锁线程安全操作,适用于高并发计数、状态标志等场景
  5. 线程局部变量(ThreadLocal:每个线程独享变量副本,ThreadLocal 为每个线程提供独立的变量副本,避免了共享带来的同步问题,适合线程封闭(Thread Confinement)场景
  6. 并发集合类:内置线程安全机制,使用 java.util.concurrent 包中的并发集合(如 ConcurrentHashMapConcurrentLinkedQueue 等)可以避免手动加锁,提升并发性能
  7. JUC 工具类:协调线程间协作,Java 并发包提供了一系列同步工具类,用于线程间的通信与协作控制,如:Semaphore(限制并发线程数)、CountDownLatch(等待多个线程完成任务)、CyclicBarrier(多线程在屏障点统一继续执行)、Exchanger(线程间交换数据)

3# Java 中有哪些常用的锁?各自适用于什么场景?

在多线程编程中,锁是保障线程安全、避免并发冲突的核心机制

  1. 内置锁(synchronized):最基本的互斥机制,Synchronized 是 Java 内置的同步机制,用于修饰方法或代码块。线程在进入同步区域前需要获取锁,离开后自动释放,保证同一时间只有一个线程能访问被保护的资源。其中,syncronized 加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁则涉及操作系统级的互斥锁
  2. 可重入锁(ReentrantLock:功能更强的显式锁,ReentrantLockjava.util.concurrent.locks 包中的显式锁,功能上比 synchronized 更灵活,支持可中断锁获取、限时等待锁、公平锁/非公平锁切换、多条件变量(Condition)支持等。ReentrantLock 使用 lock()unlock() 方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿
  3. 读写锁(ReadWriteLock):提高读多写少场景的并发性,ReadWriteLock 允许多个线程并发读取共享资源,但写操作是独占的。常见实现如 ReentrantReadWriteLock
  4. 乐观锁和悲观锁悲观锁(Pessimistic Locking):默认认为并发冲突频繁,访问数据前先加锁,如 synchronizedReentrantLock乐观锁(Optimistic Locking):假设冲突很少,不加锁而是通过版本号或时间戳在更新时校验数据是否被修改,常用 CAS 机制实现
  5. 自旋锁(SpinLock):短时等待情况下的非阻塞锁机制,自旋锁不会让线程阻塞,而是持续循环尝试获取锁(通常基于 CAS 操作)。适合锁竞争时间非常短的场景,避免线程频繁上下文切换

4# 怎么在实践中用锁的?

太长不看版

锁类型 使用方式示例 优点 适用场景
synchronized synchronized 方法或代码块 简单易用、JVM 自动管理 一般同步逻辑
ReentrantLock lock() / unlock() 可中断、公平锁、尝试锁等高级功能 对同步控制要求更高的场景
ReadWriteLock readLock() / writeLock() 多读并发、写独占、提升读多写少性能 缓存、配置读取等并发读取场景
  1. 使用 synchronized 实现简单互斥synchronized 是 Java 中最早的同步机制,可用于方法或代码块,隐式地为指定对象加锁,保证同一时间只有一个线程执行被保护的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Counter {
    private final Object lock = new Object();
    private int count = 0;

    // 同步代码块
    public void increment() {
    synchronized ( ) {
    count++;
    }
    }

    // 同步方法
    public synchronized int getCount() {
    return count;
    }
    }

    使用代码块可以灵活控制加锁粒度,并避免同步整个方法带来的性能损耗

  2. 使用 Lock 接口实现显式加锁Lock 接口提供了比 synchronized 更强大的锁控制能力,如可中断锁限时尝试锁公平锁等。ReentrantLock 是最常用的实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
    lock.lock(); // 显式加锁
    try {
    count++;
    } finally {
    lock.unlock(); // 确保锁最终释放,避免死锁
    }
    }
    }

    try-finally 块是使用 Lock 的规范做法,以确保即使抛出异常也能正确释放锁

  3. 使用 ReadWriteLock 提升读多写少的并发性能ReadWriteLock 提供一对互斥锁:读锁(readLock()写锁(writeLock()。它允许多个线程同时读取数据,但写操作必须是独

  4. 占的。典型实现为 ReentrantReadWriteLock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Cache {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();
    private Object data;

    public Object readData() {
    readLock.lock();
    try {
    return data;
    } finally {
    readLock.unlock();
    }
    }

    public void writeData(Object newData) {
    writeLock.lock();
    try {
    data = newData;
    } finally {
    writeLock.unlock();
    }
    }
    }

    多个线程可以同时读取 data,而写操作则是互斥的,这种设计大幅提升了读多写少场景的并发性能

5# Java 并发工具你知道哪些?

Java 的并发工具类大多位于 java.util.concurrent 包中,提供了丰富的 API 来简化并发编程。这里主要提供代码参考,它们的详细概念见 [1# juc 包下你常用的类有哪些?](#1# juc 包下你常用的类有哪些?)

  1. CountDownLatch 是一个同步辅助类,它允许一个或多个线程等待其他线程完成一组操作。它通过一个倒计时计数器来协调线程之间的执行顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
    int threadCount = 3;
    CountDownLatch latch = new CountDownLatch(threadCount);

    for (int i = 0; i < threadCount; i++) {
    new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + " 执行任务");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    latch.countDown(); // 任务完成,计数器减一
    System.out.println(Thread.currentThread().getName() + " 完成任务");
    }, "Worker-" + i).start();
    }

    System.out.println("主线程等待所有子线程完成...");
    latch.await(); // 阻塞直到计数器为 0
    System.out.println("所有子线程已完成,主线程继续执行");
    }
    }
  2. CyclicBarrier 允许一组线程互相等待,直到所有线程都到达屏障点(barrier)为止,然后一同继续执行。它可以循环使用,适合分阶段并行计算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class CyclicBarrierExample {
    public static void main(String[] args) {
    int threadCount = 3;
    CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
    System.out.println("所有线程已到达屏障,执行后续操作");
    });

    for (int i = 0; i < threadCount; i++) {
    new Thread(() -> {
    try {
    System.out.println(Thread.currentThread().getName() + " 执行任务");
    Thread.sleep(1000);
    barrier.await(); // 等待其他线程
    System.out.println(Thread.currentThread().getName() + " 通过屏障");
    } catch (Exception e) {
    e.printStackTrace();
    }
    }, "Worker-" + i).start();
    }
    }
    }
  3. Semaphore 是一个计数信号量,用于控制同时访问某资源的线程数量。可用于流量控制、资源池等场景

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class SemaphoreExample {
    public static void main(String[] args) {
    Semaphore semaphore = new Semaphore(2); // 同时允许两个线程访问

    for (int i = 0; i < 5; i++) {
    new Thread(() -> {
    try {
    semaphore.acquire(); // 获取许可
    System.out.println(Thread.currentThread().getName() + " 获得许可");
    Thread.sleep(2000); // 模拟处理资源
    System.out.println(Thread.currentThread().getName() + " 释放许可");
    semaphore.release(); // 释放许可
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }, "Worker-" + i).start();
    }
    }
    }
  4. Callable 接口类似于 Runnable,但可以返回结果或抛出异常。Future 表示异步任务的结果,用于查询任务是否完成、取消任务或获取结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class FutureCallableExample {
    public static void main(String[] args) throws Exception {
    ExecutorService executor = Executors.newSingleThreadExecutor();

    Callable<Integer> task = () -> {
    System.out.println(Thread.currentThread().getName() + " 执行任务");
    Thread.sleep(2000);
    return 42;
    };

    Future<Integer> future = executor.submit(task);
    System.out.println("主线程继续执行其他任务");

    Integer result = future.get(); // 阻塞直到结果返回
    System.out.println("异步任务结果: " + result);

    executor.shutdown();
    }
    }
  5. ConcurrentHashMap 是高并发环境下的线程安全哈希表。它在内部对桶进行分段锁处理,支持高效的并发读写操作,适用于读多写少或高并发环境

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ConcurrentHashMapExample {
    public static void main(String[] args) {
    ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

    map.put("key1", 1);
    map.put("key2", 2);

    // 并发读操作
    map.forEach((key, value) -> System.out.println(key + ": " + value));

    // 并发写操作
    map.computeIfAbsent("key3", k -> 3);
    }
    }

6# CountDownLatch 是做什么的?

CountDownLatch 是 Java 并发包(java.util.concurrent)中的一个同步辅助类,用于让一个或多个线程等待其他线程完成任务后再继续执行

它的核心机制是一个计数器(Counter),常用于多线程协作场景,例如主线程等待多个子线程就绪、任务分批执行等,工作原理:

  1. 初始化计数器:创建 CountDownLatch 时指定一个初始计数值(如 N
  2. 等待阻塞:调用 await() 方法的线程会被阻塞,直到计数器变为 0
  3. 计数递减:其他线程完成任务后调用 countDown() 方法,使计数器减 1
  4. 唤醒等待线程:当计数器归零,所有调用了 await() 的线程将被唤醒,继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainThreadWaitExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);

for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
Thread.sleep(1000); // 模拟耗时操作
latch.countDown(); // 当前线程完成,计数器减 1
System.out.println(Thread.currentThread().getName() + " 完成任务");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Worker-" + i).start();
}

System.out.println("主线程等待所有子线程完成任务...");
latch.await(); // 主线程阻塞,直到所有子线程完成
System.out.println("所有任务已完成,主线程继续执行");
}
}

7# synchronized 的工作原理?

synchronized 的工作原理

  1. 编译层:生成字节码指令

    • 在同步代码块(或同步方法)前后,编译器会插入 monitorenter / monitorexit 指令
    • 这两条指令由 JVM 调用 对象监视器(Monitor) 完成加锁与解锁
  2. 运行时:Monitor 的加锁 / 解锁流程

    步骤 说明
    ① 线程执行 monitorenter 尝试获取对象监视器: • 若对象未被锁定当前线程已持有锁⇒ 计数器 +1,线程继续执行; • 否则将线程加入 EntryList,进入 BLOCKED 状态,等待锁可用。
    ② 线程执行 monitorexit 将计数器 -1;当计数器变为 0 时: • 释放锁; • 从 EntryList 取出一个线程(或按公平策略)尝试获取锁。

    Wait / Notify 机制

    • 当线程在同步块内调用 wait():释放锁,计数器 -1,自身进入 WaitSet
    • 其他线程执行 notify() / notifyAll() 会将 WaitSet 中的线程移动到 EntryList,再次参与锁竞争
  3. 内存语义

    • 加锁:先 清空当前线程工作内存 中共享变量,再从主内存读取,保证后续操作能看到最新值
    • 解锁:将工作内存中被修改的共享变量 刷新回主内存,保证其他线程可见
  4. 锁的性质与性能影响

    • synchronized 属于 排他锁(互斥锁),同一时刻只能有一个线程持有
    • Java 线程与 OS 原生线程 1:1 对应,阻塞 / 唤醒需要从用户态切换到内核态,成本较高
    • JVM 为降低开销引入 偏向锁、轻量级锁、重量级锁 的分级优化策略,尽量在用户态完成自旋或 CAS 竞争,仅在竞争激烈时才升级为重量级锁

总结

synchronized 通过 对象监视器 保证了:

  • 原子性:同一时刻只有一个线程进入同步块
  • 可见性 & 有序性:锁的释放与获取隐式包含内存屏障,满足 happens‑before 规则

在实际编码中,只需使用 synchronized 关键字即可享受上述语义;理解其底层机制能帮助我们在性能调优、死锁排查等场景中更有针对性地分析问题

8# 除了用 synchronized,还有哪些方式可以实现线程同步?

  1. 使用 ReentrantLockReentrantLock 是一个可重入的互斥锁,相比 synchronized 提供了更灵活的锁定和解锁操作。它还支持公平锁和非公平锁,以及可以响应中断的锁获取操作
  2. 使用volatile关键字:虽然 volatile 不是一种锁机制,但它可以确保变量的可见性。当一个变量被声明为 volatile 后,线程将直接从主内存中读取该变量的值,这样就能保证线程间变量的可见性。但它不具备原子性
  3. 使用Atomic:Java 提供了一系列的原子类,例如 AtomicIntegerAtomicLongAtomicReference 等,用于实现对单个变量的原子操作,这些类在实现细节上利用了 CAS(Compare-And-Swap)算法,可以用来实现无锁的线程安全

9# synchronized 锁静态方法与普通方法的区别

  1. 锁的对象不同

    • 普通方法:锁的是当前对象实例(this)。同一个对象实例的多个线程访问同步方法时会互斥;而不同实例对象之间互不干扰,可以并发执行各自的同步方法

    • 静态方法:锁的是当前类的 Class 对象(ClassName.class)。JVM 中每个类对应唯一一个 Class 对象,因此无论多少个实例,访问静态同步方法时都竞争同一把锁

  2. 作用范围不同

    • 普通方法:仅对同一对象实例的同步方法调用互斥,不同实例的调用互不干扰

    • 静态方法:对整个类范围内的该静态方法调用都互斥,无论是哪个实例,甚至没有实例化对象时也是如此

  3. 多实例场景影响不同

    • 普通方法:如果多个线程访问不同对象实例的同步普通方法,由于锁对象不同,它们之间可以并发执行

    • 静态方法:而多个线程访问静态同步方法,即使是通过不同实例调用,也会串行执行,因为它们竞争的是同一个 Class 对象锁

10# 讲一下 synchronized 的锁升级过程

具体的锁升级的过程是:无锁->偏向锁->轻量级锁->重量级锁

  1. 无锁:在对象创建初期,处于无锁状态,此时对象的对象头(Mark Word)中未记录线程相关信息,如果 JVM 未开启偏向锁(或偏向锁延迟生效),线程进入同步块将直接尝试升级为轻量级锁
  2. 偏向锁:当对象第一次被某线程获取锁时,Mark Word 会记录该线程的 ID,之后该线程再次进入同步块时,只需比较 Mark Word 中的线程 ID 是否匹配,即可直接获取锁,无需 CAS 操作,也不会挂起线程;如果另一个线程尝试获取偏向锁,会触发 偏向锁撤销,撤销后锁升级为轻量级锁,同时偏向锁将不可再用于该对象(或类)
  3. 轻量级锁:适用于多线程存在竞争,但竞争不激烈的场景。当前线程在进入同步块时,在自己虚拟机栈中创建 锁记录(Lock Record),并将对象的 Mark Word 复制到锁记录中,然后尝试通过 CAS 将对象头中的 Mark Word 替换为指向这个锁记录的指针,如果 CAS 成功,表示线程获得锁;如果 CAS 失败,说明有其他线程竞争该锁 —— 进入自旋尝试再次获取,自旋多次仍无法获取,则升级为重量级锁
  4. 重量级锁:发生在多线程高竞争场景。无法通过自旋获取锁的线程将被 挂起(阻塞),由操作系统调度,等待唤醒,重量级锁的获取和释放涉及用户态与内核态切换,开销较大,但保证线程安全
synchronized 的锁升级过程
  • 线程 A 首次进入 synchronized 块:

    • 如果偏向锁已启用且对象未被锁定,则偏向 A,记录其线程 ID
    • A 再次进入同步代码时,直接命中偏向锁,无阻塞、无 CAS
  • 线程 B 也尝试获取锁:

    • 偏向锁被撤销,升级为轻量级锁

    • B 通过自旋尝试获取锁

    • 如果自旋失败,锁升级为重量级,B 被挂起等待唤醒

11# JVM 对 synchronized 的优化机制

  1. 锁膨胀synchronized 支持从无锁 → 偏向锁 → 轻量级锁 → 重量级锁的渐进式升级过程(也称“锁膨胀”):初始状态为无锁,当一个线程独占锁时,使用 偏向锁,几乎无性能开销。有线程竞争时升级为 轻量级锁,通过 CAS 和自旋尝试获取锁。多线程竞争激烈、自旋失败后,升级为 重量级锁,线程阻塞、由操作系统调度。这一机制极大减少了用户态与内核态之间的切换,提升了 synchronized 的性能

  2. 锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的

  3. 锁粗化:如果 JVM 发现某个线程在短时间内频繁进行加锁和解锁操作,则会将这些操作合并成一个更大粒度的锁范围

  4. 自适应自旋锁:如果锁在短时间内就能被释放,则自旋线程可以快速获得锁,避免线程挂起/恢复所造成的性能开销。若线程在之前的自旋中成功获取过锁,则可能增加下一次的自旋次数;若多次自旋失败,则减少自旋次数甚至不再自旋,直接阻塞

12# 怎么理解可重入锁?

可重入锁指的是:同一个线程在已经获取锁的情况下,仍然可以再次获取该锁而不会发生死锁。这种机制确保了在方法调用嵌套或递归时,线程不会因为已经持有的锁而被自己阻塞

1
2
3
4
5
6
7
public synchronized void outer() {
inner(); // inner 也是 synchronized 方法
}

public synchronized void inner() {
// do something
}

如果锁不具备可重入性,那么线程在调用 outer() 后再次尝试进入 inner() 时就会阻塞自己,造成死锁。而使用可重入锁机制,线程能够顺利重入,程序才能正常执行

ReentrantLockjava.util.concurrent.locks 包下的可重入互斥锁,其可重入机制的核心是锁持有线程记录和重入计数器

  • 当一个线程首次获取锁时:锁的持有线程被设置为当前线程,重入计数器设置为 1
  • 当该线程再次获取该锁时:如果判断当前线程已是锁的持有者,则只需将计数器加 1不会被阻塞
  • 当线程执行完毕,释放锁时:每释放一次锁,计数器减 1只有当计数器减为 0 时,锁才真正释放,其他线程才有机会获取

这种计数机制避免了重复阻塞和死锁问题

13# synchronized 支持重入吗?如何实现的?

是的,synchronized可重入锁。这意味着:同一个线程在已经持有某个对象锁的情况下,可以再次获取该对象的锁而不会被阻塞。例如,在一个 synchronized 方法中调用该对象的另一个 synchronized 方法,当前线程不会因为再次请求锁而被挂起,这正是可重入性的体现

如何实现?

synchronized 的底层实现依赖于 JVM 的监视器锁机制,而这些监视器锁通常是通过操作系统的 互斥量 实现的。其内部维护了两个核心状态:锁的持有线程和重入计数器

  1. 锁的获取流程

    • **当线程首次请求锁:**检查锁的状态,若当前锁未被持有(状态为 0),则通过 CAS操作设置当前线程为锁持有者,同时将锁状态设为 1

    • 当锁已被占用时:如果当前线程再次请求该锁(即线程 ID 与持有锁线程一致),表示是重入,允许请求成功,并将重入计数器加 1;否则,线程将进入阻塞状态,等待锁释放

      1. 锁的释放流程
      • 当线程执行完同步代码块或方法后,会释放锁
      • 对于可重入锁,每释放一次锁,重入计数器减 1,仅当计数器减到 0 时,锁才真正释放,其他线程才能竞争该锁

14# ReentrantLock 工作原理

ReentrantLock 的底层实现主要依赖于 AbstractQueuedSynchronizer(AQS)这个抽象类。AQS 是一个提供了基本同步机制的框架,其中包括了队列、状态值等

ReentrantLock 在 AQS 的基础上通过内部类 Sync 来实现具体的锁操作。不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑:

  • 可中断性: ReentrantLock 实现了可中断性,这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待。在底层,ReentrantLock 使用了与 LockSupport.park() 和 LockSupport.unpark() 相关的机制来实现可中断性

  • 设置超时时间: ReentrantLock 支持在尝试获取锁时设置超时时间,即等待一定时间后如果还未获得锁,则放弃锁的获取。这是通过内部的 tryAcquireNanos 方法来实现的

  • 公平锁和非公平锁: 在直接创建 ReentrantLock 对象时,默认情况下是非公平锁。公平锁是按照线程等待的顺序来获取锁,而非公平锁则允许多个线程在同一时刻竞争锁,不考虑它们申请锁的顺序。公平锁可以通过在创建 ReentrantLock 时传入 true 来设置,例如:

    1
    ReentrantLock fairLock = new ReentrantLock(true);
  • 多个条件变量: ReentrantLock 支持多个条件变量,每个条件变量可以与一个 ReentrantLock 关联。这使得线程可以更灵活地进行等待和唤醒操作,而不仅仅是基于对象监视器的 wait() 和 notify()。多个条件变量的实现依赖于 Condition 接口,例如:

    1
    2
    3
    4
    5
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    // 使用下面方法进行等待和唤醒
    condition.await();
    condition.signal();
  • 可重入性: ReentrantLock 支持可重入性,即同一个线程可以多次获得同一把锁,而不会造成死锁。这是通过内部的 holdCount 计数来实现的。当一个线程多次获取锁时,holdCount 递增,释放锁时递减,只有当 holdCount 为零时,其他线程才有机会获取锁

15# synchronized 和 ReentrantLock 的区别

synchronizedReentrantLock 都是 Java 中常用的可重入锁机制,用于实现线程间的互斥访问,但它们在语法、特性和底层实现等方面存在显著差异

维度 synchronized ReentrantLock
语法使用 关键字形式,可修饰方法或代码块 通过显式调用 lock()unlock() 进行加锁与释放,仅支持代码块
加锁/释放机制 自动加锁与释放,进入同步块即加锁,退出即释放 需手动加锁与释放,必须配合 try-finally 使用避免死锁
锁的公平性 默认非公平,无法更改 支持公平锁(先到先得)与非公平锁(默认),通过构造函数设置
中断响应能力 不支持中断,线程在等待锁时无法被中断 支持中断,如 lockInterruptibly() 可响应中断,适用于死锁恢复
条件变量支持 支持单一条件队列(即 wait() / notify() 支持多个条件变量,基于 Condition 对象,控制更精细
性能差异 在现代 JVM 中性能已大幅优化,轻量、适合简单场景 灵活性强,适用于高并发场景,但使用复杂度稍高
底层实现 基于 JVM 实现,使用对象的监视器锁(Monitor),生成 monitorenter/monitorexit 字节码 基于 AQS(AbstractQueuedSynchronizer)实现

16# 介绍一下 AQS

一、简介

AQS 是 Java 提供的一个用于构建锁和同步器的抽象类,位于 java.util.concurrent.locks 包中。它是并发包的核心组件之一,被广泛应用于构建如 ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLockFutureTask 等高级并发工具类

AQS 的核心思想是:如果共享资源空闲,则当前线程可直接获取锁;否则,线程需进入队列等待,直到被唤醒重新尝试获取锁。这个等待机制基于 CLH(Craig, Landin, and Hagersten)队列的变体 —— 一个 虚拟的双向 FIFO 队列。AQS 会将每个请求资源但未成功的线程封装成一个 Node 节点,并串联到队列中,形成有序等待

CLH 变体队列

二、AQS 的核心组成

AQS 的工作机制可以拆解为以下三大核心部分:

  1. 同步状态 state:AQS 通过一个 volatile int state 变量来表示当前的同步状态,在不同的同步器中,state 表示不同含义:在 ReentrantLock 中:表示锁的重入次数;在 Semaphore 中:表示剩余的许可证数量;在 CountDownLatch 中:表示计数器的剩余数量

    AQS 提供如下原子操作方法对 state 进行管理,这些操作基于底层的 Unsafe 类 和 CAS 机制:

    1
    2
    3
    getState()
    setState(int newState)
    compareAndSetState(int expect, int update)
  2. FIFO 同步队列(CLH 变体):当线程无法成功获取同步状态时,它会被封装为一个 Node 并加入到 AQS 内部维护的 FIFO 双向等待队列中。该队列保证了线程获取锁的先来先服务原则,同时用于线程的阻塞与唤醒管理

    AQS 就像一个“线程调度管家”,协调着所有竞争资源的线程:排队入列;等待唤醒;出队重新尝试获取锁

  3. 获取与释放的模板方法(需子类实现):AQS 通过模板方法设计模式,将核心的锁获取与释放逻辑交给具体的同步器子类实现。常用需要重写的方法包括:

    1
    2
    3
    4
    protected boolean tryAcquire(int arg)
    protected boolean tryRelease(int arg)
    protected boolean tryAcquireShared(int arg)
    protected boolean tryReleaseShared(int arg)
    • 获取操作:通常涉及 acquireacquireShared 方法,若获取失败,线程将进入等待队列
    • 释放操作:通常是 releasereleaseShared,当资源释放后,唤醒下一个等待的线程

    各个具体同步器类会根据自身语义实现这些方法,比如:ReentrantLock 实现了独占锁的获取与释放;CountDownLatch 实现了共享锁逻辑中的“倒计时”功能;Semaphore 控制并发线程数量,即许可数量管理

三、AQS 的职责总结

作用 描述
同步状态管理 通过 state 和 CAS 保证原子性操作
线程排队机制 构建 CLH 队列,管理线程排队与调度
模板方法扩展 子类通过重写 tryAcquire 等方法定制锁语义

17# CAS 与 AQS 的关系与区别

一、两者的区别

  1. CAS(Compare-And-Swap):CAS 是一种乐观锁机制,它通过对比内存中当前值和预期值是否一致,来决定是否更新数据。整个过程具备原子性,通常由底层硬件指令实现。CAS 的逻辑是:若内存地址 V 当前的值等于 A,则将 V 的值更新为 B;否则不做任何修改
  2. AQS(AbstractQueuedSynchronizer):AQS 是一个用于构建同步器的抽象框架,例如 ReentrantLockSemaphoreCountDownLatch 等都基于它实现。其核心机制包括:
    • 使用一个 volatile int state 表示同步状态
    • 使用一个变体的 CLH FIFO 队列 管理等待线程
    • 通过模板方法模式暴露出 tryAcquire()tryRelease() 等方法,供具体同步器实现
    • AQS 提供了如 acquire()release() 等通用同步操作,这些操作背后依赖状态的原子修改

二、两者的关系

CAS 是 AQS 实现的基础手段之一。在 AQS 内部,为了实现线程安全地更新共享状态(即 state 变量),大量使用了 CAS 操作。其关键联系如下:

功能 说明
状态修改 AQS 通过 CAS 原子地修改 state 值,避免加锁操作
获取资源 acquire() 操作中,线程首先使用 CAS 尝试获取资源(修改 state);如果失败,才进入阻塞队列
释放资源 release() 操作中,线程使用 CAS 安全地减少或重置 state,并唤醒下一个等待线程
保证并发安全 CAS 保证了 state 更新过程中的原子性,是整个 AQS 框架并发控制的关键保障

18# 如何用 AQS 实现一个可重入的公平锁?

AQS 实现一个可重入的公平锁的详细步骤:

  1. 继承 AbstractQueuedSynchronizer:创建一个内部类继承自 AbstractQueuedSynchronizer,重写 tryAcquiretryReleaseisHeldExclusively 等方法,这些方法将用于实现锁的获取、释放和判断锁是否被当前线程持有
  2. 实现可重入逻辑:在 tryAcquire 方法中,检查当前线程是否已经持有锁,如果是,则增加锁的持有次数(通过 state 变量);如果不是,尝试使用 CAS 操作来获取锁
  3. 实现公平性:在 tryAcquire 方法中,按照队列顺序来获取锁,即先检查等待队列中是否有线程在等待,如果有,当前线程必须进入队列等待,而不是直接竞争锁
  4. 创建锁的外部类:创建一个外部类,内部持有 AbstractQueuedSynchronizer 的子类对象,并提供 lockunlock 方法,这些方法将调用 AbstractQueuedSynchronizer 子类中的方法
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class FairReentrantLock {

/** 核心同步器,继承 AQS */
private static class Sync extends AbstractQueuedSynchronizer {

/* ========= 独占锁基础 ========= */

/** 当前线程是否占有锁 */
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}

/** 尝试获取锁(公平 + 可重入)*/
@Override
protected 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()) { // ★ 可重入
int next = c + acquires;
if (next < 0) throw new Error("lock count overflow");
setState(next);
return true;
}
return false; // 竞争失败
}

/** 尝试释放锁 */
@Override
protected boolean tryRelease(int releases) {
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();

int c = getState() - releases;
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 可重入计数减 1
return free; // true → 唤醒队列首线程
}

/* ========= 条件变量 ========= */

Condition newCondition() { // 每个锁可派生多个条件队列
return new ConditionObject();
}
}

/* ========= 对外 API ========= */

private final Sync sync = new Sync();

// 加锁方法
public void lock() { sync.acquire(1); }
// 解锁方法
public void unlock() { sync.release(1); }
// 判断当前线程是否持有锁
public boolean isHeldByMe() { return sync.isHeldExclusively(); }
// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
public Condition newCondition() { return sync.newCondition(); }
}

代码解释

内部类 Sync

  • isHeldExclusively:使用 getExclusiveOwnerThread 方法检查当前锁是否被当前线程持有
  • tryAcquire
    • 首先获取当前锁的状态 c
    • 如果 c 为 0,表示锁未被持有,此时进行公平性检查,通过 hasQueuedPredecessors 检查是否有前驱节点在等待队列中。如果没有,使用 compareAndSetState 尝试将状态设置为 acquires(通常为 1),并设置当前线程为锁的持有线程
    • 如果 c 不为 0,说明锁已被持有,检查是否为当前线程持有。如果是,增加锁的持有次数(可重入),但要防止溢出
  • tryRelease
    • 先将状态减 releases(通常为 1)
    • 检查当前线程是否为锁的持有线程,如果不是,抛出异常
    • 如果状态减为 0,说明锁被完全释放,将持有线程设为 null
  • newCondition:创建一个 ConditionObject 用于更复杂的同步操作,如等待 / 通知机制。

外部类 FairReentrantLock

  • lock 方法:调用 sync.acquire(1) 尝试获取锁
  • unlock 方法:调用 sync.release(1) 释放锁
  • isLocked 方法:调用 sync.isHeldExclusively 判断锁是否被当前线程持有
  • newCondition 方法:调用 sync.newCondition 提供条件变量

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
FairReentrantLock lock = new FairReentrantLock();
Condition condition = lock.newCondition();

lock.lock();
try {
// 临界区
while (!ready) // 条件不满足
condition.await(); // 原子释放锁 + 挂起
// ...
} finally {
lock.unlock();
}

19# ThreadLocal 的作用、结构、原理与潜在问题

ThreadLocal 是 Java 提供的一种线程本地变量机制,它为每个线程提供独立的变量副本,避免多线程共享变量导致的线程安全问题

一、ThreadLocal 的作用

  1. 线程隔离
    每个线程都维护自己的变量副本,线程之间互不影响,天然线程安全。常用于用户信息、数据库连接、事务上下文等场景

  2. 简化参数传递,降低耦合度
    不需要在方法间层层传递参数,便于在过滤器、拦截器等框架中保存线程上下文

  3. 提升性能
    避免加锁带来的开销,相比同步机制具有更优的性能,尤其适合高并发场景下的读操作

二、ThreadLocalMap 的内部结构

ThreadLocalMap
  • 实际由一个 Entry 数组 实现(本质是一个定制的 HashMap);
  • Entry 的 key 是 ThreadLocal 实例(使用弱引用 WeakReference)
  • Entry 的 value 是对应线程的变量副本
1
2
3
4
5
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
}
}

注意:虽然 key 是 WeakReference(GC 可回收),但 value 是强引用,这种设计容易引发内存泄漏问题:

  • 如果 ThreadLocal 对象被外部代码丢弃(无强引用指向),而没有调用 remove(),即使 key 被 GC 回收,但 value 仍在 ThreadLocalMap 中,Entry 不会被自动清除,若线程是线程池中的线程(不会很快销毁),就会导致该 value 无法回收,造成 内存泄漏

三、ThreadLocal 的工作原理

ThreadLocal 的实现依赖于 Thread 类中一个专属的 ThreadLocalMap 字段,每个 ThreadLocal 实例就是这个 Map 的 key,Map 中的 Value 就是当前线程对应的变量值

  • get() 方法:读取当前线程的 ThreadLocalMap,查找当前 ThreadLocal 作为 key 的 entry,找到则返回 value;找不到时,调用 initialValue() 初始化,存入 Map 并返回
  • set(value) 方法:将当前 ThreadLocal 作为 key,value 存入当前线程的 ThreadLocalMap
  • remove() 方法:移除当前线程中与该 ThreadLocal 实例关联的 entry,释放引用,防止内存泄漏

20# 悲观锁和乐观锁的区别?

在并发编程中,悲观锁乐观锁是两种常见的并发控制策略,它们的核心区别在于对资源竞争冲突的预期和处理方式不同

一、悲观锁

悲观锁对并发持谨慎态度,假设资源竞争一定会发生,因此在访问数据之前主动加锁,确保同一时间只有一个线程能访问该资源

悲观锁通过各种加锁机制实现,在操作数据之前即加锁,阻止其他线程并发访问。性能损耗较大,但冲突处理成本低,适合写操作频繁、冲突概率高或对一致性要求极高的业务场景(如金融、订单系统)


二、乐观锁

乐观锁采取 “乐观” 的态度,认为线程之间的资源竞争是偶发的,大多数情况下不会发生冲突,因此不会在操作前加锁

乐观锁采用 “先检查再更新” 的方式进行控制,一般通过 CAS 机制 实现,在读取数据时不加锁,在更新时,通过对比数据是否被修改来决定是否成功更新,如果检测到冲突(数据已被其他线程修改),则重试或失败。适合读多写少或并发量大但冲突概率低的业务场景(如计数器、缓存更新等)

21# Java 中实现乐观锁的常见方式?

实现方式 原理 场景适用 优点
CAS 原子操作 比较并交换(无锁) 高并发、无共享状态的并发控制 快速、无阻塞
版本号控制 基于值的一致性检测 ORM 框架、数据库并发更新 精确控制、简单可靠
时间戳控制 基于时间的一致性检测 对更新时序敏感的场景 直观、可追踪
  1. CAS 操作: CAS 是乐观锁的基础。Java 提供了 java.util.concurrent.atomic 包中的原子类(如 AtomicIntegerAtomicLong),这些类使用 CAS 操作实现了线程安全的原子操作,可以用来实现乐观锁
  2. 版本号控制:为共享资源维护一个“版本号”字段(或称 version / revision),在更新前后对比版本号是否一致
  3. 时间戳:使用数据的更新时间戳进行冲突检测。在更新数据时,在比较时间戳,如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败

22# CAS 有什么缺点?

  1. ABA 问题:CAS 更新的过程中,当读取到的值是 A,然后准备赋值的时候仍然是 A,但是实际上有可能 A 的值被改成了 B,然后又被改回了 A,这个 CAS 更新的漏洞就叫做 ABA。只是 ABA 的问题大部分场景下都不影响并发的最终效果。Java 中有 AtomicStampedReference 来解决这个问题,它加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新
  2. 循环时间长开销大:自旋 CAS 的方式如果长时间不成功,会给 CPU 带来很大的开销
  3. 只能保证一个共享变量的原子操作:CAS 只能保证单个变量的原子更新。对于多个变量的复合操作,CAS 无法直接确保整体的原子性,容易产生中间状态或数据不一致的问题。可以使用 AtomicReference 进行对象整体引用的原子替换,对于复杂的多变量一致性需求,仍需使用传统的同步机制(如 synchronizedReentrantLock

23# 为什么不能所有的锁都使用 CAS?

  1. 自旋重试开销大,浪费 CPU 资源:当 CAS 失败时,线程不会挂起,而是不断地进行重试,大量线程同时自旋,频繁占用 CPU,会导致系统负载升高,甚至出现性能下降
  2. 无法处理复杂的临界区逻辑:CAS 适合用于更新简单变量(如整数、引用)的场景,但不适合处理具有复杂临界区逻辑的同步问题(如多个变量同时更新、存在阻塞等待、必须保证某段代码原子执行)
  3. 缺乏阻塞机制,不适用于等待/唤醒模型:CAS 本身没有提供线程挂起与唤醒机制,一旦失败只能选择不停地重试

24# volatile 关键字有什么作用?

一、保证可见性

当一个变量被声明为 volatile 时,它保证了该变量对所有线程的可见性。一个线程对 volatile 变量的写操作,会立即刷新到主内存,其他线程在读取该变量时,总是直接从主内存中获取值,而不是从线程本地缓存中读取, 这样可以避免由于线程工作内存(缓存)不同步导致的数据不一致问题

二、禁止指令重排序)

volatile 可以通过插入内存屏障来禁止特定类型的指令重排序,从而在一定程度上保证有序性:

  • 写屏障:在对 volatile 变量写入之前插入。在 volatile 写之前的所有普通写操作在内存中都可见,防止这些写操作被重排序到 volatile 写之后
  • 读屏障:在读取 volatile 变量之后插入。防止后续普通读操作被提前到 volatile 读操作之前执行,确保读取的是最新数据
  • 写-读屏障:在 volatile 写之后和 volatile 读之间。volatile 写之前的所有写操作在 volatile 读之前对其他线程可见;同时防止 volatile 读之后的操作被重排到前面

25# 指令重排序的原理是什么?

为了提升程序运行效率,编译器CPU处理器通常会对指令执行顺序进行优化,称为指令重排序。这是一种在保证单线程语义不变的前提下,重新调整代码执行顺序的技术,它必须遵守以下两个核心原则:

  1. 单线程语义等价:重排序不能改变单线程环境下程序的执行结果。换句话说,从单线程的角度看,重排序前后程序的行为应保持一致

  2. **遵守数据依赖性约束:如果两条语句存在数据依赖关系(如读写同一个变量),它们的执行顺序不能被重排

    • 写后读(Write → Read)

    • 写后写(Write → Write)

    • 读后写(Read → Write)

所以重排序不会对单线程有影响,只会破坏多线程的执行语义

1
2
3
int a = 1;  // A
int b = 2; // B
int c = a + b; // C

在这个例子中:

  • C 依赖 AB,即它需要在 AB 之后执行
  • 因此,C 不能被重排序AB 之前
  • 但是 AB 之间没有依赖关系,它们的执行顺序可以被编译器或处理器重排

虽然在单线程中重排序不会影响最终结果,但在多线程环境下,不同线程看到的执行顺序可能不一致,从而引发不可预期的问题,尤其是涉及共享变量的读写操作时。因此,Java 提供了 volatile 关键字和 synchronized 等机制,来禁止特定的重排序,确保在多线程下的可见性和有序性

26# volatile 能保证线程安全吗?

volatile 关键字只能保证变量的可见性不能保证原子性,因此 并不能完全保证线程安全

  • volatile 并不能保证原子性(如 i++ 操作就不是原子的)
  • 如果需要保证原子性,应考虑使用 synchronized 或原子类(如 AtomicInteger
  • 它适用于状态标志类变量(如 stopFlagready 等),但不适合用于涉及复合操作(读-改-写)的逻辑

27# volatile 与 sychronized 的区别?

特性 volatile synchronized
可见性 ✅ 保证 ✅ 保证
原子性 ❌ 不保证 ✅ 保证(代码块原子执行)
互斥性 ❌ 无 ✅ 存在
禁止重排序 ✅ 有效 ✅ 间接实现(通过内存语义)
使用成本 较低(无锁,非阻塞) 较高(可能阻塞,涉及上下文切换)
适用场景 状态标志、单例懒加载等 临界区保护、资源读写同步

一、synchronized 重量级同步机制

  • 作用:解决多线程访问共享资源时可能出现的竞态条件和数据不一致问题,保证线程安全
  • 特性
    • 保证互斥性(同一时刻只有一个线程能执行同步代码块)
    • 保证可见性(进入同步块前会将线程工作内存与主内存同步)
    • 隐式原子性保障(对共享资源的操作是原子的)
  • 使用方式:可用于方法或代码块,加锁的对象可以是类、实例或任意对象
  • 性能:开销较大,尤其在高并发下容易导致线程阻塞和上下文切换

二、volatile 轻量级可见性保证

  • 作用:用于保证变量在多线程环境下的可见性禁止指令重排序不保证原子性
  • 特性
    • 不具备互斥性,无法保证操作的原子性
    • 保证可见性(修改立即对其他线程可见)
    • 禁止指令重排序(通过内存屏障防止重排序破坏执行语义)
  • 使用方式:用于修饰变量,一般用于状态标志、配置开关等场景
  • 性能:非常轻量,线程不会阻塞

28# 什么是公平锁和非公平锁?

一、公平锁

  • 定义:按照线程请求锁的顺序来获取锁,先请求的线程先获得锁,后请求的线程排队等待
  • 特点:类似 “排队买票”,线程按照先来先服务的原则依次执行。每个线程都有公平的机会获得锁,不容易发生线程饥饿
  • 优点:公平性强,避免某些线程长时间得不到锁
  • 缺点:等待队列的调度成本较高,频繁上下文切换会降低吞吐量整体性能

二、 非公平锁

  • 定义:线程在尝试获取锁时不考虑等待队列的顺序,先尝试直接获取锁,获取失败才进入队列排队等待。
  • 特点:类似 “插队”,后来的线程可能比排队线程更早获得锁。提高了竞争成功的可能性,但可能导致部分线程长时间得不到锁(线程饥饿
  • 优点:整体性能更高吞吐量更大,减少线程切换
  • 缺点:公平性较差,某些线程可能长期 “饿死”

29# 非公平锁吞吐量为什么比公平锁大?

主要原因在于其减少了线程上下文切换和排队等待的开销。以下是两者执行流程的对比说明:

特性 公平锁 非公平锁
获取顺序 严格按排队顺序,先来先得 不保证顺序,可能“插队”
性能表现 有更多上下文切换,性能相对较低 避免频繁挂起与恢复,性能更优
实现机制 线程入队 → 挂起 → 唤醒 → 获取锁 CAS尝试获取 → 失败则入队等待
吞吐量 相对较低 相对较高
是否可能饥饿 否,公平调度 是,排在后面的线程可能长时间得不到锁

一、公平锁执行流程

  • 当线程尝试获取锁时,会先被加入到等待队列的尾部,然后挂起(休眠),等待被调度
  • 当前持有锁的线程释放锁后,会唤醒队列中最前面的线程来尝试获取锁
  • 整个过程保证了 “先来先服务” 的公平性,每个等待线程需要经历从运行态 → 阻塞态 → 运行态的切换,这些状态切换涉及用户态与内核态之间的转换,这是一种昂贵的操作
  • 因此,在高并发场景下,公平锁的上下文切换频繁,导致整体性能下降、吞吐量变

二、非公平锁执行流程

  • 当线程尝试获取锁时,会先直接通过 CAS 操作抢占锁:如果抢到锁,就直接执行,无需排队;如果没抢到,再进入等待队列排队等待下一次竞争机会
  • 由于不强制按照排队顺序执行,非公平锁能减少线程挂起/恢复的频率,避免了大量上下文切换,线程有更高的概率 “插队” 获取锁,因此锁的利用效率更高
  • 这使得非公平锁在高并发环境下的响应更快,吞吐量更大

30# synchronized 是公平锁吗?

不是。synchronized不保证公平性,因此它并不是一种公平锁。在多线程竞争锁资源时,synchronized 采用的是非公平策略:当锁被释放时,所有等待线程都会同时竞争该锁,谁先抢到谁执行,线程的获取顺序不一定按照等待的先后顺序

相比之下,ReentrantLock 是 Java 中提供的可重入锁,它支持公平和非公平两种模式,默认情况下,ReentrantLock 使用的是非公平策略;若需要公平性,可以通过构造方法 new ReentrantLock(true) 来显式指定为公平锁

31# ReentrantLock是怎么实现公平锁的?

ReentrantLock 通过 Sync 内部类(继承自 AQS) 来区分 “公平锁” 和 “非公平锁”。二者在获取锁时的关键分支不同:

太长不看版

比较维度 公平锁 (tryAcquire) 非公平锁 (nonfairTryAcquire)
获取策略 先检查 hasQueuedPredecessors(),前驱为空才能抢锁 直接 CAS 抢锁,失败后再排队
性能 上下文切换多,吞吐量稍低 吞吐量高,但可能产生线程饥饿
场景选择 需要严格先来先得 / 避免饥饿 追求吞吐量、对公平性不敏感

公平锁源码(节选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // ① 读锁状态
if (c == 0) {
// ② “排队检查”:AQS 队列前面是否还有线程?
if (!hasQueuedPredecessors() && // 公平锁核心:必须排第一
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { // ③ 可重入
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}

核心语句hasQueuedPredecessors()

  • 返回 true ⇒ 队列中已有前驱线程,当前线程不得“插队”,必须排队
  • 返回 false ⇒ 当前线程位于队首,可立即 CAS 抢锁

因此,公平锁 严格按照先来后到 的排队顺序获取锁,避免了饿死现象,但带来了更多上下文切换成本


非公平锁源码(节选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // ① 读锁状态
if (c == 0) {
// ② 直接 CAS 抢锁,不看队列
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { // ③ 可重入
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
  • 没有队列检查:线程一来就尝试 CAS 抢锁,成功则立即进入临界区,失败再进入队列挂起
  • 好处:吞吐量更高(锁刚释放时附近线程可立即命中锁)
  • 风险:可能出现“插队”,导致等待队列里的某些线程长时间得不到锁(轻度饥饿)

tryLock() 的特例

无论 ReentrantLock 创建时指定的是公平 (new ReentrantLock(true)) 还是非公平模式,tryLock() 一律使用非公平策略

1
2
3
public boolean tryLock() {
return sync.nonfairTryAcquire(1); // 始终调用非公平实现
}

tryLock() 本身语义就是“立即尝试一次”,所以可以“插队”成功,而不保证排队顺序

32# 什么情况会产生死锁问题?如何解决?

死锁的发生必须同时满足以下四个条件

  1. 互斥条件:某个资源一次只能被一个线程占用,不能共享使用
  2. 占有且等待条件: 线程已持有至少一个资源,并且在等待获取其他被别的线程占用的资源,不释放当前已有资源
  3. 不可抢占条件:线程已获取的资源在未使用完之前不能被强行剥夺,只能线程自己释放
  4. 循环等待条件:存在一个线程资源的循环等待链,即线程 A 等待线程 B 占用的资源,而线程 B 又在等待线程 A 的资源

只要打破任意一个死锁条件,就可以有效避免死锁。最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件:给资源编号,规定线程必须按照固定顺序申请资源,例如,无论线程 A 还是线程 B,始终按照 “先申请资源 R1,再申请资源 R2” 的顺序来获取资源

线程池

1# 介绍一下线程池的工作原理

线程池的工作原理

线程池的主要作用是复用线程资源,避免频繁创建和销毁线程带来的性能开销。它通过预先创建一定数量的线程,并维护一个任务队列来管理并发任务,从而提高系统整体吞吐量和响应速度

线程池的设计理念是通过线程重用 + 队列排队 + 最大并发限制来管理资源和任务调度,它的典型执行策略是:

  • 优先使用核心线程
  • 核心线程满 → 入队列
  • 队列满 → 创建非核心线程(直到最大线程数)
  • 超出最大线程数 → 执行拒绝策略

2# 线程池的参数有哪些?

  1. corePoolSize(核心线程数):线程池中常驻的线程数量。即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut(true)

  2. maximumPoolSize(最大线程数):线程池中允许创建的最大线程数量。当任务过多、核心线程和队列都满时,线程池才会继续创建新线程,直到达到该上限

  3. keepAliveTime(空闲线程存活时间):当线程数大于核心线程数时,空闲线程在达到该时间后将被销毁,以节省资源

  4. unit(时间单位)keepAliveTime 的时间单位,例如 TimeUnit.SECONDSTimeUnit.MILLISECONDS 等。

  5. workQueue(任务队列):用来保存待执行的任务。当所有核心线程都在忙时,新的任务会被加入该队列等待执行。常见实现包括:

    • ArrayBlockingQueue(有界队列)

    • LinkedBlockingQueue(无界队列)

    • SynchronousQueue(直接交付)

    • PriorityBlockingQueue(优先级队列)

  6. threadFactory(线程工厂):用于自定义线程的创建方式,通常可用来设置线程名称、优先级、是否为守护线程等

  7. handler(拒绝策略):当线程池达到最大线程数并且任务队列已满时的处理策略

3# 线程池有哪些拒绝策略?

  • AbortPolicy(抛出异常策略):默认策略,直接抛出 RejectedExecutionException 异常,提醒程序任务已被拒绝处理

  • CallerRunsPolicy(调用者执行策略):由提交任务的线程(通常是主线程)直接执行该任务,从而降低任务提交的速度,避免任务快速堆积

  • DiscardPolicy(静默丢弃策略):不抛异常,也不执行被拒绝的任务,直接丢弃任务

  • DiscardOldestPolicy(丢弃最旧任务策略):丢弃任务队列中最早的任务,然后尝试重新提交当前任务

此外,通过实现 RejectedExecutionHandler 接口,可以根据业务需求编写自定义拒绝策略,例如记录日志、报警、持久化任务等:

1
2
3
4
5
6
7
public class CustomRejectHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义处理逻辑,如记录日志、发送告警、持久化等
System.out.println("任务被拒绝:" + r.toString());
}
}

4# 如何设置线程池参数?

核心线程数(corePoolSize)设置原则:

  • CPU 密集型任务(如加解密、图像处理等):corePoolSize = CPU 核数 + 1,避免线程频繁上下文切换,充分利用每个核心

  • IO 密集型任务(如网络、数据库访问):corePoolSize = CPU 核数 × 2 或更高,线程大部分时间阻塞于 IO,适当提高线程数以提升吞吐

场景一:电商高并发请求处理,突发流量高、任务处理快、对响应时间敏感

1
2
3
4
5
6
7
new ThreadPoolExecutor(
16, // corePoolSize(假设8核CPU × 2)
32, // maximumPoolSize,短时扩容
10, TimeUnit.SECONDS, // 非核心线程空闲 10 秒后回收
new SynchronousQueue<>(), // 无缓冲,任务直达线程
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:快速失败
);
  • 使用 SynchronousQueue 保证任务直达线程,响应及时
  • 非核心线程可临时扩容,适应突发流量
  • 拒绝策略使用 AbortPolicy,快速失败,结合前端提示“活动火爆”与缓存降级,保护系统稳定

场景二:后台批处理或日志分析,流量稳定、任务耗时长、允许延迟

1
2
3
4
5
6
7
new ThreadPoolExecutor(
8, // 固定线程数,避免资源波动
8,
0, TimeUnit.SECONDS, // 线程始终保留,不回收
new ArrayBlockingQueue<>(1000), // 有界队列,容量1000
new ThreadPoolExecutor.CallerRunsPolicy() // 调用线程兜底执行
);
  • 限制线程数量,避免资源过载
  • 队列缓冲大量任务,适用于慢速处理
  • 使用 CallerRunsPolicy,队列满时由调用线程执行任务,起到背压效果
  • 配合监控系统,当队列使用率超过阈值(如 80%)可自动触发扩容或报警

场景三:微服务中处理 HTTP 请求,IO 密集型,依赖下游服务响应,需考虑请求超时和排队时间

1
2
3
4
5
6
7
new ThreadPoolExecutor(
16, // 8 核 CPU × 2,适应 IO 阻塞
64, // 最大线程数,支持更多并发
60, TimeUnit.SECONDS, // 非核心线程空闲 1 分钟后回收
new LinkedBlockingQueue<>(200), // 有界队列,防止OOM
new CustomRetryPolicy() // 自定义拒绝策略
);
  • 较高的最大线程数应对下游慢响应
  • 有界队列防止内存泄露
  • 自定义拒绝策略(例如将任务放入 Redis 或消息队列)实现异步重试或降级
  • 可配合服务熔断、限流等机制构建完整容灾链路

5# 核心线程数设置为 0 可以吗?

可以,当核心线程数为 0 的时候,会创建一个非核心线程进行执行

任务提交后,任务会先尝试加入任务队列,如果当前没有线程在运行(即 workerCount == 0),则会创建一个非核心线程来处理队列中的任务,当任务处理完毕,且该线程处于空闲状态超过 keepAliveTime,它也会被回收

6# 线程池种类有哪些?

Java 提供了多种内置线程池,位于 java.util.concurrent.Executors 工具类中,适用于不同的任务调度场景:

  1. ScheduledThreadPool

    • 功能: 支持定时执行周期性执行任务

    • 适用场景: 适合需要在固定时间间隔内重复执行任务的场景,例如:每隔 10 秒执行一次日志清理任务

    • 核心特点: 使用 ScheduledExecutorService 接口,可通过 schedule()scheduleAtFixedRate() 方法配置延迟或周期性任务

  2. FixedThreadPool

    • 功能: 创建一个固定线程数量的线程池

    • 适用场景: 稳定并发量的业务场景,如处理日志、消息消费等

    • 核心特点:

      • 核心线程数 = 最大线程数,线程数固定
      • 超过线程数的任务会被放入阻塞队列等待执行
      • 队列满时不会再扩展线程数量,可能触发拒绝策略
      • 线程空闲不会被销毁,长期占用资源
  3. CachedThreadPool

    • 功能: 创建一个可以无限扩展线程数量的线程池(理论上最大线程数为 Integer.MAX_VALUE

    • 适用场景: 适合执行大量、短生命周期的异步任务

    • 核心特点:

      • 使用 SynchronousQueue 作为任务队列(容量为 0,不存储任务,只做转交)
      • 没有核心线程,任务来就创建线程
      • 空闲线程在 60 秒后回收
      • 创建和销毁线程代价相对较高,慎用
  4. SingleThreadExecutor

    • 功能: 创建一个只有单个工作线程的线程池

    • 适用场景: 所有任务需要严格按照提交顺序执行的场景

    • 核心特点:

      • 使用一个线程串行执行所有任务
      • 线程异常退出时,会自动创建新的线程保障执行连续性
      • 类似 FixedThreadPool(1),但具备更强的顺序性保障
  5. SingleThreadScheduledExecutor

    • 功能: 单线程版本的 ScheduledThreadPool,支持定时和周期性任务调度

    • 适用场景: 需要按顺序、定时执行任务的单线程场景

    • 核心特点:

      • 内部只有一个线程
      • 能保证任务按顺序定时执行
      • ScheduledThreadPoolExecutor 的特例

总结

类型 是否固定线程 是否支持定时 是否支持扩容 是否保证顺序
FixedThreadPool
CachedThreadPool
ScheduledThreadPool
SingleThreadExecutor ✅(1个)
SingleThreadScheduledExecutor ✅(1个)

7# 线程池一般是怎么用的?

Java 中的 Executors 类定义了一些快捷的工具方法,来帮助我们快速创建线程池。《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 new ThreadPoolExecutor 来创建线程池

为什么尽量不要用 Executors 快捷工厂?

Executors 工厂方法 潜在风险 典型事故场景
newFixedThreadPool 无界阻塞队列任务持续堆积 → 内存飙升 高并发秒杀、批量导入,队列挤满导致 OOM
newCachedThreadPool 线程无限制增长 (Integer.MAX_VALUE)快速消耗 CPU / 句柄 下游接口持续超时,线程池疯狂扩容抢锁、最终拖垮机器
  • 我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数
  • 任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题

除了建议手动声明线程池以外,我还建议用一些监控手段来观察线程池的状态。线程池这个组件往往会表现得任劳任怨、默默无闻,除非是出现了拒绝策略,否则压力再大都不会抛出一个异常。如果我们能提前观察到线程池队列的积压,或者线程数量的快速膨胀,往往可以提早发现并解决问题。

8# 线程池中 shutdown ()、shutdownNow() 这两个方法有什么作用?

方法名 行为特征
shutdown() 优雅关闭:执行中的任务继续,等待队列中的任务照常处理
shutdownNow() 强制关闭:中断正在执行的任务,清空队列,立即返回未处理任务
共同点 都会拒绝新的任务提交,线程池状态都会改变,最终尝试终止线程池

一、shutdown()

调用 shutdown() 方法后,线程池的状态将被设置为 SHUTDOWN,此时:

  • 已提交但尚未执行的任务会继续排队等待执行
  • 正在执行的任务会继续执行直至完成
  • 线程池不再接收新的任务提交,否则会抛出 RejectedExecutionException 异常
  • 不会中断正在运行的线程

适用于优雅关闭线程池的场景


二、shutdownNow()

调用 shutdownNow() 方法后,线程池状态被设置为 STOP,并执行如下操作:

  • 立即中断所有正在运行的线程(通过 Thread.interrupt() 实现)
  • 放弃队列中尚未执行的任务,并将这些任务作为 List<Runnable> 返回
  • 同样不再接受新的任务提交

但请注意:Thread.interrupt() 仅能中断那些处于阻塞状态(如 sleep()wait()join()Condition.await()、定时锁等)的线程,对于纯计算类任务并无实际中断效果

因此,调用 shutdownNow() 并不意味着线程池能够立即关闭,它可能仍需等待某些任务结束才能真正终止

9# 提交给线程池的任务可以撤回吗?

可以。在 Java 中,当向线程池提交任务时,会返回一个 Future 对象。通过这个对象,可以对任务的执行过程进行控制,例如:获取任务执行结果、判断任务是否完成、尝试取消任务执行

Future 接口提供了 cancel(boolean mayInterruptIfRunning) 方法,用于取消任务:

1
boolean cancel(boolean mayInterruptIfRunning);
  • mayInterruptIfRunning = true:如果任务正在运行,尝试中断该任务(调用 Thread.interrupt()
  • mayInterruptIfRunning = false:如果任务已启动,则不会中断,仅取消尚未开始的任务

此外,Future 还提供了以下常用方法:

1
2
3
4
boolean isCancelled();   // 判断任务是否已取消
boolean isDone(); // 判断任务是否已完成(成功/失败/取消都算完成)
V get(); // 获取任务结果(阻塞直到结果返回或异常抛出)
V get(long timeout, TimeUnit unit); // 限时获取任务结果

取消线程池中的任务示例代码:

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
public class CancelTaskDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();

Future<?> future = service.submit(() -> {
try {
// 模拟耗时任务
Thread.sleep(5000);
System.out.println("任务完成");
} catch (InterruptedException e) {
System.out.println("任务被中断");
}
});

try {
// 等待任务完成
future.get(2, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
System.out.println("任务超时,尝试取消...");
// 超时后尝试取消任务(允许中断)
future.cancel(true);
} finally {
service.shutdown();
}
}
}

场景

1# 多线程打印奇偶数,怎么控制打印的顺序?

交替打印奇偶数(wait/notify 方案)

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
public class PrintOddEven {

private static final Object LOCK = new Object();
private static int counter = 1;
private static final int MAX = 10;

public static void main(String[] args) {
Thread odd = new Thread(() -> printNumber(true), "OddThread");
Thread even = new Thread(() -> printNumber(false), "EvenThread");
odd.start();
even.start();
}

/** @param printOdd true 打印奇数,false 打印偶数 */
private static void printNumber(boolean printOdd) {
while (true) {
synchronized (LOCK) {
if (counter > MAX) {
LOCK.notifyAll(); // 唤醒对方后退出
break;
}
if ((counter & 1) == (printOdd ? 1 : 0)) {
System.out.printf("%s : %d%n",
Thread.currentThread().getName(), counter++);
LOCK.notifyAll(); // 打印完唤醒对方
} else {
try { LOCK.wait(); } // 条件不满足,挂起
catch (InterruptedException ignored) {}
}
}
}
}
}

为什么能交替输出?

  1. 共享状态counter 决定当前应打印奇数还是偶数
  2. 互斥synchronized (LOCK) 保证同一时刻只有一个线程在修改 counter
  3. 协作:打印线程完成一次输出后调用 notifyAll(),提醒对方线程条件可能满足,如果发现当前不该自己打印,则调用 wait() 释放锁并挂起,等待被唤醒

这样就形成了“打印 → 唤醒对方 → 自己等待”的循环,保证奇、偶顺序交替

注意notifyAll()notify() 更安全,避免“假唤醒”或只有单线程等待场景时对方仍沉睡

2# 单例模型既然已经用了 synchronized,为什么还要在加 volatile?

使用 synchronizedvolatile 一起,可以创建一个既线程安全又能正确初始化的单例模式,避免了多线程环境下的各种潜在问题。这是一种比较完善的线程安全的单例模式实现方式,尤其适用于高并发环境

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

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

synchronized 关键字的作用用于确保在多线程环境下,只有一个线程能够进入同步块(这里是 synchronized (Singleton.class))。在创建单例对象时,通过 synchronized 保证了创建过程的线程安全性,避免多个线程同时创建多个单例对象

volatile 确保了对象引用的可见性和创建过程的有序性,避免了由于指令重排序而导致的错误

instance = new Singleton(); 这行代码并不是一个原子操作,它实际上可以分解为以下几个步骤:

  • 分配内存空间。
  • 实例化对象。
  • 将对象引用赋值给 instance

由于 Java 内存模型允许编译器和处理器对指令进行重排序,在没有 volatile 的情况下,可能会出现重排序,例如先将对象引用赋值给 instance,但对象的实例化操作尚未完成

这样,其他线程在检查 instance == null 时,会认为单例已经创建,从而得到一个未完全初始化的对象,导致错误

volatile 可以保证变量的可见性和禁止指令重排序。它确保对 instance 的修改对所有线程都是可见的,并且保证了上述三个步骤按顺序执行,避免了在单例创建过程中因指令重排序而导致的问题

3# 3 个线程并发执行,1 个线程等待这三个线程全部执行完在执行,怎么实现?

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 LatchDemo {

public static void main(String[] args) {
final int workers = 3;
CountDownLatch latch = new CountDownLatch(workers);

// 3 个工作线程
for (int i = 1; i <= workers; i++) {
int id = i;
new Thread(() -> {
try {
System.out.println("Worker-" + id + " working...");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("Worker-" + id + " done.");
} catch (InterruptedException ignored) {
} finally {
latch.countDown(); // 完成后递减计数
}
}).start();
}

// 等待所有工作线程结束
new Thread(() -> {
try {
System.out.println("Awaiting completion of workers...");
latch.await(); // 阻塞,直到计数=0
System.out.println("All workers finished. Main task starts.");
} catch (InterruptedException ignored) {}
}, "Coordinator").start();
}
}

代码说明

  1. CountDownLatch latch = new CountDownLatch(3);
    创建一个计数器,初始值为 3,代表有 3 个线程需要 “倒计时”
  2. 每个线程在执行完任务后,调用 latch.countDown() 将计数器减 1
  3. 第 4 个线程调用 latch.await(),阻塞等待直到计数器变为 0,再继续执行自己的任务

4# 假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?

在没有任何同步机制的情况下,两个线程并发对同一个整型变量进行 50 次加 1 操作,最终结果可能是 100,也可能小于 100,最坏的结果是 50,也就是最终的结果可能是在 [50, 100]

原因

num++ 操作并不是原子操作,它实际包含以下三个步骤:

  1. 读取变量的当前值
  2. 将值加 1
  3. 将新值写回变量

在多线程环境中,如果两个线程同时读取了相同的值并分别加 1,然后再写回,就会导致 “加 1” 操作丢失,从而导致最终结果小于预期

解决方案一:使用 AtomicInteger

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
public class AtomicIntegerAddition {
private static AtomicInteger num = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
num.incrementAndGet();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
num.incrementAndGet();
}
});

thread1.start();
thread2.start();

thread1.join();
thread2.join();

System.out.println("最终结果: " + num.get());
}
}

解决方案二:使用 synchronized 或 ReentrantLock

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
public class SynchronizedAddition {
private static int num = 0;
private static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
synchronized (lock) {
num++;
}
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
synchronized (lock) {
num++;
}
}
});

thread1.start();
thread2.start();

thread1.join();
thread2.join();

System.out.println("最终结果: " + num);
}
}