Skip to content

右侧栏

JAVA 多线程面试题

[toc]

什么是原子性?

原子性指的是一个或者多个操作,要么全部执行,要么都不执行,再执行的过程中,不被其他操作中断或干扰。

在并发过程中,原子性是一个非常重要的概念,如果操作不是原子的,在多线程并发环境下,多个线程可能访问同一个变量,可能会导致数据不一致。

Java 中基本数据类型的赋值和读取是原子的吗?

处理long 和 double (可以通过 volatile 修饰实现原子性)注意:volatile 修饰的 i++,i--操作不具有原子性,volatile 主要是为了解决可见性,其他的基本数据类型的读取和赋值都是原子性的。

java
int x = 100;//原子
x++;//非原子
int y = x;//非原子
x = x + 1;//非原子

上面的代码只有第一行是原子的,其他三个都包含两个以上的操作,首先都是要求读取 x 变量的值,经过计算后再将新的值写入到内存中去

volatile 的局限性

  • 不保证原子性
    • volatile 变量的复合操作(如 i++)仍是非原子的,可能被线程切换打断。
  • 不解决指令重排序
    • 编译器或 CPU 仍可能重排 volatile 与非 volatile 操作的顺序(除非配合内存屏障)。
  • 不适用于多线程同步
    • 现代多核 CPU 的缓存一致性协议(如 MESI)通常能自动同步内存,volatile 的可见性作用被弱化,而原子性和顺序性成为更关键的问题。

什么是可见性?如何保证可见性?

可见性是指一个线程对共享变量的修改,其他线程可以立刻看到该值。

多个线程可能会访问同一个变量,由于线程本地缓存(如 cpu 缓存)的存在,一个线程对变量的修改,但没有及时刷新到到主内存,其他线程读取的可能是旧的副本值,而不是最新的。这就导致了可见性问题。

如何保证可见性?

  1. volatile 关键字。

    1. volatile 保证的了对变量的写操作会立刻写入到主内存中去。
    2. 对变量的读操作会从主内存中读取最新值。
    3. 保证可见性,但是不保证原子性。
  2. 使用 Synchronized 或 lock

    Synchronized 关键字不仅保证互斥性(即同一个时刻只有一个线程执行临界区代码)还保证可见性:进入 Synchronized 代码块时,会清空本地缓存,从主内存中读取最新值;退出时,会将变量刷新到主内存中去。

  3. 使用 final(初始化时的可见性)

    使用 final 字段,java 在构造函数完成后,会确保所有线程都能够看到该字段的正确值。

什么是有序性?如何保证有序性?

有序性指代码运行的顺序符合代码编写时的先后顺序。

但是在实际执行中,出于优化的目的(如提高性能),编译器、CPU、和 java 虚拟机都可能对代码进行“指令重排序”,从而打破原本代码书写的顺序。这就可能导致并发程序中产生预期之外的结果。

如何保证有序性?

  1. volatile 关键字

    1. 限制了某些类型的指令重排序

    2. 写入 volatile 变量之前的所有操作,在内存语义上都会在写操作之前完成

    3. 保证了 写-读操作的有序性。

      java
      volatile boolean ready = false;
      int a = 0
      //线程 A修改值
      a=42;
      
      read = true; //volatile 写屏障,强制前面的先执行
      //线程 B
      if (ready) {
      		System.out.println(a);//保证看到 a=42
      }
  2. Synchronized / lock

    1. Synchronized 不仅保证互斥性和可见性,还会禁止同步块内的重排序。
    2. 进入同步块:读取主内存,禁止重排序
    3. 退出同步块;刷行变量到主内存,保证前后代码的顺序。
  3. 内存屏障(Memory Barrier)或 happens-before 规则(JMM 内存模型)

    1. Java 内存模型通过 happens-before 关系来定义哪些操作之间是有序的。
    2. 比如:
      1. Synchronized 之间的操作是有序的
      2. volatile 写->读是有序的
      3. 线程 start() 之前的操作,对新线程是可见且有序的
      4. 线程 join()之后的操作,一定在被 join 的线程所有操作之后

为什么要使用多线程?

  1. 更好的利用多核 cpu(并行计算)

  2. 提升程序的响应速度(改善用户体验)

  3. 提高程序执行效率(cpu利用率),防止阻塞

    单线程程序在 io 时会阻塞,cpu 闲置,多线程可以在一个线程等待时,其他线程继续工作,从而充分利用 cpu。

创建线程的几种方式?

  1. 继承 Thread 类重新 run 方法

  2. 实现 runable 接口重写run 方法

  3. 实现 Callable 接口+FutureTask(有返回值)

    java
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    class MyCallable implements Callable<String> {
        @Override
        public String call() {
            return "Result from Callable";
        }
    }
    
    public class Main {
        public static void main(String[] args) throws Exception {
            FutureTask<String> task = new FutureTask<>(new MyCallable());
            new Thread(task).start();
          
          //task.get()方法会阻塞
            System.out.println("Callable result: " + task.get());
        }
    }

    需要注意的是

    • callable 是一个可以产生返回值的任务。
    • 你可以通过 future 或 futureTask获取这个返回值;
    • 如果你不调用 get() 方法,主线程不会阻塞

什么是守护线程与非守护线程?如何创建?

守护线程是指在后台默默提供服务的线程。当所有用户线程(非守护线程)都结束后,jvm 会自动退出,及时守护线程还在运行。常用于垃圾回收、异步清理任务、日志、监控、心跳等后台服务线程

非守护线程(也称之为用户线程)是程序中默认创建的线程,JVM 会一直运行,知道所有非守护线程结束,才会退出。

Thread 有一个属性用户区分时候是守护线程,通过 Thread.setDaemon(true) 来设置(必须在启动之前)

线程的几种状态,如果流转的?

Java 中线程一共有 6 种状态,由java.lang.Thread.State 枚举定义:

状态名含义
NEW新建状态:线程已创建但尚未启动
RUNNABLE可运行状态:正在运行或等待 CPU 调度
BLOCKED阻塞状态:等待锁(monitor)
WAITING无限期等待:等待别的线程显式唤醒
TIMED_WAITING有时间限制的等待:如 sleep()join(1000)
TERMINATED终止状态:线程执行完毕或异常退出

生命周期:

scss
        ┌──────────┐
        │   NEW    │
        └────┬─────┘
             │ start()

        ┌────────────┐
        │ RUNNABLE   │
        └───┬─┬──────┘
            │ │
            │ └────────────┐
            ▼              ▼
      ┌─────────┐     ┌──────────────┐
      │ BLOCKED │     │ WAITING      │◄─────────┐
      └────┬────┘     └──────────────┘          │
           │               ▲                    │
           │               │                    │
           ▼               │                    │
    ┌──────────────┐       │             ┌──────────────┐
    │ TIMED_WAITING│───────┘             │TERMINATED    │
    └──────────────┘                     └──────────────┘

线程的优先级有什么用?

它的作用是"影响线程被 CPU 调度的概率"

  • 高优先级线程在理论上更有可能被操作系统线程调度器选中。
  • 但这只是一个指标,具体行为依赖与操作系统和 JVM 实现。不能依赖它来保证顺序性或者公平性。(优先级不是抢占式控制,不是强制调度!)

java 中每个线程都有一个整数优先级,取值范围是:

