JAVA 多线程面试题
[toc]
什么是原子性?
原子性指的是一个或者多个操作,要么全部执行,要么都不执行,再执行的过程中,不被其他操作中断或干扰。
在并发过程中,原子性是一个非常重要的概念,如果操作不是原子的,在多线程并发环境下,多个线程可能访问同一个变量,可能会导致数据不一致。
Java 中基本数据类型的赋值和读取是原子的吗?
处理long 和 double (可以通过 volatile 修饰实现原子性)注意:volatile 修饰的 i++,i--操作不具有原子性,volatile 主要是为了解决可见性,其他的基本数据类型的读取和赋值都是原子性的。
int x = 100;//原子
x++;//非原子
int y = x;//非原子
x = x + 1;//非原子上面的代码只有第一行是原子的,其他三个都包含两个以上的操作,首先都是要求读取 x 变量的值,经过计算后再将新的值写入到内存中去
volatile的局限性
- 不保证原子性:
- 对
volatile变量的复合操作(如i++)仍是非原子的,可能被线程切换打断。- 不解决指令重排序:
- 编译器或 CPU 仍可能重排
volatile与非volatile操作的顺序(除非配合内存屏障)。- 不适用于多线程同步:
- 现代多核 CPU 的缓存一致性协议(如 MESI)通常能自动同步内存,
volatile的可见性作用被弱化,而原子性和顺序性成为更关键的问题。
什么是可见性?如何保证可见性?
可见性是指一个线程对共享变量的修改,其他线程可以立刻看到该值。
多个线程可能会访问同一个变量,由于线程本地缓存(如 cpu 缓存)的存在,一个线程对变量的修改,但没有及时刷新到到主内存,其他线程读取的可能是旧的副本值,而不是最新的。这就导致了可见性问题。
如何保证可见性?
volatile 关键字。
- volatile 保证的了对变量的写操作会立刻写入到主内存中去。
- 对变量的读操作会从主内存中读取最新值。
- 保证可见性,但是不保证原子性。
使用 Synchronized 或 lock
Synchronized 关键字不仅保证互斥性(即同一个时刻只有一个线程执行临界区代码)还保证可见性:进入 Synchronized 代码块时,会清空本地缓存,从主内存中读取最新值;退出时,会将变量刷新到主内存中去。
使用 final(初始化时的可见性)
使用 final 字段,java 在构造函数完成后,会确保所有线程都能够看到该字段的正确值。
什么是有序性?如何保证有序性?
有序性指代码运行的顺序符合代码编写时的先后顺序。
但是在实际执行中,出于优化的目的(如提高性能),编译器、CPU、和 java 虚拟机都可能对代码进行“指令重排序”,从而打破原本代码书写的顺序。这就可能导致并发程序中产生预期之外的结果。
如何保证有序性?
volatile 关键字
限制了某些类型的指令重排序
写入 volatile 变量之前的所有操作,在内存语义上都会在写操作之前完成
保证了 写-读操作的有序性。
javavolatile boolean ready = false; int a = 0 //线程 A修改值 a=42; read = true; //volatile 写屏障,强制前面的先执行 //线程 B if (ready) { System.out.println(a);//保证看到 a=42 }
Synchronized / lock
- Synchronized 不仅保证互斥性和可见性,还会禁止同步块内的重排序。
- 进入同步块:读取主内存,禁止重排序
- 退出同步块;刷行变量到主内存,保证前后代码的顺序。
内存屏障(Memory Barrier)或 happens-before 规则(JMM 内存模型)
- Java 内存模型通过 happens-before 关系来定义哪些操作之间是有序的。
- 比如:
- Synchronized 之间的操作是有序的
- volatile 写->读是有序的
- 线程 start() 之前的操作,对新线程是可见且有序的
- 线程 join()之后的操作,一定在被 join 的线程所有操作之后
为什么要使用多线程?
更好的利用多核 cpu(并行计算)
提升程序的响应速度(改善用户体验)
提高程序执行效率(cpu利用率),防止阻塞
单线程程序在 io 时会阻塞,cpu 闲置,多线程可以在一个线程等待时,其他线程继续工作,从而充分利用 cpu。
创建线程的几种方式?
继承 Thread 类重新 run 方法
实现 runable 接口重写run 方法
实现 Callable 接口+FutureTask(有返回值)
javaimport 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 | 终止状态:线程执行完毕或异常退出 |
生命周期:
┌──────────┐
│ NEW │
└────┬─────┘
│ start()
▼
┌────────────┐
│ RUNNABLE │
└───┬─┬──────┘
│ │
│ └────────────┐
▼ ▼
┌─────────┐ ┌──────────────┐
│ BLOCKED │ │ WAITING │◄─────────┐
└────┬────┘ └──────────────┘ │
│ ▲ │
│ │ │
▼ │ │
┌──────────────┐ │ ┌──────────────┐
│ TIMED_WAITING│───────┘ │TERMINATED │
└──────────────┘ └──────────────┘线程的优先级有什么用?
它的作用是"影响线程被 CPU 调度的概率"
- 高优先级线程在理论上更有可能被操作系统线程调度器选中。
- 但这只是一个指标,具体行为依赖与操作系统和 JVM 实现。不能依赖它来保证顺序性或者公平性。(优先级不是抢占式控制,不是强制调度!)
java 中每个线程都有一个整数优先级,取值范围是:
| 常量名 | 值 | 说明 |
|---|---|---|
Thread.MIN_PRIORITY | 1 | 最低优先级 |
Thread.NORM_PRIORITY | 5(默认) | 普通优先级 |
Thread.MAX_PRIORITY | 10 | 最高优先级 |
可用通过以下当时设置优先级:
Thread t = new Thread(...);
t.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级线程优先级只是调度建议,不是强制控制,不能依赖它实现功能或控制顺序,但可用于性能调优和资源让渡。
怎么让 3 个线程按顺序执行?编程实现?
使用 join()方法,join()方法的主要作用是让当前线程等待调用 join 的线程执行完成后再继续执行
javapackage 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(); } }使用单线程池,线程池中只有一个核心线程,提交任务给线程池时,线程任务会被阻塞
javapackage 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(); //如果不加这个,程序不会终止 } }使用 Wait 和 notify 机制
javapackage 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(); } }使用 Condition 实现
javapackage 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 异常
使用 CountDownLatch
javapackage 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(); } }使用 CompletableFuture
javapackage 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()的线程执行完毕,具体来说
- 阻塞当前线程:调用某线程的 join()方法会使当前线程进入阻塞状态
- 等待目标线程执行完成:直到被 join 的线程执行完毕,当前线程才会继续执行
- 线程同步:用于协调线程的执行顺序
原理
- join方法底层是使用了 wait notify 机制,调用线程的 wait()方法使当前线程进入等待状态。
- 通过在while循环中指令 isAlive 方法判断 join 的线程是不是还存在,若存在,则当前线程一直 wait
伪代码实现
join();
while (目标线程.isAlive())
当前线程.wait()如何让一个线程休眠?
调用线程的 sleep方法比如
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 的区别?
- Thread.sleep()
- 属于 java.lang.Thread的静态方法
- 作用:让当前线程暂停执行指定的时间,进入 time_waiting 状态,不释放锁。
- 唤醒条件:时间到期后自动恢复,或线程会中断(InterruptedException)
- 使用场景:模拟延迟、控制任务执行节奏
- 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,也可能不让) | 确定(必须休眠指定时间) |
| 用途 | 提高线程间公平性 | 强制延迟执行 |
编程实现:
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 的执行次数会更多注意事项:
yield()不保证效果:- 某些 JVM 实现可能直接忽略
yield(),尤其是在单核 CPU 上。 - 现代 JVM 的线程调度通常基于优先级和时间片,
yield()的影响较小。
- 某些 JVM 实现可能直接忽略
- 与
sleep()的选择:- 若需要 明确延迟 → 用
sleep()。 - 若希望 提高线程公平性(但不强求)→ 用
yield()。
- 若需要 明确延迟 → 用
- 避免过度依赖
yield():- 线程调度的最终控制权在操作系统,
yield()仅是一个提示,不能替代正确的同步机制(如wait()/notify())。
- 线程调度的最终控制权在操作系统,
怎么理解 java 中的线程中断?中断和stop 的区别?编程实现一个线程中断的例子?
线程中断(Interruption)的理解
java 中线程中断是一种协作式线程终止机制,通过设置线程中的中断标志位(interrupt status)来请求目标线程停止执行,但不强制终止线程
- 核心思想:有被中断的线程自行决定如何响应中断(如清理资源后退出)
- 关键方法:
- thread.interrupt():设置线程中的中断标志位(若线程在阻塞状态,会触发 InterruptedException).
- thread.isInterrupted():检查当前线程是否设置中断标志位
- Thread.interrupted():检查并清楚当前线程的中断标志位(静态方法)
中断(interrupt)与 stop() 的区别
维度 中断(Interrupt) stop()(已废弃)机制 协作式(请求线程自行处理) 强制终止线程(立即停止) 安全性 安全(线程可清理资源) 不安全(可能导致资源未释放、数据不一致) 方法状态 推荐使用 已废弃(Java 1.2 后标记为 @Deprecated)触发异常 可能抛出 InterruptedException无异常,直接终止线程 为什么废弃 stop?
- 强制终止线程会立即释放所有锁,可能导致共享数据处于不一致状态(如写操作突然终止)
- 无法保证资源(如文件句柄、数据库连接)的正确释放
编程示例:线程中断的实现
场景:一个后台任务持续运行,主线程在 3 秒后中断它,任务线程检测到中断后安全退出。
javapublic 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 线程被中断,准备退出关键细节
- 阻塞方法的中断处理:
- 若线程在
sleep()、wait()、join()等阻塞方法(广义上的阻塞,线程暂停执行释放 cpu)中,调用interrupt()会触发InterruptedException,并清除中断标志位。 - 需在捕获异常后 重新设置中断标志(如
Thread.currentThread().interrupt()),否则中断状态可能丢失。
- 若线程在
- 非阻塞线程的中断检查:
- 对于未阻塞的线程,需通过
isInterrupted()主动检查中断标志位。
- 对于未阻塞的线程,需通过
- 不可中断的阻塞操作:
- 如
Socket I/O、锁获取(Lock.lock())等操作不会响应中断,需通过其他方式终止(如关闭底层资源)。
- 如
- 阻塞方法的中断处理:
中断的最佳实践
响应中断的方式:
javaif (Thread.currentThread().isInterrupted()) { // 清理资源 return; // 或抛出 InterruptedException }避免屏蔽中断
不要捕获 InterruptedException 后不做任何处理(至少恢复中断状态)
javatry { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 }
你怎么理解多线程分组?编程实现一个线程分组的例子
多线程分组是指将多个线程按照某种逻辑或功能进行分类管理。这种分组可以帮助我们更好的组合和管理线程,特别是在复杂系统中,可以按分组进行线程的批量操作(如中断、优先级设置等)
多线程分组的主要用途:
- 逻辑分类:将执行相同任务的线程归为一组
- 批量管理:可以同时对一组线程进行操作
- 资源分配:为不同组分配不同的系统资源
- 监控和调试:更容易跟踪特定功能模块的线程
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();
}
}
}
}线程分组的实际应用场景
- web 服务器:将处理不同请求类型的线程分组(如 Http 请求,WebSocket 连接等)
- 游戏开发:将渲染线程、物理计算线程、AI 线程等分组管理
- 数据处理:将数据读取、处理和写入的线程分组
- 交易系统:将不同优先级或不同类型的交易处理线程分组
通过线程分组可以更有效的管理系统资源、简化线程管理逻辑,并提高代码的可维护性
如何理解 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类?
这是一个设计上的关键决策,原因如下
锁是基于对象的(Monitor 机制)
- java 的同步机制是基于对象的,每个对象都有一个内置锁(Monitor)
- wait()、notify()和 notifyAll() 是锁的协作机制,必须和 Synchronized 配合使用,如果它们定义在 Thread类,就无法和对象锁直接关联,导致设计混乱
线程可以等待多个不同的对象
一个线程可以同时持有多个对象的锁,并在不同条件下等待:
javasynchronized (lock1) { synchronized (lock2) { lock1.wait(); // 释放 lock1,但仍持有 lock2 } }如果 wait() 是 Thread 的方法,就无法指定等待那个对象的锁。
notify 需要明确目标对象,如果 notify 是 Thread 的方法,就无法精确控制唤醒哪个锁上的线程
更符合面向对象的设计
注意事项
- 必须在同步代码块中使用,否则会抛出 IllegalMonitorStateException
- 通常 Wait() 方法应该放在循环中检查条件(使用 While 而不是 if)防止虚假唤醒
- notify 和 notifyAll 的使用取决于具体使用场景
- 被 notify()或 notifyAll()唤醒的线程在重新获取锁后,会从 wait() 调用之后继续执行,而不是从头开始执行同步块(wait的时候或保存上下文环境)
- 从 java 5 开始,更推进使用 java.util.concurrent 包中的高级同步工具,如 Conditon,BlockingQueue 等)
线程池中的线程抛出了异常,如何处理?
当线程池中抛出异常时,如果不处理,这些异常信息会被“吞掉”,导致难以调试问题。
常见处理方式:
使用 execute(Runnable)提交任务
手动在任务内存 try-catch
javaexecutor.execute(() -> { try { // 你的逻辑 } catch (Exception e) { // 异常处理逻辑,比如记录日志 e.printStackTrace(); } });
使用 submit(Callable) 或 submit(Runnable)提交任务
异常会被封装在 future 中,不会立即抛出,需要通过 future.get()主动捕获。
javaFuture<?> 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(); //主线程保留中断状态,可用于防止中断丢失 }
自定义线程池的 afterExecute 方法
如果你使用的是 ThreadPoolExecutor,可以继承它并覆盖 afterExecute 方法,全局统一处理未捕获异常
javapackage 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(); } } }; } }
同步和异步、阻塞和非阻塞的区别?
同步&异步
关注点:任务的完成机制(如何等待结果)
- 同步(synchronous)
- 调用方主动等待结果
- 任务必须按顺序执行,前一个任务未完成时,后续任务会被阻塞。
- 示例:
- 同步函数调用(函数返回即得到结构)
- fs.readFileSync() (Node.js同步读取文件,阻塞直到返回文件内容)
- 异步(Asynchronous)
- 调用方不等待结果,通过回调、事件或通知获取结果
- 任务发起后,程序继续执行其他操作,结果通过回调函数、promise 等机制返回
- 示例:
- fs.readFile (Node.js异步读取文件,通过回调返回结果)
- AJAX请求(浏览器异步发送请求,通过回调处理响应)
阻塞&非阻塞
关注点:调用方的状态(能否做其他事)
- 阻塞(Blocking)
- 调用方被挂起,直到操作完成
- 期间无法执行其他任务
- 示例:
- java的 InputStream.read() (线程阻塞直到数据到达)
- 非阻塞(Non-blocking)
- 调用方立即返回(无论操作是否完成)可继续执行其他任务
- 需要通过轮询,事件监听等方式检查结果
- 示例:
- select() 或 epoll(linux 非阻塞 I/O,检查文件描述符状态)
- Node.js的事件循环(单线程处理多个非阻塞 I/O)
什么是死锁?如何避免死锁?
死锁是指多个进程(线程)在执行过程中,因争夺资源而造成的一种互相等待现象,导致这些进程都无法执行下去。
死锁的四个必要条件(Coffman条件)
- 互斥条件(Mutual Exclusion):资源一次只能被一个进程占用
- **占有并等待(Hold and Wait):**进程持有至少有一个资源,同时等待获取其他被占用的资源。
- **非抢占(No Preemption):**已分配给进程的资源不能被强行剥夺,必须有进程自行释放。
- **循环等待(Circular Wait):**多个进程形成一种头尾相接的循环等待关系
如何避免死锁?
- 破坏死锁的条件
- 破坏互斥条件
- 使用共享资源(如读写锁),但某些资源(如打印机)无法共享
- 破坏占有并等待:
- 一次性申请所有资源(如银行家算法),避免部分持有
- 如果无法获取全部资源,则释放已持有的资源(超时回退)。
- 破坏非抢占条件:
- 允许系统强制剥夺资源(如数据库死锁检测后回滚事务)。
- 破坏循环等待:
- 按固定顺序申请资源(如所有线程必须先申请锁 A,再申请锁 B)
- 破坏互斥条件
- 死锁预防策略
- 资源有序分配法:
- 所有线程必须按照全局统一的顺序申请锁(如锁 A-> 锁 B-> 锁 C),避免循环等待。
- 超时机制:
- 如果线程在指定时间内未获取所有资源,则释放已持有的锁并重试(如 tryLock(timeOut))
- 资源有序分配法:
- 死锁检测与恢复
- 检测:
- 维护资源分配图(RAG),定期检测是否存在环路
- 数据库、操作系统等场景常用(如 MySQL 死锁检测)
- 恢复:
- 终止进程:强制终止部分进程(如优先级低的进程)
- 回滚:让部分进程回滚到安全状态(如事务回滚)
- 检测:
什么是活锁?什么是无锁?
活锁(Livelock)
活锁是指多个线程(或进程)在执行任务时,由于互相谦让或不断改变状态,导致任务无法推进,但线程本省并未阻塞,仍然在消耗 CPU 资源。
特点:
- 线程没有阻塞(仍在运行),但任务没有进展。
- 通常由不合理的重试策略或冲突解决机制导致。
示例:
- 两个线程互相让路:
- 线程 A 和 线程 B 都需要通过一个狭窄的通道,但它们每次都同时向同一侧移动,导致永远无法通过。
- 消息队列的重试冲突:
- 两个服务互相发送(失败-重试)消息,导致无想循环。
如何避免活锁?
- 引入随机退避(Random Backoff):
- 让线程在冲突时随机等待一段时间(如 TCP 拥塞控制)
- 限制重试次数:
- 超过一定次数后放弃或执行降级策略。
- 调整调度策略:
- 让其中一个线程优先执行(如设置优先级)
无锁(Lock-Free)
无锁编程是指不使用传统锁(如 mutex、synchronized)来实现并发安全,而是依赖原子操作(CAS,Atomic)或无锁数据结构(如无锁队列)。
特点:
- 无死锁风险(因为没有锁竞争)
- 高并发性能(减少线程阻塞)
- 可能面临 ABA 问题(需要特殊处理,如版本号)
实现方式:
CAS
javaAtomicInteger 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)。以下是其核心实现原理:
volatile 保证可见性
AtomicInteger内部通过volatile修饰的int值保证多线程下的可见性:javaprivate volatile int value;volatile确保对value的修改能立即被其他线程看到,避免脏读。
CAS 实现原子操作
核心方法(如
getAndIncrement()、compareAndSet())通过 CAS 机制实现无锁线程安全:javapublic final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }- Unsafe 类:通过本地方法(Native)调用硬件级别的 CAS 指令(如 x86 的
LOCK CMPXCHG)。 - CAS 流程:
- 读取当前值
oldValue。 - 计算新值
newValue = oldValue + delta(如+1)。 - 比较内存中的当前值是否仍为
oldValue:- 如果是,更新为
newValue,返回成功。 - 否则,重试(自旋)或失败。
- 如果是,更新为
- 读取当前值
- Unsafe 类:通过本地方法(Native)调用硬件级别的 CAS 指令(如 x86 的
偏移量(Offset)与内存操作
通过字段的内存偏移量直接操作内存
javaprivate static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }valueOffset是value字段在对象内存中的偏移地址,用于 Unsafe 直接读写该字段。
JDK9+ 的优化:VarHandle
在 JDK 9 之后,
AtomicInteger逐步改用VarHandle替代Unsafe:javaprivate static final VarHandle VALUE = MethodHandles.lookup() .findVarHandle(AtomicInteger.class, "value", int.class);VarHandle提供了更安全的底层内存操作 API,功能类似Unsafe,但权限可控
性能优势
- 无锁化:相比 Synchronized,CAS 减少了线程阻塞和上下文切换开销
- 自旋优化:在低竞争场景下效率极高(如
AtomicInteger的计数器场景)。
局限性
- ABA 问题:CAS 无法感知值从
A→B→A的变化。可通过AtomicStampedReference加入版本号解决。 - 高竞争性能下降:自旋可能导致 CPU 空耗,此时可用
LongAdder分段计数优化。
- ABA 问题:CAS 无法感知值从
什么是 CAS?CAS 有什么缺点?CAS 底层使用了哪个操作类?
什么是 CAS
CAS 是一种 原子操作,在多线程编程中用于实现无锁(lock-free)的并发控制。它的核心思想是:
- 给定一个内存位置
V,期望的旧值A,以及新值B; - CAS 会做如下检查:
- 如果
V中的值等于A,则将其更新为B,并返回 成功; - 如果
V中的值不等于A,则不做修改,并返回 失败。
- 如果
公式化理解:
CAS(V, A, B):
if (V == A)
V = B
return true
else
return false这是一种乐观锁思想:认为冲突少,先尝试更新,不行再重试。
CAS 的缺点
虽然 CAS 提供了高效的无锁操作,但也有一些缺陷:
- ABA 问题
- 如果某个变量原来是 A,被线程 1 读到;
- 然后线程 2 把它改成 B,又改回 A;
- 线程 1 再做 CAS 时,发现还是 A,就会以为没变,其实数据状态已经被改过。
- 解决办法:加上 版本号(比如
AtomicStampedReference)。
- 自旋开销大
- 如果竞争激烈,CAS 会一直自旋重试,消耗 CPU
- 适合低竞争场景,高竞争下可能不如锁。
- 只能保证一个变量的原子性
- 对多个共享变量操作时,CAS 不能保证整体原子性,需要用锁或者
AtomicReference等封装。
- 对多个共享变量操作时,CAS 不能保证整体原子性,需要用锁或者
CAS 底层使用了哪个操作类?
在 java 中,CAS 的实现主要依赖Unsafe类提供本地方法(CompareAndSwapInt、CompareAndSwapLong、CompareAndSwapObject)
Unsafe是sun.misc.Unsafe,提供了一组可以直接操作内存的 native 方法。- 实际上,
Unsafe底层调用的是 CPU 的原子指令,比如:- x86 架构上的
cmpxchg指令 - ARM 架构上的
LDREX/STREX
- x86 架构上的
- 所以 Java 中的
AtomicInteger、AtomicLong等类,都是基于Unsafe+ CAS 来实现的。
在 Java 9 之后,虽然官方逐渐对 Unsafe 做了限制,但 CAS 的底层实现仍然基于 Unsafe,只是 JDK 对外提供了更规范的 API 来替代直接调用 Unsafe:
java 9 之后的情况
Unsafe依然存在,内部类库(例如AtomicInteger、ConcurrentHashMap)还是用它来做底层 CAS。- 但 JDK 引入了更“安全”的公开类
VarHandle(在java.lang.invoke包里)- 可以用来替代
Unsafe做字段、数组、变量的原子操作。 - 提供了类似
compareAndSet的方法,底层还是调用Unsafe或者 JVM intrinsic(内联的 CPU 指令)。
- 可以用来替代
java.util.concurrent.atomic包的类(如AtomicInteger、AtomicLong)在 JDK 9+ 里逐步过渡到基于VarHandle实现,而不是直接写Unsafe。
为什么要这样做?
Unsafe不安全:它能直接操作内存地址,容易破坏 JVM 安全模型。- 模块化系统(Java 9 的 Jigsaw):
Unsafe属于内部 API,不再默认对外暴露。 - VarHandle 是官方标准:提供与
Unsafe类似的功能,但有更清晰的权限控制和更好的可维护性。
底层原理
不管是
Unsafe还是VarHandle,最终 CAS 的实现还是落到 JVM intrinsic,调用的是 CPU 的原子指令(如cmpxchg)。你可以理解为:
- Java 8 及之前:直接用
Unsafe.compareAndSwapXxx() - Java 9 之后:对外推荐用
VarHandle,内部AtomicXxx类有时用 VarHandle,有时还保留对 Unsafe 的调用(为了兼容)。 - 本质:还是 CPU 提供的 CAS 指令。
- Java 8 及之前:直接用
CAS 在 JDK 中有哪些应用?
CAS(Compare-And-Swap)几乎是 Java 并发包的基石,JDK 中大量的并发工具类都是基于它实现的。
原子类(
java.util.concurrent.atomic)最直接的应用就是原子变量类:
AtomicInteger,AtomicLong,AtomicBoolean,AtomicReference…它们的
getAndIncrement()、compareAndSet()等方法,内部就是通过 CAS + 自旋 来更新变量值。
👉 取代了synchronized的加锁方式。
并发容器
很多并发容器的底层原理操作依赖 CAS 来保证无锁更新:
ConcurrentHashMap- 在插入新节点时,使用 CAS 更新桶位数组。
- 在扩容时,也用 CAS 保证迁移过程安全
ConcurrentLinkedQueue/ConcurrentLinkedDeque- 用 CAS 来更新队列的
head和tail指针,实现无锁队列。
- 用 CAS 来更新队列的
LinkedTransferQueue- 用 CAS 维护链表的插入/匹配操作
同步器框架(AQS)
AbstractQueuedSynchronizer(AQS)是 java 并发锁(ReentrantLock、CountDownLatch、Semaphore)的核心框架。- 内部有一个
State变量,用来表示锁的状态。 - AQS 通过 CAS 修改 State 来实现加锁/解锁操作(例如
tryAcquire、tryRelease)
例如:
javaif (compareAndSetState(0, 1)) { // 成功获取锁 }- 内部有一个
线程池
ThreadPoolExecutor- 使用 CAS 来维护 线程池的运行状态(ctl 变量),该变量同时记录了线程池状态和工作线程数。
- 避免了对线程池状态修改时的锁竞争。
StampedLock
- 在
StampedLock中,读写锁的状态值也是通过 CAS 修改的,避免传统ReentrantReadWriteLock的锁开销。
- 在
Future/CompletableFuture
CompletableFuture在完成任务时,使用 CAS 来更新结果状态,保证只有一个线程能成功设置结果。
ThreadLocalRandom
ThreadLocalRandom在更新种子值时用 CAS,保证并发环境下更新无锁安全。
其他典型应用
- LongAdder / DoubleAdder:
- 针对
AtomicLong在高并发下的 CAS 自旋热点问题,LongAdder 采用 分段累加 + CAS,提升吞吐量。
- 针对
- StampedReference / AtomicMarkableReference / AtomicStampedReference:
- 这些是 CAS 的 ABA 问题解决方案,通过版本号或标记位来避免 ABA。
- LongAdder / DoubleAdder:
用伪代码写一个 CAS 算法的核心
CAS 算法的核心在于 比较+更新,只有内存值和期望一致时才更新;如果 CAS 失败,就再次尝试;无锁实现,也能保证线程安全。
function CAS(address, expectedValue, newValue):
oldValue = *address // 取出内存中的当前值
if oldValue == expectedValue: // 判断是否与期望值一致
*address = newValue // 如果一致,更新为新值
return true // 返回成功
else:
return false // 否则返回失败多线程情况下,进行数字累加(count++)需要注意什么
在多线程情况下,count++不是一个原子操作,在并发场景下会出现问题
为什么 Count++ 线程不安全?
count++实际上分为三步:- 读取变量值:
temp = count - 执行加一:
temp = temp + 1 - 写回变量:
count = temp
如果两个线程同时执行:
- 线程 A 读到
count = 5 - 线程 B 也读到
count = 5 - A +1 → 写回 6
- B +1 → 写回 6
最后
count = 6,而不是期望的 7。
👉 出现了 竞态条件(race condition)。- 读取变量值:
解决方案
- 加锁 使用
Synchronized或者ReetrantLock,简单可靠,有锁开销,性能可能不是很高 - 使用原子类(推荐),基于 CAS,无锁,性能高,在高并发场景下自旋重试开销大
- 使用
LongAdder(高并发推荐)- LongAdder 内部把一个变量分成多个 Cell,减少 CAS 热点竞争。
- 在高并发下比 AtomicInteger 性能更好。
- 加锁 使用
有了 AtomicInteger,为什么 JDK 又出了个 LongAdder?不同的使用场景?
AtomicInteger的问题
AtomicInteger 内部是基于 CAS + 自旋重试 来保证原子性的:
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[]的值加起来
使用场景对比
| 特性 | AtomicInteger | LongAdder |
|---|---|---|
| 实现方式 | 单个变量 + 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 或共享退出标志,让线程自行、安全地结束。
为什么推荐
会直接杀死线程,不管线程在干什么
stop()的行为是:- 在线程任意执行点抛出
ThreadDeath - 立即终止执行
👉 不给线程任何“善后”的机会
- 在线程任意执行点抛出
会破坏对象状态一致性(核心问题)
这是最致命的问题
javasynchronized (obj) { obj.a = 1; obj.b = 2; }如果线程在这个时候被强制 stop():
a = 1已执行b = 2还没执行- 锁被强制释放
👉 其他线程看到的是不一致的对象状态
会强制释放锁(非常危险)
- stop() 会释放所有持有的锁
- 不管临界区是否执行完
👉 直接破坏 synchronized 的原子性保障
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 / join | interrupt |
| BlockingQueue | interrupt |
| NIO / Selector | interrupt |
| 纯 CPU 计算 | volatile |
| 线程池任务 | interrupt |
线程池中如何优雅停止?(工程常见)
正确关闭线程池
executor.shutdown(); // 拒绝新任务
executor.awaitTermination(...);
executor.shutdownNow(); // 中断正在执行的线程shutdownNow()内部就是调用interrupt()
绝对不要用的方式 ❌
| 方式 | 问题 |
|---|---|
| Thread.stop() | 破坏一致性 |
| Thread.suspend() | 死锁 |
| Thread.resume() | 不安全 |
| 强制 kill 线程 | 不可控 |