常量名说明
Thread.MIN_PRIORITY1最低优先级
Thread.NORM_PRIORITY5(默认)普通优先级
Thread.MAX_PRIORITY10最高优先级

可用通过以下当时设置优先级:

java
Thread t = new Thread(...);
t.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级

线程优先级只是调度建议,不是强制控制,不能依赖它实现功能或控制顺序,但可用于性能调优和资源让渡。

怎么让 3 个线程按顺序执行?编程实现?

  1. 使用 join()方法,join()方法的主要作用是让当前线程等待调用 join 的线程执行完成后再继续执行

    java
    package com.hongsipeng.juc;
    
    /**
     * @author hongsipeng
     * @apiNote join 方法测试
     * @since 2025/7/30
     */
    public class JoinExample {
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                System.out.println("线程 1 开始");
            });
            // join() 方法可以让一个线程等待另一个线程执行完毕
            Thread thread2 = new Thread(() -> {
                try {
                    thread1.join();
                    System.out.println("线程 2 开始");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
    
            Thread thread3 = new Thread(() -> {
                try {
                    thread2.join();
                    System.out.println("线程 3 开始");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            thread1.start();
            thread2.start();
            thread3.start();
    
        }
    }
  2. 使用单线程池,线程池中只有一个核心线程,提交任务给线程池时,线程任务会被阻塞

    java
    package com.hongsipeng.juc;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * @author hongsipeng
     * @apiNote 单线程池控制线程按照顺序执行
     * @since 2025/7/30
     */
    public class SingleThreadExecutorExample {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            executorService.execute(()->{
                System.out.println("线程 1 执行");
            });
            executorService.execute(()->{
                System.out.println("线程 2 执行");
            });
            executorService.execute(()->{
                System.out.println("线程 3 执行");
            });
            executorService.shutdown(); //如果不加这个,程序不会终止
        }
    }
  3. 使用 Wait 和 notify 机制

    java
    package com.hongsipeng.juc;
    
    /**
     * @author hongsipeng
     * @apiNote wait notify example
     * @since 2025/8/3
     */
    public class WaitNotifyExample {
        private static int currentThread = 1;
    
        public static void main(String[] args) {
    
            Object lock = new Object();
            new Thread(() -> {
                synchronized (lock) {
                    while (currentThread != 1) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Thread " + currentThread + " is over");
                    currentThread = 2;
                    lock.notifyAll();
                }
            }).start();
    
            new Thread(() -> {
                synchronized (lock) {
                    while (currentThread != 2) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Thread " + currentThread + " is over");
                    currentThread = 3;
                    lock.notifyAll();
                }
            }).start();
    
            new Thread(() -> {
                synchronized (lock) {
                    while (currentThread != 3) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Thread " + currentThread + " is over");
                    currentThread = 4;
                    lock.notifyAll();
                }
            }).start();
        }
    }
  4. 使用 Condition 实现

    java
    package com.hongsipeng.juc;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @author hongsipeng
     * @apiNote 使用 Condition实现线程按照顺序执行
     * @since 2025/8/4
     */
    public class ThreadOrderWithConditionTest {
        private static final Lock lock = new ReentrantLock();
        private static final Condition condition1 = lock.newCondition();
        private static final Condition condition2 = lock.newCondition();
        private static final Condition condition3 = lock.newCondition();
        private static int currentThread = 1;
    
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    lock.lock();
                    while (currentThread != 1) {
                        try {
                            condition1.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Thread " + currentThread + " is over");
                    currentThread = 2;
                    condition2.signal();
                } finally {
                    lock.unlock();
                }
            }).start();
    
            new Thread(() -> {
                try {
                    lock.lock();
                    while (currentThread != 2) {
                        try {
                            condition2.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Thread " + currentThread + " is over");
                    currentThread = 3;
                    condition3.signal();
                } finally {
                    lock.unlock();
                }
            }).start();
    
            new Thread(() -> {
                try {
                    lock.lock();
                    while (currentThread != 3) {
                        try {
                            condition3.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Thread " + currentThread + " is over");
                    condition1.signal();//如果需要循环执行
                } finally {
                    lock.unlock();
                }
            }).start();
        }
    }

    相比 wait 和 notify,condition 提供了更精确的线程唤醒机制,可以为不同的等待条件创建不同的 Condition 对象。

    注意:必须在 lock 和 unlock 之间调用 condition 的方法,否则会抛出IllegalMonitorStateException 异常

  5. 使用 CountDownLatch

    java
    package com.hongsipeng.juc;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * @author hongsipeng
     * @apiNote CountDownLatch 测试例子,控制线程按照顺序执行
     * @since 2025/8/4
     */
    public class CountDownLatchExample {
        static CountDownLatch countDownLatch1 = new CountDownLatch(1);
        static CountDownLatch countDownLatch2 = new CountDownLatch(1);
    
        public static void main(String[] args) {
            new Thread(() -> {
                System.out.println("线程 1 执行");
                countDownLatch1.countDown();
            }).start();
    
            new Thread(() -> {
                try {
                    countDownLatch1.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程 2 执行");
                countDownLatch2.countDown();
            }).start();
    
            new Thread(() -> {
                try {
                    countDownLatch2.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程 3 执行");
            }).start();
        }
    }
  6. 使用 CompletableFuture

    java
    package com.hongsipeng.juc;
    
    import java.util.concurrent.CompletableFuture;
    
    /**
     * @author hongsipeng
     * @apiNote CompletableFuture 测试类
     * @since 2025/8/4
     */
    public class CompletableFutureExample {
        public static void main(String[] args) {
            CompletableFuture.runAsync(()->{
                System.out.println("线程 1 执行");
            }).thenRun(()->{
                System.out.println("线程 2 执行");
            }).thenRun(()->{
                System.out.println("线程 3 执行");
            }).join();
        }
    }

    CompletableFuture 是 java8 引入的一个强大的异步编程工具,属于 java.util.concurrent包下,它是对 future 的增强版,支持链式调用,组合异步调用,异常处理等。详细介绍请看本文后半部分。

join 方法有什么用?什么原理?编程实现?

join()方法的主要作用是让当前线程等待被调用join()的线程执行完毕,具体来说

  1. 阻塞当前线程:调用某线程的 join()方法会使当前线程进入阻塞状态
  2. 等待目标线程执行完成:直到被 join 的线程执行完毕,当前线程才会继续执行
  3. 线程同步:用于协调线程的执行顺序

原理

  1. join方法底层是使用了 wait notify 机制,调用线程的 wait()方法使当前线程进入等待状态。
  2. 通过在while循环中指令 isAlive 方法判断 join 的线程是不是还存在,若存在,则当前线程一直 wait

伪代码实现

java
join();
	while (目标线程.isAlive()) 
		当前线程.wait()

如何让一个线程休眠?

调用线程的 sleep方法比如

java
Thread.sleep(1000*60);//休眠 1 分钟

//可读性高的写法
TimeUnit.MINUTES.sleep(1);
TimeUnit.SECONDS.sleep(1);
....

启动一个线程是 start 还是 run? 两者有什么区别?

  • 使用 start 方法,调用,start 之后,线程会出于就绪状态,有调度器决定什么时候执行。

  • start 方法只能调用一次,不能重复调用,否则会抛出 IllegalThreadStateException

  • run 方法表示执行当前线程的同步方法,不会创建新的线程(不是),可以多次调用

一个线程多次调用 start 会发生什么?

线程不能多次调用 start,重复调用会抛出异常 IllegalThreadStateException

线程 sleep 和 wait 的区别?

  1. Thread.sleep()
    • 属于 java.lang.Thread的静态方法
    • 作用:让当前线程暂停执行指定的时间,进入 time_waiting 状态,不释放锁。
    • 唤醒条件:时间到期后自动恢复,或线程会中断(InterruptedException)
    • 使用场景:模拟延迟、控制任务执行节奏
  2. Object.wait()
    • 属于 java.lang.Object 的实例方法
    • 作用:让当前线程释放锁并进入等待状态(waiting 或 time_waiting),直到被其他线程唤醒
    • 唤醒条件:
      • 其他线程调用同一个对象的 notify 或 notifyAll 方法
      • 超时时间到(若指定了超时时间)
      • 线程被中断(InterruptedException)
    • 前提条件:必须在 synchronized 中调用,否则抛出 IllegalMonitorStateException
    • 使用场景:线程间协作(生产者消费者模型)、等待某个条件满足

Thread.yield方法有什么用?yield 方法和 sleep 方法的区别?编程实现 yield 的例子?

Thread.yield() 是 Java 线程调度的一个静态方法,它的主要作用是 提示线程调度器当前线程愿意让出 CPU 资源,使其他具有相同或更高优先级的线程有机会运行。

  • 不保证立即让出 CPU:具体是否让出取决于 JVM 和操作系统的线程调度策略。
  • 线程状态不变:调用 yield() 后,线程仍处于 RUNNABLE 状态(不会进入阻塞状态)。
  • 适用场景:适用于协作式多任务处理,避免某个线程长时间占用 CPU。

yield()sleep() 的区别

维度yield()sleep(long millis)
所属类Thread 的静态方法Thread 的静态方法
线程状态保持 RUNNABLE进入 TIMED_WAITING(阻塞状态)
锁行为不释放锁不释放锁
唤醒条件由线程调度器决定是否切换固定时间到期或线程被中断
确定性非确定(可能让出 CPU,也可能不让)确定(必须休眠指定时间)
用途提高线程间公平性强制延迟执行

编程实现:

java
public class YieldExample {
    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadA 执行第 " + i + "");
                Thread.yield(); // 提示让出 CPU
            }
        });

        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadB 执行第 " + i + "");
            }
        });

        threadA.start();
        threadB.start();
    }
}

//由于 yield()的非确定性,输出顺序可能不同,但是 Thread 的执行次数会更多

注意事项:

  1. yield() 不保证效果
    • 某些 JVM 实现可能直接忽略 yield(),尤其是在单核 CPU 上。
    • 现代 JVM 的线程调度通常基于优先级和时间片,yield() 的影响较小。
  2. sleep() 的选择
    • 若需要 明确延迟 → 用 sleep()
    • 若希望 提高线程公平性(但不强求)→ 用 yield()
  3. 避免过度依赖 yield()
    • 线程调度的最终控制权在操作系统,yield() 仅是一个提示,不能替代正确的同步机制(如 wait()/notify())。

怎么理解 java 中的线程中断?中断和stop 的区别?编程实现一个线程中断的例子?

  1. 线程中断(Interruption)的理解

    java 中线程中断是一种协作式线程终止机制,通过设置线程中的中断标志位(interrupt status)来请求目标线程停止执行,但不强制终止线程

    • 核心思想:有被中断的线程自行决定如何响应中断(如清理资源后退出)
    • 关键方法:
      • thread.interrupt():设置线程中的中断标志位(若线程在阻塞状态,会触发 InterruptedException).
      • thread.isInterrupted():检查当前线程是否设置中断标志位
      • Thread.interrupted():检查并清楚当前线程的中断标志位(静态方法)
  2. 中断(interrupt)与 stop() 的区别

    维度中断(Interrupt)stop()(已废弃)
    机制协作式(请求线程自行处理)强制终止线程(立即停止)
    安全性安全(线程可清理资源)不安全(可能导致资源未释放、数据不一致)
    方法状态推荐使用已废弃(Java 1.2 后标记为 @Deprecated
    触发异常可能抛出 InterruptedException无异常,直接终止线程

    为什么废弃 stop?

    • 强制终止线程会立即释放所有锁,可能导致共享数据处于不一致状态(如写操作突然终止)
    • 无法保证资源(如文件句柄、数据库连接)的正确释放
  3. 编程示例:线程中断的实现

    场景:一个后台任务持续运行,主线程在 3 秒后中断它,任务线程检测到中断后安全退出。

    java
    public class ThreadInterruptExample {
        public static void main(String[] args) {
            Thread worker = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("Worker 线程正在运行...");
                    try {
                        Thread.sleep(1000); // 模拟工作,休眠1秒
                    } catch (InterruptedException e) {
                        System.out.println("Worker 线程被中断,准备退出");
                        Thread.currentThread().interrupt(); // 重新设置中断标志
                        break;
                    }
                }
            });
    
            worker.start();
    
            try {
                Thread.sleep(3000); // 主线程休眠3秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            worker.interrupt(); // 中断worker线程
        }
    }
    
    //输出结果
    Worker 线程正在运行...
    Worker 线程正在运行...
    Worker 线程正在运行...
    Worker 线程被中断,准备退出
  4. 关键细节

    1. 阻塞方法的中断处理
      • 若线程在 sleep()wait()join() 等阻塞方法(广义上的阻塞,线程暂停执行释放 cpu)中,调用 interrupt() 会触发 InterruptedException,并清除中断标志位。
      • 需在捕获异常后 重新设置中断标志(如 Thread.currentThread().interrupt()),否则中断状态可能丢失。
    2. 非阻塞线程的中断检查
      • 对于未阻塞的线程,需通过 isInterrupted() 主动检查中断标志位。
    3. 不可中断的阻塞操作
      • Socket I/O锁获取(Lock.lock()) 等操作不会响应中断,需通过其他方式终止(如关闭底层资源)。
  5. 中断的最佳实践

    • 响应中断的方式:

      java
      if (Thread.currentThread().isInterrupted()) {
          // 清理资源
          return; // 或抛出 InterruptedException
      }
    • 避免屏蔽中断

      • 不要捕获 InterruptedException 后不做任何处理(至少恢复中断状态)

        java
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
        }

你怎么理解多线程分组?编程实现一个线程分组的例子

多线程分组是指将多个线程按照某种逻辑或功能进行分类管理。这种分组可以帮助我们更好的组合和管理线程,特别是在复杂系统中,可以按分组进行线程的批量操作(如中断、优先级设置等)

多线程分组的主要用途:

  1. 逻辑分类:将执行相同任务的线程归为一组
  2. 批量管理:可以同时对一组线程进行操作
  3. 资源分配:为不同组分配不同的系统资源
  4. 监控和调试:更容易跟踪特定功能模块的线程

java 线程分组示例

java
package com.hongsipeng.juc;

/**
 * @author hongsipeng
 * @apiNote 线程分组示例
 * @since 2025/8/8
 */
public class ThreadGroupExample {
    public static void main(String[] args) {

        // 创建线程组
        ThreadGroup groupA = new ThreadGroup("Group A");
        ThreadGroup groupB = new ThreadGroup("Group B");

        // 创建属于不同组的线程
        Thread thread1 = new Thread(groupA, new Task(), "Thread-1");
        Thread thread2 = new Thread(groupA, new Task(), "Thread-2");
        Thread thread3 = new Thread(groupB, new Task(), "Thread-3");
        Thread thread4 = new Thread(groupB, new Task(), "Thread-4");

        // 启动所有线程
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

        // 获取组A中的活动线程数
        System.out.println("Group A active threads: " + groupA.activeCount());

        // 列出组B中的所有线程
        System.out.println("Group B threads:");
        Thread[] groupBThreads = new Thread[groupB.activeCount()];
        groupB.enumerate(groupBThreads);
        for (Thread t : groupBThreads) {
            System.out.println(t.getName());
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " from " +
                        Thread.currentThread().getThreadGroup().getName() + " is running");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程分组的实际应用场景

  1. web 服务器:将处理不同请求类型的线程分组(如 Http 请求,WebSocket 连接等)
  2. 游戏开发:将渲染线程、物理计算线程、AI 线程等分组管理
  3. 数据处理:将数据读取、处理和写入的线程分组
  4. 交易系统:将不同优先级或不同类型的交易处理线程分组

通过线程分组可以更有效的管理系统资源、简化线程管理逻辑,并提高代码的可维护性

如何理解 wait、notify、notifyAll?为什么这三个方法不是 Thread 类中的,而是 Object 类中的,使用过程中有什么需要注意的吗?

Wait、notify、notifyAll 是 Object 类中的重要方法,用于线程之间的协调通信,它们是 java 多线程编程中实现线程间下作的基础机制。

wait方法:让当前线程进入等待状态,并释放所持有的锁,直到被唤醒

  • 使当前线程进入等待状态,释放对象的锁
  • 必须在同步代码块或同步方法中调用(即必须先获得对象的监视器锁(对象的监视器锁指的是 jvm级别的锁的内置实现))
  • 调用后线程会一直等待,直到其他线程调用对象的 notify 或 notifyAll()方法
  • 有三个重载版本
    • wait() - 无限等待
    • wait(long timeOut) - 等待指令毫秒数
    • wait(long timeOut, int nanos) - 更精确的等待时间

notify 方法:随机唤醒一个正在等待该对象锁的线程

  • 唤醒一个正在等待该对象监视器的线程
  • 同样该方法必须在同步代码块或同步方法中调用
  • 如果有多个线程在等待,JVM 会任意选择一个线程唤醒,被唤醒的线程不会立刻执行,会重新竞争获取锁。

notifyAll 方法:唤醒所有正在等待该对象锁的进程,让它们竞争锁

  • 唤醒所有正在等待该对象监视器的线程
  • 这些线程将竞争获取锁对象
  • 通过比 notify()更安全,避免某些线程永远等待

为什么这三个方法定义在 Object 类,而不是 Thread类?

这是一个设计上的关键决策,原因如下

  1. 锁是基于对象的(Monitor 机制)

    • java 的同步机制是基于对象的,每个对象都有一个内置锁(Monitor)
    • wait()、notify()和 notifyAll() 是锁的协作机制,必须和 Synchronized 配合使用,如果它们定义在 Thread类,就无法和对象锁直接关联,导致设计混乱
  2. 线程可以等待多个不同的对象

    • 一个线程可以同时持有多个对象的锁,并在不同条件下等待:

      java
      synchronized (lock1) {
          synchronized (lock2) {
              lock1.wait();  // 释放 lock1,但仍持有 lock2
          }
      }
    • 如果 wait() 是 Thread 的方法,就无法指定等待那个对象的锁。

  3. notify 需要明确目标对象,如果 notify 是 Thread 的方法,就无法精确控制唤醒哪个锁上的线程

  4. 更符合面向对象的设计

注意事项

  1. 必须在同步代码块中使用,否则会抛出 IllegalMonitorStateException
  2. 通常 Wait() 方法应该放在循环中检查条件(使用 While 而不是 if)防止虚假唤醒
  3. notify 和 notifyAll 的使用取决于具体使用场景
  4. 被 notify()或 notifyAll()唤醒的线程在重新获取锁后,会从 wait() 调用之后继续执行,而不是从头开始执行同步块(wait的时候或保存上下文环境)
  5. 从 java 5 开始,更推进使用 java.util.concurrent 包中的高级同步工具,如 Conditon,BlockingQueue 等)

线程池中的线程抛出了异常,如何处理?

当线程池中抛出异常时,如果不处理,这些异常信息会被“吞掉”,导致难以调试问题。

常见处理方式:

  1. 使用 execute(Runnable)提交任务

    • 手动在任务内存 try-catch

      java
      executor.execute(() -> {
          try {
              // 你的逻辑
          } catch (Exception e) {
              // 异常处理逻辑,比如记录日志
              e.printStackTrace();
          }
      });
  2. 使用 submit(Callable) 或 submit(Runnable)提交任务

    • 异常会被封装在 future 中,不会立即抛出,需要通过 future.get()主动捕获。

      java
      Future<?> future = executor.submit(() -> {
          // 你的逻辑
          throw new RuntimeException("出错了");
      });
      
      try {
          future.get(); // 会在这里抛出 ExecutionException
      } catch (ExecutionException e) {
          // 处理任务内部异常
          Throwable cause = e.getCause(); // 原始异常
          cause.printStackTrace();
      } catch (InterruptedException e) {
          Thread.currentThread().interrupt(); //主线程保留中断状态,可用于防止中断丢失
      }
  3. 自定义线程池的 afterExecute 方法

    • 如果你使用的是 ThreadPoolExecutor,可以继承它并覆盖 afterExecute 方法,全局统一处理未捕获异常

      java
      package com.hongsipeng.juc;
      
      import java.util.concurrent.*;
      
      /**
       * @author hongsipeng
       * @apiNote 线程池自定已捕获异常
       * @since 2025/8/8
       */
      public class ThreadPoolExecutorExample {
          public static void main(String[] args) {
              ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,
                      1,
                      20L,
                      TimeUnit.MINUTES,
                      new LinkedBlockingQueue<>()){
                  /**
                   * Method invoked upon completion of execution of the given Runnable.
                   * This method is invoked by the thread that executed the task. If
                   * non-null, the Throwable is the uncaught {@code RuntimeException}
                   * or {@code Error} that caused execution to terminate abruptly.
                   *
                   * <p>This implementation does nothing, but may be customized in
                   * subclasses. Note: To properly nest multiple overridings, subclasses
                   * should generally invoke {@code super.afterExecute} at the
                   * beginning of this method.
                   *
                   * <p><b>Note:</b> When actions are enclosed in tasks (such as
                   * {@link FutureTask}) either explicitly or via methods such as
                   * {@code submit}, these task objects catch and maintain
                   * computational exceptions, and so they do not cause abrupt
                   * termination, and the internal exceptions are <em>not</em>
                   * passed to this method. If you would like to trap both kinds of
                   * failures in this method, you can further probe for such cases,
                   * as in this sample subclass that prints either the direct cause
                   * or the underlying exception if a task has been aborted:
                   *
                   * <pre> {@code
                   * class ExtendedExecutor extends ThreadPoolExecutor {
                   *   // ...
                   *   protected void afterExecute(Runnable r, Throwable t) {
                   *     super.afterExecute(r, t);
                   *     if (t == null
                   *         && r instanceof Future<?>
                   *         && ((Future<?>)r).isDone()) {
                   *       try {
                   *         Object result = ((Future<?>) r).get();
                   *       } catch (CancellationException ce) {
                   *         t = ce;
                   *       } catch (ExecutionException ee) {
                   *         t = ee.getCause();
                   *       } catch (InterruptedException ie) {
                   *         // ignore/reset
                   *         Thread.currentThread().interrupt();
                   *       }
                   *     }
                   *     if (t != null)
                   *       System.out.println(t);
                   *   }
                   * }}</pre>
                   *
                   * @param r the runnable that has completed
                   * @param t the exception that caused termination, or null if
                   *          execution completed normally
                   */
                  @Override
                  protected void afterExecute(Runnable r, Throwable t) {
                      super.afterExecute(r, t);
                      if (t == null && r instanceof Future<?>) {
                          try {
                              Future<?> future = (Future<?>) r;
                              if (future.isDone()) {
                                  future.get(); // 触发异常
                              }
                          } catch (CancellationException ce) {
                              t = ce;
                          } catch (ExecutionException ee) {
                              t = ee.getCause(); // 原始异常
                          } catch (InterruptedException ie) {
                              Thread.currentThread().interrupt(); // 重设中断
                          }
                      }
      
                      if (t != null) {
                          // 统一处理异常,比如记录日志
                          System.err.println("任务抛出异常:" + t.getMessage());
                          // throw new RuntimeException(); //这里如果再抛出异常外面是无法感知的
                          t.printStackTrace();
                      }
      
                  }
              };
          }
      }

同步和异步、阻塞和非阻塞的区别?

同步&异步

关注点:任务的完成机制(如何等待结果)

  1. 同步(synchronous)
    • 调用方主动等待结果
    • 任务必须按顺序执行,前一个任务未完成时,后续任务会被阻塞。
    • 示例:
      • 同步函数调用(函数返回即得到结构)
      • fs.readFileSync() (Node.js同步读取文件,阻塞直到返回文件内容)
  2. 异步(Asynchronous)
    • 调用方不等待结果,通过回调、事件或通知获取结果
    • 任务发起后,程序继续执行其他操作,结果通过回调函数、promise 等机制返回
    • 示例:
      • fs.readFile (Node.js异步读取文件,通过回调返回结果)
      • AJAX请求(浏览器异步发送请求,通过回调处理响应)

阻塞&非阻塞

关注点:调用方的状态(能否做其他事)

  1. 阻塞(Blocking)
    • 调用方被挂起,直到操作完成
    • 期间无法执行其他任务
    • 示例:
      • java的 InputStream.read() (线程阻塞直到数据到达)
  2. 非阻塞(Non-blocking)
    • 调用方立即返回(无论操作是否完成)可继续执行其他任务
    • 需要通过轮询,事件监听等方式检查结果
    • 示例:
      • select() 或 epoll(linux 非阻塞 I/O,检查文件描述符状态)
      • Node.js的事件循环(单线程处理多个非阻塞 I/O)

什么是死锁?如何避免死锁?

死锁是指多个进程(线程)在执行过程中,因争夺资源而造成的一种互相等待现象,导致这些进程都无法执行下去。

死锁的四个必要条件(Coffman条件)

  1. 互斥条件(Mutual Exclusion):资源一次只能被一个进程占用
  2. **占有并等待(Hold and Wait):**进程持有至少有一个资源,同时等待获取其他被占用的资源。
  3. **非抢占(No Preemption):**已分配给进程的资源不能被强行剥夺,必须有进程自行释放。
  4. **循环等待(Circular Wait):**多个进程形成一种头尾相接的循环等待关系

如何避免死锁?

  1. 破坏死锁的条件
    • 破坏互斥条件
      • 使用共享资源(如读写锁),但某些资源(如打印机)无法共享
    • 破坏占有并等待:
      • 一次性申请所有资源(如银行家算法),避免部分持有
      • 如果无法获取全部资源,则释放已持有的资源(超时回退)。
    • 破坏非抢占条件:
      • 允许系统强制剥夺资源(如数据库死锁检测后回滚事务)。
    • 破坏循环等待:
      • 按固定顺序申请资源(如所有线程必须先申请锁 A,再申请锁 B)
  2. 死锁预防策略
    • 资源有序分配法:
      • 所有线程必须按照全局统一的顺序申请锁(如锁 A-> 锁 B-> 锁 C),避免循环等待。
    • 超时机制:
      • 如果线程在指定时间内未获取所有资源,则释放已持有的锁并重试(如 tryLock(timeOut))
  3. 死锁检测与恢复
    • 检测:
      • 维护资源分配图(RAG),定期检测是否存在环路
      • 数据库、操作系统等场景常用(如 MySQL 死锁检测)
    • 恢复:
      • 终止进程:强制终止部分进程(如优先级低的进程)
      • 回滚:让部分进程回滚到安全状态(如事务回滚)

什么是活锁?什么是无锁?

活锁(Livelock)

活锁是指多个线程(或进程)在执行任务时,由于互相谦让或不断改变状态,导致任务无法推进,但线程本省并未阻塞,仍然在消耗 CPU 资源。

特点:

  • 线程没有阻塞(仍在运行),但任务没有进展。
  • 通常由不合理的重试策略或冲突解决机制导致。

示例:

  • 两个线程互相让路:
    • 线程 A 和 线程 B 都需要通过一个狭窄的通道,但它们每次都同时向同一侧移动,导致永远无法通过。
  • 消息队列的重试冲突:
    • 两个服务互相发送(失败-重试)消息,导致无想循环。

如何避免活锁?

  • 引入随机退避(Random Backoff):
    • 让线程在冲突时随机等待一段时间(如 TCP 拥塞控制)
  • 限制重试次数:
    • 超过一定次数后放弃或执行降级策略。
  • 调整调度策略:
    • 让其中一个线程优先执行(如设置优先级)

无锁(Lock-Free)

无锁编程是指不使用传统锁(如 mutex、synchronized)来实现并发安全,而是依赖原子操作(CAS,Atomic)或无锁数据结构(如无锁队列)。

特点:

  • 无死锁风险(因为没有锁竞争)
  • 高并发性能(减少线程阻塞)
  • 可能面临 ABA 问题(需要特殊处理,如版本号)

实现方式:

  • CAS

    java
    AtomicInteger value = new AutomicInteger(0);
    value.compareAndSet(0,1)//如果当前值是 0,则设置为 1
  • 无锁数据结构:

    • 如 ConcurrentLinkedQueue(Java无锁队列)

适用场景:

* 高并发计数器(如 AtomicLong)
* 无锁队列/栈 (如 Disruptor 框架)

AtomicInteger 的底层实现是怎样的?

AtomicInteger 是 Java 中用于实现原子操作的整数类,其底层实现主要依赖于 CAS(Compare-And-Swap)指令volatile 变量Unsafe 类(或 JDK 9+ 后的 VarHandle)。以下是其核心实现原理:

  1. volatile 保证可见性

    AtomicInteger 内部通过 volatile 修饰的 int 值保证多线程下的可见性:

    java
    private volatile int value;
    • volatile 确保对 value 的修改能立即被其他线程看到,避免脏读。
  2. CAS 实现原子操作

    核心方法(如 getAndIncrement()compareAndSet())通过 CAS 机制实现无锁线程安全:

    java
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    • Unsafe 类:通过本地方法(Native)调用硬件级别的 CAS 指令(如 x86 的 LOCK CMPXCHG)。
    • CAS 流程
      1. 读取当前值 oldValue
      2. 计算新值 newValue = oldValue + delta(如 +1)。
      3. 比较内存中的当前值是否仍为 oldValue
        • 如果是,更新为 newValue,返回成功。
        • 否则,重试(自旋)或失败。
  3. 偏移量(Offset)与内存操作

    通过字段的内存偏移量直接操作内存

    java
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    • valueOffsetvalue 字段在对象内存中的偏移地址,用于 Unsafe 直接读写该字段。
  4. JDK9+ 的优化:VarHandle

    在 JDK 9 之后,AtomicInteger 逐步改用 VarHandle 替代 Unsafe

    java
    private static final VarHandle VALUE = MethodHandles.lookup()
        .findVarHandle(AtomicInteger.class, "value", int.class);
    • VarHandle 提供了更安全的底层内存操作 API,功能类似 Unsafe,但权限可控
  5. 性能优势

    • 无锁化:相比 Synchronized,CAS 减少了线程阻塞和上下文切换开销
    • 自旋优化:在低竞争场景下效率极高(如 AtomicInteger 的计数器场景)。
  6. 局限性

    • ABA 问题:CAS 无法感知值从 A→B→A 的变化。可通过 AtomicStampedReference 加入版本号解决。
    • 高竞争性能下降:自旋可能导致 CPU 空耗,此时可用 LongAdder 分段计数优化。

什么是 CAS?CAS 有什么缺点?CAS 底层使用了哪个操作类?

什么是 CAS

CAS 是一种 原子操作,在多线程编程中用于实现无锁(lock-free)的并发控制。它的核心思想是:

  • 给定一个内存位置 V,期望的旧值 A,以及新值 B
  • CAS 会做如下检查:
    • 如果 V 中的值等于 A,则将其更新为 B,并返回 成功
    • 如果 V 中的值不等于 A,则不做修改,并返回 失败

公式化理解:

java
CAS(V, A, B):
    if (V == A)
        V = B
        return true
    else
        return false

这是一种乐观锁思想:认为冲突少,先尝试更新,不行再重试。

CAS 的缺点

虽然 CAS 提供了高效的无锁操作,但也有一些缺陷:

  1. ABA 问题
    • 如果某个变量原来是 A,被线程 1 读到;
    • 然后线程 2 把它改成 B,又改回 A;
    • 线程 1 再做 CAS 时,发现还是 A,就会以为没变,其实数据状态已经被改过。
    • 解决办法:加上 版本号(比如 AtomicStampedReference)。
  2. 自旋开销大
    • 如果竞争激烈,CAS 会一直自旋重试,消耗 CPU
    • 适合低竞争场景,高竞争下可能不如锁。
  3. 只能保证一个变量的原子性
    • 对多个共享变量操作时,CAS 不能保证整体原子性,需要用锁或者AtomicReference等封装。

CAS 底层使用了哪个操作类?

在 java 中,CAS 的实现主要依赖Unsafe类提供本地方法(CompareAndSwapInt、CompareAndSwapLong、CompareAndSwapObject

  • Unsafesun.misc.Unsafe,提供了一组可以直接操作内存的 native 方法。
  • 实际上,Unsafe 底层调用的是 CPU 的原子指令,比如:
    • x86 架构上的 cmpxchg 指令
    • ARM 架构上的 LDREX/STREX
  • 所以 Java 中的 AtomicIntegerAtomicLong 等类,都是基于 Unsafe + CAS 来实现的。

Java 9 之后,虽然官方逐渐对 Unsafe 做了限制,但 CAS 的底层实现仍然基于 Unsafe,只是 JDK 对外提供了更规范的 API 来替代直接调用 Unsafe

  1. java 9 之后的情况

    • Unsafe 依然存在,内部类库(例如 AtomicIntegerConcurrentHashMap)还是用它来做底层 CAS。
    • 但 JDK 引入了更“安全”的公开类
    • VarHandle(在 java.lang.invoke 包里)
      • 可以用来替代 Unsafe 做字段、数组、变量的原子操作。
      • 提供了类似 compareAndSet 的方法,底层还是调用 Unsafe 或者 JVM intrinsic(内联的 CPU 指令)。
    • java.util.concurrent.atomic的类(如 AtomicIntegerAtomicLong)在 JDK 9+ 里逐步过渡到基于 VarHandle 实现,而不是直接写 Unsafe
  2. 为什么要这样做?

    • Unsafe 不安全:它能直接操作内存地址,容易破坏 JVM 安全模型。
    • 模块化系统(Java 9 的 Jigsaw)Unsafe 属于内部 API,不再默认对外暴露。
    • VarHandle 是官方标准:提供与 Unsafe 类似的功能,但有更清晰的权限控制和更好的可维护性。
  3. 底层原理

    不管是 Unsafe 还是 VarHandle,最终 CAS 的实现还是落到 JVM intrinsic,调用的是 CPU 的原子指令(如 cmpxchg)。

    你可以理解为:

    • Java 8 及之前:直接用 Unsafe.compareAndSwapXxx()
    • Java 9 之后:对外推荐用 VarHandle,内部 AtomicXxx 类有时用 VarHandle,有时还保留对 Unsafe 的调用(为了兼容)。
    • 本质:还是 CPU 提供的 CAS 指令。

CAS 在 JDK 中有哪些应用?

CAS(Compare-And-Swap)几乎是 Java 并发包的基石,JDK 中大量的并发工具类都是基于它实现的。

  1. 原子类(java.util.concurrent.atomic

    最直接的应用就是原子变量类:

    • AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference

    • 它们的 getAndIncrement()compareAndSet() 等方法,内部就是通过 CAS + 自旋 来更新变量值。
      👉 取代了 synchronized 的加锁方式。

  2. 并发容器

    很多并发容器的底层原理操作依赖 CAS 来保证无锁更新:

    • ConcurrentHashMap
      • 在插入新节点时,使用 CAS 更新桶位数组。
      • 在扩容时,也用 CAS 保证迁移过程安全
    • ConcurrentLinkedQueue/ConcurrentLinkedDeque
      • 用 CAS 来更新队列的 headtail 指针,实现无锁队列。
    • LinkedTransferQueue
      • 用 CAS 维护链表的插入/匹配操作
  3. 同步器框架(AQS)

    AbstractQueuedSynchronizer(AQS)是 java 并发锁(ReentrantLockCountDownLatchSemaphore)的核心框架。

    • 内部有一个 State 变量,用来表示锁的状态。
    • AQS 通过 CAS 修改 State 来实现加锁/解锁操作(例如 tryAcquiretryRelease

    例如:

    java
    if (compareAndSetState(0, 1)) {
        // 成功获取锁
    }
  4. 线程池

    • ThreadPoolExecutor
      • 使用 CAS 来维护 线程池的运行状态(ctl 变量),该变量同时记录了线程池状态和工作线程数。
      • 避免了对线程池状态修改时的锁竞争。
  5. StampedLock

    • StampedLock 中,读写锁的状态值也是通过 CAS 修改的,避免传统 ReentrantReadWriteLock 的锁开销。
  6. Future/CompletableFuture

    • CompletableFuture 在完成任务时,使用 CAS 来更新结果状态,保证只有一个线程能成功设置结果。
  7. ThreadLocalRandom

    • ThreadLocalRandom 在更新种子值时用 CAS,保证并发环境下更新无锁安全。
  8. 其他典型应用

    • LongAdder / DoubleAdder
      • 针对 AtomicLong 在高并发下的 CAS 自旋热点问题,LongAdder 采用 分段累加 + CAS,提升吞吐量。
    • StampedReference / AtomicMarkableReference / AtomicStampedReference
      • 这些是 CAS 的 ABA 问题解决方案,通过版本号或标记位来避免 ABA。

用伪代码写一个 CAS 算法的核心

CAS 算法的核心在于 比较+更新,只有内存值和期望一致时才更新;如果 CAS 失败,就再次尝试;无锁实现,也能保证线程安全。

java
function CAS(address, expectedValue, newValue):
    oldValue = *address            // 取出内存中的当前值
    if oldValue == expectedValue:  // 判断是否与期望值一致
        *address = newValue        // 如果一致,更新为新值
        return true                // 返回成功
    else:
        return false               // 否则返回失败

多线程情况下,进行数字累加(count++)需要注意什么

在多线程情况下,count++不是一个原子操作,在并发场景下会出现问题

  1. 为什么 Count++ 线程不安全?

    count++ 实际上分为三步:

    1. 读取变量值:temp = count
    2. 执行加一:temp = temp + 1
    3. 写回变量:count = temp

    如果两个线程同时执行:

    • 线程 A 读到 count = 5
    • 线程 B 也读到 count = 5
    • A +1 → 写回 6
    • B +1 → 写回 6

    最后 count = 6,而不是期望的 7。
    👉 出现了 竞态条件(race condition)

  2. 解决方案

    1. 加锁 使用 Synchronized 或者 ReetrantLock,简单可靠,有锁开销,性能可能不是很高
    2. 使用原子类(推荐),基于 CAS,无锁,性能高,在高并发场景下自旋重试开销大
    3. 使用 LongAdder(高并发推荐)
      1. LongAdder 内部把一个变量分成多个 Cell,减少 CAS 热点竞争。
      2. 在高并发下比 AtomicInteger 性能更好。

有了 AtomicInteger,为什么 JDK 又出了个 LongAdder?不同的使用场景?

AtomicInteger的问题

AtomicInteger 内部是基于 CAS + 自旋重试 来保证原子性的:

java
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

低并发 时,这种方式很高效(无锁化,避免阻塞)。

但是在 高并发 时,很多线程同时去 CAS 修改同一个 value 字段,竞争非常激烈,导致自旋失败率高,重试次数多,CPU 开销大。
👉 出现 热点竞争(hot spot contention)

LongAdder 的改进

  • JDK8 引入 LongAdder,专门解决高并发下的技术性能问题。
  • 核心思路:分段累加
    • LongAdder 内部维护一个 base变量+一个 Cell[] 数组。
    • 低并发时:直接累加 base (类似 AtomicInteger,快)。
    • 高并发时:把冲突的线程分到 Cell[] 的不同槽位上,各自执行CAS更新,减少竞争。
    • 需要获取最终值时,把 base +所有 Cell[] 的值加起来

使用场景对比

特性AtomicIntegerLongAdder
实现方式单个变量 + CAS分段累加(base + Cells)
适用场景低并发、对实时结果要求高高并发、大量累加操作(计数、统计)
性能并发低时快,但高并发下自旋开销大高并发性能优越(分散竞争)
读取结果get() 即可,实时准确sum() 汇总,可能有微小延迟
内存占用低(一个变量)较高(Cell 数组,额外内存)

举例:

  • 用 AtomicInteger
    • 适合线程数量不大(几十个线程以内),比如 计数器、序列号生成器
    • 需要实时获取准确值的场景。
  • 用 LongAdder
    • 适合高并发、大规模累加统计,比如:
      • 网站的 访问次数统计
      • 监控系统的 请求 QPS 计数
      • 日志系统统计 日志条数
    • 这些场景下,少量的读不需要实时绝对精确,性能更重要。

LongAdder 为什么性能更好,原理是什么?那有没有什么缺点呢?

LongAdder 是 JDK 8 引入的高并发计数器,在高并发写(累加)场景下性能明显优于 AtomicLong。你可以从你平时做服务端统计、限流、监控指标这类场景来理解,它本质上是用空间换时间,减少 CAS 冲突

把一个热点变量拆分成多个分散变量,让线程各自更新,最后再求和,有效的降低了高并发场景下的自旋冲突

LongAdder 的核心原理(重点)

基本结构

LongAdder
 ├── base      (一个 long,低并发时用)
 └── Cell[]    (高并发时用,多个分段)
        ├── Cell[0].value
        ├── Cell[1].value
        ├── Cell[2].value
        └── ...

累加流程(add)

伪流程是这样的:

public void add(long x) {
    if (cells == null) {
        // 低并发:直接 CAS base
        if (CAS(base, base + x)) return;
    }
    // 高并发:
    // 1. 根据线程 hash 定位一个 Cell
    // 2. CAS 修改这个 Cell.value
    // 3. 冲突了才扩容 cells
}

关键点

  • 先尝试 base
  • CAS 失败 → 启用 Cell 数组
  • 线程被 hash 到不同 Cell
  • Cell 之间完全无竞争

线程是如何“分散”的?

LongAdder 使用:

  • ThreadLocalRandom 的 probe 值
  • 计算:index = probe & (cells.length - 1)

👉 不同线程命中不同 Cell
👉 冲突概率大幅下降


读取(sum)

long sum() {
    long result = base;
    for (Cell c : cells) {
        result += c.value;
    }
    return result;
}

注意:不是原子快照!

缺点

sum() 不是原子操作,在 sum 的过程中,其他线程仍然在 add,得到的是一个近似值。不能用于强一致性场景;

因为采用了分散的思想,Cell 对象更多,更占用内存

不支持 compareAndSet 语义,不能做条件更新,只能做累加操作

LongAccumulator是什么?和 LongAdder怎么选?

LongAccumulator 是 LongAdder 的泛化版本,允许自定义累积函数,适合高并发下做最大值、最小值等统计;如果只是计数,优先选 LongAdder,性能更好、语义更清晰。

官方抽象

  • LongAdder只能做加法
  • LongAccumulator可以做任意二元累积运算
LongAccumulator accumulator =
    new LongAccumulator((x, y) -> x + y, 0L);

👆 这个写法本质上就等价于 LongAdder

并行和并发的区别?

**并行:**同一时刻真正处理多件事,需要多核

**并发:**同一时间段内处理多件事(交替进行),不需要多核

常见误区

❌ 误区 1:多线程 = 并行

错 ❌
单核 CPU 下,多线程只是并发,不是并行


❌ 误区 2:并行一定比并发快

错 ❌

  • 线程创建
  • 上下文切换
  • 竞争锁

👉 小任务并行反而更慢


❌ 误区 3:并发和并行只能选一个

错 ❌

可以同时存在

  • 系统层面:并发设计
  • 执行层面:并行运行

为什么不推荐使用 stop 停止线程?如何优雅的终止一个线程?

Thread.stop() 会在任意时刻强制终止线程并释放锁,破坏数据一致性和锁语义,因此被废弃;正确的做法是通过 interrupt 或共享退出标志,让线程自行、安全地结束。

为什么推荐

  1. 会直接杀死线程,不管线程在干什么

    stop() 的行为是:

    • 在线程任意执行点抛出 ThreadDeath
    • 立即终止执行

    👉 不给线程任何“善后”的机会

  2. 会破坏对象状态一致性(核心问题)

    这是最致命的问题

    java
    synchronized (obj) {
        obj.a = 1;
        obj.b = 2;
    }

    如果线程在这个时候被强制 stop():

    • a = 1 已执行
    • b = 2 还没执行
    • 锁被强制释放

    👉 其他线程看到的是不一致的对象状态

  3. 会强制释放锁(非常危险)

    • stop() 会释放所有持有的锁
    • 不管临界区是否执行完

    👉 直接破坏 synchronized 的原子性保障

  4. finally 可能执行不完整

    • 资源释放逻辑(关闭链接、回滚事务)
    • 可能被跳过或中断

    👉 容易造成:

    • 资源泄漏
    • 脏数据
    • 死锁隐患

stop 的本质问题是线程被外部“强制终止”,而不是“自己决定退出”

优雅终止线程的正确方式(重点)

核心原则

线程自己检查“退出信号”,然后正常 return / break


方式一:使用 interrupt()(最推荐)

1️⃣ 原理
  • interrupt() 不会杀线程
  • 只是设置一个 中断标志
  • 线程自己决定怎么处理

2️⃣ 正确写法(标准模板)
class Worker implements Runnable {
    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                // 执行任务
                doWork();
            }
        } catch (InterruptedException e) {
            // 收到中断信号
            Thread.currentThread().interrupt(); // 恢复中断状态
        } finally {
            cleanup(); // 资源释放
        }
    }
}
Thread t = new Thread(new Worker());
t.start();

// 终止线程
t.interrupt();

3️⃣ 为什么这是“优雅”的?
  • 不抢锁
  • 不破坏状态
  • 能执行 finally
  • 线程自己退出

方式二:使用 volatile / AtomicBoolean 标志位

适用场景
  • 线程不会阻塞(不 sleep / wait / IO)
  • 纯计算或轮询任务
class Worker implements Runnable {
    private volatile boolean running = true;

    public void stop() {
        running = false;
    }

    @Override
    public void run() {
        while (running) {
            doWork();
        }
    }
}

interrupt vs 标志位(怎么选)

场景推荐方式
sleep / wait / joininterrupt
BlockingQueueinterrupt
NIO / Selectorinterrupt
纯 CPU 计算volatile
线程池任务interrupt

线程池中如何优雅停止?(工程常见)

正确关闭线程池

executor.shutdown();          // 拒绝新任务
executor.awaitTermination(...);
executor.shutdownNow();       // 中断正在执行的线程
  • shutdownNow() 内部就是调用 interrupt()

绝对不要用的方式 ❌

方式问题
Thread.stop()破坏一致性
Thread.suspend()死锁
Thread.resume()不安全
强制 kill 线程不可控

什么是重入锁?重入锁有哪些重要的方法?如何使用?

如何理解重入锁的重入?最多可以重入多少次?

Synchronized 是重入锁吗?是怎么实现的?

Synchronized 与 ReetrantLock 的区别?

Synchronized 同步锁有哪几种用法?

Synchronized 锁的是什么?Synchronized 关键字的底层实现原理?

Synchronized 可以保证原子性吗?可以保证可见性吗?可以保证有序性吗?如何实现的?

Java 对 Synchronized 进行了哪些优化?

什么是读写锁?有没有比 ReedWriteLock 读写锁更快的锁?

有哪些锁优化的方式?

什么是自旋锁?

什么是锁消除?

什么是锁粗化?

什么是锁升级?锁升级的过程是怎么样的?

什么是偏向锁?

什么是轻量级锁?

什么是重量级锁?

什么是线程池?使用线程池有什么好处?

谈谈多线程中的 ExecutorSevice 接口和 ThreadPoolExecutor类?

线程池的工作流程是怎样的?

线程是 ExecutorService 和 Executors 的区别?

Java 里面有哪些内置的线程池?

为什么阿里巴巴开发手册不让用 Executors 工具类来创建线程池?

线程池的拒绝策略有哪几种?

线程池 submit 和 execute 有什么区别?

如何查看线程池的运行状态?

如何设置线程池的大小?

如何关闭线程池?

谈谈多线程中的 CompletionService 接口?CompletionService 用完需要关闭吗?怎么关闭?

谈谈多线程中的 ExecutorCompletionService类?

Java 实现异步编程有什么方案?

Future 是什么?编程实现一个 Future 的使用例子?

FutureTask 是什么?编程实现一个 FutureTask 的例子?

Future 和 FutureTask的区别?

CompletableFuture 是什么?有哪些应用场景?

CompletableFuture 和 Future 的区别?

CompletableFuture 的 get 和 join 的区别?

CompletableFuture 的 thenApply 和 thenCompose 区别?

CompletableFuture 如何处理异常?

CompletableFuture默认使用的什么线程池?

CompletableFuture 如何自定义线程池?

CompletableFuture 如何优化性能?

编程实现一个 CompletableFuture 的使用例子?

谈谈多线程中的 CompletionStage 接口?

AQS 是什么?底层原理什么什么?

Fork Join框架有什么用?

Fork Join 框架的运行流程?

Fork Join 框架底层什么机制?

Fork Join 框架核心类有哪些?

使用 Fork Join 框架有什么需要注意的?

编程实现使用 Fork Join 框架的例子?

ThreadLocal 有什么用?

ThredLocal 的底层实现怎么样的?

ThreadLocal中的 Key 为什么需要设计为弱引用?

ThreadLocal 为什么会导致内存溢出?

编程实现 ThreadLocal 的使用例子?

ThreadLocal 父子线程如何传递数据?

ThreadLocal 与 InheritableThreadLocal 的区别?

InheritableThreadLocal 的底层实现原理?

volatile 关键字有什么用?

volatile 有哪些应用场景?

volatile 可以保证原子性吗?可以保证可见性吗?可以保证有序性吗?怎么实现的?

Volatile 可以代替 Synchronized 使用吗?为什么?

CountDownLatch 有什么用?编程实现一个 CountDownLatch 的使用示例?

CyclicBarrier 有什么用?编程实现一个例子吗?

CountDownLatch 与 CyclicBarrier 的区别?

Semaphore 有什么用?编程实现一个使用用例?

Exchanger 有什么用?

编程实现两个线程彼此交换数据的例子?

LockSupport 有什么?

LockSupport 和 wait-notify-notiofyAll有什么区别?

编程实现一个 LockSupport 阻塞唤醒线程的使用示例

Java 中原子操作的类有哪些?

什么是 ABA 问题?如何解决 ABA 问题?

什么是 Happens-Before 原则?Happens-Before 原则有哪些?

Java并发容器有哪些?

什么是协程?有了多线程为什么还有搞出协程?

Java 支持协程吗?支持协程的框架有哪些?

SimpleDateFotmat是线程安全的吗?为什么?如何解决?

什么是 ParallelStream?和 Stream 的区别?底层原理什么?

ParallelStream 是线程安全的吗?默认启动了多少个线程?如何修改默认线程数?

本站访客数 人次 本站总访问量