Java并发面试题300道(详解版)
每道题包含:答案详解 + 代码示例 + 记忆要点
目录
基础篇(1-50)
Q1: 线程和进程的区别?
答案:
| 维度 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU调度的基本单位 |
| 内存 | 独立的内存空间 | 共享进程的内存空间 |
| 通信 | 需要IPC(管道、共享内存等) | 直接共享变量,方便通信 |
| 切换开销 | 大(需要保存上下文、TLB等) | 小(只需保存寄存器、栈指针) |
| 创建销毁 | 慢 | 快 |
| 独立性 | 进程间独立 | 依赖父进程 |
记忆要点:
- 进程 = 资源容器(独立的地址空间)
- 线程 = 执行单位(共享进程资源)
- 一句话:进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位
Q2: 线程的5种状态及转换?
答案:
NEW (新建)
│
│ start()
▼
RUNNABLE (可运行)
/ │ \
│ │ │
│ │ │ yield()/时间片用完
│ │ ▼
│ │ RUNNABLE
│ │
│ │ 获取锁失败
│ │ │
│ │ ▼
│ │ BLOCKED (阻塞)
│ │ │
│ │ 获得锁
│ │ │
│ │ │
│ │ ▼
│ │ RUNNABLE
│ │
│ │ wait()/join()/park()
│ │ │
│ │ ▼
│ │ WAITING (等待)
│ │ │
│ │ notify()/unpark()
│ │ │
│ │ ▼
│ │ RUNNABLE
│ │
│ │ wait(1000)/sleep(1000)
│ │ │
│ │ ▼
│ │ TIMED_WAITING (计时等待)
│ │ │
│ │ 超时/notify()
│ │ │
│ │ ▼
│ │ RUNNABLE
│ │
│ │ 执行完毕/异常
│ │ │
│ │ ▼
│ │ TERMINATED (终止)
代码示例:
public class ThreadState {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000); // TIMED_WAITING
synchronized (ThreadState.class) {
ThreadState.class.wait(); // WAITING
}
} catch (Exception e) {}
});
System.out.println("NEW: " + thread.getState()); // NEW
thread.start();
System.out.println("RUNNABLE: " + thread.getState()); // RUNNABLE
Thread.sleep(500);
System.out.println("TIMED_WAITING: " + thread.getState()); // TIMED_WAITING
synchronized (ThreadState.class) {
ThreadState.class.notifyAll();
}
Thread.sleep(100);
System.out.println("TERMINATED: " + thread.getState()); // TERMINATED
}
}
记忆要点:
- 5种状态:NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
- RUNNABLE = 就绪 + 运行
- BLOCKED = 等锁
- WAITING = 无限期等待(wait/join)
- TIMED_WAITING = 限时等待(sleep/wait带时间)
Q3: 如何创建线程?有哪些方式?
答案:
// 方式1: 继承Thread类(不推荐)
class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread");
}
}
new MyThread().start();
// 方式2: 实现Runnable接口(推荐)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable");
}
}
new Thread(new MyRunnable()).start();
// 方式3: Lambda表达式(推荐)
new Thread(() -> {
System.out.println("Lambda创建线程");
}).start();
// 方式4: 实现Callable接口(可获取返回值)
Callable<String> callable = () -> {
return "Callable返回值";
};
FutureTask<String> future = new FutureTask<>(callable);
new Thread(future).start();
String result = future.get(); // 阻塞获取结果
// 方式5: 线程池(推荐)
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> System.out.println("线程池"));
// 方式6: CompletableFuture(推荐)
CompletableFuture.runAsync(() -> System.out.println("异步任务"));
记忆要点:
- 继承Thread:单继承限制,不推荐
- 实现Runnable:灵活,推荐
- 实现Callable:可返回值、抛异常
- 线程池:生产环境推荐
- 核心原则:优先使用线程池,避免手动创建线程
Q4: start()和run()的区别?
答案:
| 特性 | start() | run() |
|---|---|---|
| 功能 | 启动新线程,调用run() | 普通方法调用,不创建新线程 |
| 线程 | 在新线程中执行 | 在当前线程中执行 |
| 调用次数 | 只能调用一次 | 可多次调用 |
| 并发 | 真正的并发 | 串行执行 |
代码示例:
public class StartVsRun {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("当前线程: " + Thread.currentThread().getName());
});
System.out.println("=== 调用start() ===");
thread.start(); // 输出: Thread-0(新线程)
System.out.println("=== 调用run() ===");
thread.run(); // 输出: main(当前线程)
}
}
start()源码核心逻辑:
// Thread.start() 简化版
public synchronized void start() {
if (threadStatus != 0) // 检查状态
throw new IllegalThreadStateException();
start0(); // native方法,创建线程并调用run()
}
private native void start0(); // JVM底层实现
记忆要点:
- start() = 启动新线程 → 创建线程 → 调用run()
- run() = 普通方法,在当前线程执行
- 常见错误:调用run()而不是start(),导致没有真正并发
Q5: sleep()和wait()的区别?
答案:
| 特性 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 锁持有 | 不释放锁 | 释放锁 |
| 唤醒方式 | 超时自动唤醒 | 需要notify/notifyAll或超时 |
| 使用位置 | 任意位置 | 必须在synchronized块中 |
| 异常 | InterruptedException | InterruptedException + IllegalMonitorStateException |
代码示例:
public class SleepVsWait {
private static final Object lock = new Object();
public static void main(String[] args) throws Exception {
// sleep()示例 - 不释放锁
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread-1: 获得锁");
try {
Thread.sleep(3000); // 睡眠但持有锁
} catch (InterruptedException e) {}
System.out.println("Thread-1: 释放锁");
}
}).start();
Thread.sleep(100);
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread-2: 获得锁");
}
}).start();
// ========================================
// wait()示例 - 释放锁
synchronized (lock) {
System.out.println("Main: 获得锁");
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread: 获得锁并通知");
lock.notify();
}
}).start();
Thread.sleep(100);
System.out.println("Main: 释放锁并等待");
lock.wait(); // 释放锁,等待唤醒
System.out.println("Main: 被唤醒,重新获得锁");
}
}
}
wait()源码核心:
// Object.wait() 简化逻辑
public final void wait() throws InterruptedException {
if (!Thread.currentThread().holdsLock(this))
throw new IllegalMonitorStateException(); // 必须持有锁
wait(0); // 调用native方法,释放锁并等待
}
记忆要点:
- sleep() = 休眠不释放锁(CPU给其他线程,但锁还在手里)
- wait() = 等待释放锁(CPU+锁都交出去)
- wait()必须在synchronized中,否则IllegalMonitorStateException
- 面试重点:wait会释放锁,sleep不会
Q6: notify()和notifyAll()的区别?
答案:
| 特性 | notify() | notifyAll() |
|---|---|---|
| 唤醒数量 | 唤醒一个等待线程 | 唤醒所有等待线程 |
| 选择策略 | JVM选择(不确定是哪个) | 全部唤醒 |
| 效率 | 高(只唤醒一个) | 低(唤醒所有) |
| 风险 | 可能死锁或假死 | 无死锁风险 |
代码示例:
public class NotifyVsNotifyAll {
private static final Object lock = new Object();
private static int conditionA = 0;
private static int conditionB = 0;
public static void main(String[] args) {
// 等待条件A的线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
synchronized (lock) {
while (conditionA == 0) {
try { lock.wait(); } catch (Exception e) {}
}
System.out.println("条件A满足");
}
}).start();
}
// 等待条件B的线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
synchronized (lock) {
while (conditionB == 0) {
try { lock.wait(); } catch (Exception e) {}
}
System.out.println("条件B满足");
}
}).start();
}
// 使用notify()的问题
new Thread(() -> {
synchronized (lock) {
conditionA = 1;
lock.notify(); // 可能唤醒了等待B的线程!
// 唤醒的线程检查conditionB不满足,继续wait
// 真正等待A的线程永远无法被唤醒 → 假死
}
}).start();
}
}
// 正确做法:使用notifyAll()
synchronized (lock) {
conditionA = 1;
lock.notifyAll(); // 唤醒所有,让每个线程自己检查条件
}
记忆要点:
- notify() = 唤醒一个,但不确定是哪个
- notifyAll() = 唤醒所有,各线程自己判断条件
- 最佳实践:始终使用notifyAll(),避免信号丢失
- 特殊情况可以用notify():只有一个等待线程时
Q7: wait()为什么要在synchronized块中?
答案:
原因1:防止竞争条件
// 如果wait()不需要synchronized,会发生什么?
public class RaceCondition {
private boolean flag = false;
// 线程1
public void waitMethod() {
if (!flag) { // ← 检查
wait(); // ← 等待 (如果中间flag被修改怎么办?)
}
}
// 线程2
public void notifyMethod() {
flag = true; // ← 修改
notify(); // ← 通知
}
}
// 问题:检查和等待之间不是原子的,可能漏掉通知
原因2:避免lost wake-up问题
// 错误示例:没有synchronized
public class LostWakeUp {
private boolean ready = false;
public void wait() throws InterruptedException {
if (!ready) {
this.wait(); // 假设可以
}
}
public void notify() {
ready = true;
this.notify(); // 先notify了!
}
}
// 如果notify()发生在wait()之前,通知就丢失了
原因3:源码强制检查
// Object.wait() 的实现
public final void wait() throws InterruptedException {
// 检查当前线程是否持有对象的监视器锁
if (!Thread.currentThread().holdsLock(this)) {
throw new IllegalMonitorStateException();
}
wait0(); // native方法
}
正确做法:
synchronized (lock) {
while (!condition) { // while循环防止虚假唤醒
lock.wait(); // 释放锁,等待唤醒
}
// 执行业务逻辑
}
记忆要点:
- wait/notify必须在synchronized中
- 防止竞争条件:检查和等待必须是原子的
- 防止lost wake-up:先notify后wait会导致通知丢失
- 必须用while循环检查条件,防止虚假唤醒
Q8: 为什么wait()会释放锁而sleep()不会?
答案:
设计理念不同:
| 方法 | 设计目的 | 是否应该释放锁 |
|---|---|---|
| wait() | 等待条件满足 | 必须释放,否则其他线程无法改变条件 |
| sleep() | 暂停执行 | 不应释放,仅仅休息一下 |
wait()释放锁的原因:
public class ProducerConsumer {
private final Queue<String> queue = new LinkedList<>();
private final int maxSize = 10;
private final Object lock = new Object();
// 消费者:等待队列非空
public String consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // ← 必须释放锁!
// 否则生产者永远无法put,死锁
}
return queue.poll();
}
}
// 生产者:等待队列不满
public void produce(String item) throws InterruptedException {
synchronized (lock) {
while (queue.size() >= maxSize) {
lock.wait(); // ← 必须释放锁!
// 否则消费者永远无法poll,死锁
}
queue.offer(item);
lock.notifyAll();
}
}
}
如果wait()不释放锁:
- 消费者检查queue.isEmpty() = true
- 消费者调用wait(),但持有锁
- 生产者想put,但被消费者持有的锁阻塞
- 死锁!
sleep()不释放锁的原因:
public class SleepWithLock {
private final Object lock = new Object();
private int counter = 0;
public void increment() {
synchronized (lock) {
try {
Thread.sleep(100); // 仅仅休息一下
// 休息期间不需要其他线程访问counter
counter++;
} catch (InterruptedException e) {}
}
}
}
JVM实现层面:
// HotSpot JVM 简化逻辑
// Object.wait()
void ObjectMonitor::wait() {
// 1. 释放当前线程持有的锁
release_lock();
// 2. 把当前线程放入等待队列
enqueue(waiter);
// 3. 阻塞当前线程
block_current_thread();
// 4. 被唤醒后,重新获取锁
acquire_lock();
}
// Thread.sleep()
void os::sleep() {
// 1. 只是把线程从就绪队列移除
remove_from_ready_queue();
// 2. 让出CPU时间片
yield_cpu();
// 3. 不涉及锁操作
}
记忆要点:
- wait() = 等待条件,必须释放锁让其他线程改变条件
- sleep() = 休息一下,不涉及等待条件,不应释放锁
- 面试标准答案:wait()需要其他线程修改等待条件,所以必须释放锁
Q9: 如何停止一个线程?
答案:
方法1: 使用volatile标志(推荐)
public class VolatileStop {
private volatile boolean running = true;
public void start() {
new Thread(() -> {
while (running) { // 检查标志
doWork();
}
}).start();
}
public void stop() {
running = false; // 设置标志
}
}
方法2: 使用interrupt()(推荐)
public class InterruptStop {
public void start() {
Thread thread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) { // 检查中断标志
doWork();
}
// 或捕获InterruptedException
try {
while (true) {
Thread.sleep(100);
doWork();
}
} catch (InterruptedException e) {
// 收到中断信号,退出
Thread.currentThread().interrupt(); // 恢复中断状态
}
}
};
thread.start();
// 停止线程
thread.interrupt(); // 设置中断标志
}
}
方法3: 使用ExecutorService(推荐)
public class ExecutorStop {
private ExecutorService executor = Executors.newFixedThreadPool(10);
public void stop() {
// 方式1: shutdown() - 平滑关闭
executor.shutdown(); // 不再接受新任务,等待已提交任务完成
// 方式2: shutdownNow() - 立即关闭
List<Runnable> unfinished = executor.shutdownNow(); // 尝试停止所有任务
// 等待关闭
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 强制关闭
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
❌ 已废弃的方法:stop(), suspend(), resume()
// 不要使用!这些方法已被废弃
thread.stop(); // 不安全,可能造成数据不一致
thread.suspend(); // 可能导致死锁
thread.resume(); // 配合suspend使用,不安全
为什么stop()不安全?
public class UnsafeStop {
private static int counter = 0;
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter++; // 不是原子操作!
}
System.out.println("完成,counter = " + counter);
});
thread.start();
Thread.sleep(1);
thread.stop(); // 立即停止!counter可能处于中间状态
System.out.println("主线程,counter = " + counter);
// 可能输出: counter = 123(不完整的结果)
}
}
最佳实践总结:
public class BestPracticeStop {
private volatile boolean running = true;
private ExecutorService executor;
private Future<?> future;
public void start() {
executor = Executors.newSingleThreadExecutor();
future = executor.submit(() -> {
try {
while (running && !Thread.currentThread().isInterrupted()) {
doWork();
}
} catch (InterruptedException e) {
// 被中断,正常退出
}
});
}
public void stop() {
running = false; // 1. 设置标志
if (future != null) {
future.cancel(true); // 2. 取消任务
}
if (executor != null) {
executor.shutdownNow(); // 3. 关闭线程池
}
}
}
记忆要点:
- 不要使用stop()、suspend()、resume()(已废弃,不安全)
- 推荐方法:interrupt() 或 volatile标志
- 使用ExecutorService时,用shutdownNow()
- 线程应该定期检查中断标志或响应InterruptedException
Q10: interrupt()、isInterrupted()、interrupted()的区别?
答案:
| 方法 | 作用 | 静态 | 是否清除标志 |
|---|---|---|---|
interrupt() |
设置中断标志 | 否 | 否(设置标志) |
isInterrupted() |
检查中断标志 | 否 | 否(不清除) |
interrupted() |
检查并清除标志 | 是 | 是(清除标志) |
代码示例:
public class InterruptDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程开始");
// 方式1: isInterrupted() - 不清除标志
while (!Thread.currentThread().isInterrupted()) {
// doWork();
}
System.out.println("检测到中断1");
System.out.println("再次检查: " +
Thread.currentThread().isInterrupted()); // true
// 方式2: interrupted() - 清除标志
boolean interrupted = Thread.interrupted();
System.out.println("interrupted() = " + interrupted); // true
System.out.println("再次检查: " +
Thread.currentThread().isInterrupted()); // false (已清除)
});
thread.start();
thread.interrupt(); // 设置中断标志
}
}
interrupt()的工作机制:
// Thread.interrupt() 简化实现
public void interrupt() {
// 1. 设置中断标志
interrupted = true;
// 2. 如果线程在sleep/wait/join等阻塞状态
// 会收到InterruptedException,并清除中断标志
if (threadState == BLOCKED || threadState == WAITING) {
// 唤醒线程,抛出InterruptedException
throw new InterruptedException();
}
}
各种场景下的响应:
// 场景1: 正常运行 - 需要主动检查
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑
}
});
// 场景2: 阻塞在sleep() - 抛出异常
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000); // 被interrupt时会抛InterruptedException
} catch (InterruptedException e) {
// 处理中断
}
});
// 场景3: 阻塞在wait() - 抛出异常
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
lock.wait(); // 被interrupt时会抛InterruptedException
} catch (InterruptedException e) {}
}
});
// 场景4: 阻塞在I/O - 抛出ClosedByInterruptException
Thread thread = new Thread(() -> {
try {
SocketChannel channel = SocketChannel.open(...);
channel.configureBlocking(true);
channel.read(buffer); // 被interrupt时会抛ClosedByInterruptException
} catch (ClosedByInterruptException e) {
// 处理中断
}
});
// 场景5: 阻塞在Selector.select() - 立即返回
Thread thread = new Thread(() -> {
try {
Selector selector = Selector.open();
selector.select(); // 被interrupt时立即返回
} catch (ClosedSelectorException e) {}
});
最佳实践:
public class BestPracticeInterrupt {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
doWork();
}
} catch (InterruptedException e) {
// 重要:捕获异常后,通常需要退出线程
// 可以执行清理工作
cleanup();
} finally {
// 恢复中断状态(如果需要通知调用者)
Thread.currentThread().interrupt();
}
});
}
}
记忆要点:
interrupt()= 设置中断标志,唤醒阻塞线程isInterrupted()= 检查标志(不清除)interrupted()= 检查并清除标志(静态方法)- 线程应该定期检查中断标志或响应InterruptedException
Q11: 什么是守护线程?
答案:
定义:
守护线程(Daemon Thread)是优先级较低的线程,用于服务用户线程。当所有用户线程结束时,JVM会退出,不管守护线程是否执行完毕。
特点:
| 特性 | 说明 |
|---|---|
| 生命周期 | 依赖于用户线程,用户线程结束则守护线程终止 |
| 优先级 | 通常较低,但不绝对 |
| 用途 | 后台任务:GC、监控、定时清理等 |
| JVM退出 | 只要有用户线程存活,JVM不退出 |
代码示例:
public class DaemonThreadDemo {
public static void main(String[] args) throws Exception {
Thread daemon = new Thread(() -> {
int i = 0;
while (true) {
System.out.println("守护线程: " + i++);
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
}
});
daemon.setDaemon(true); // 设置为守护线程
daemon.start();
System.out.println("主线程休眠2秒");
Thread.sleep(2000);
System.out.println("主线程结束,JVM退出");
// 守护线程也会被强制终止
}
}
对比:用户线程 vs 守护线程
public class UserVsDaemon {
public static void main(String[] args) throws Exception {
// 守护线程
Thread daemon = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("守护线程: " + i);
try { Thread.sleep(500); } catch (Exception e) {}
}
System.out.println("守护线程完成");
});
daemon.setDaemon(true);
// 用户线程
Thread user = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("用户线程: " + i);
try { Thread.sleep(500); } catch (Exception e) {}
}
System.out.println("用户线程完成");
});
daemon.start();
user.start();
user.join(); // 等待用户线程结束
System.out.println("主线程结束");
// JVM退出,守护线程可能没执行完
}
}
常见守护线程:
FinalizerThread- 执行对象的finalize()方法ReferenceHandler- 处理软/弱/虚引用- JVM内置的垃圾回收线程
设置限制:
public class DaemonLimit {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程执行");
});
// ✅ 正确:启动前设置
thread.setDaemon(true);
thread.start();
// ❌ 错误:启动后设置
Thread thread2 = new Thread(() -> {});
thread2.start();
thread2.setDaemon(true); // 抛出IllegalThreadStateException
}
}
内存泄漏风险:
public class DaemonMemoryLeak {
private static final Map<Integer, Object> cache = new HashMap<>();
public static void main(String[] args) {
Thread daemon = new Thread(() -> {
while (true) {
for (int i = 0; i < 10000; i++) {
cache.put(i, new byte[1024]); // 持续分配内存
}
try { Thread.sleep(100); } catch (Exception e) {}
}
});
daemon.setDaemon(true);
daemon.start();
// 主线程结束,但JVM还没退出(等待GC)
// 守护线程仍在分配内存,可能导致OOM
}
}
记忆要点:
- 守护线程 = 后台服务线程,用户线程结束则终止
- GC线程、Finalizer线程都是守护线程
- 必须在start()前设置setDaemon(true)
- 不要在守护线程中持有重要资源或执行关键任务
Q12: ThreadLocal的作用?
答案:
定义:
ThreadLocal提供线程局部变量,每个线程都有独立的副本,互不影响。
核心作用:
| 作用 | 场景 | 示例 |
|---|---|---|
| 线程隔离 | 避免线程安全问题 | SimpleDateFormat、数据库连接 |
| 上下文传递 | 链路追踪 | TraceId、UserId |
| 资源持有 | 减少传参 | 数据库连接、Session |
代码示例:
public class ThreadLocalDemo {
// 示例1: 线程隔离 - SimpleDateFormat
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public void formatDate() {
// 每个线程有自己的SimpleDateFormat,线程安全
String date = dateFormat.get().format(new Date());
System.out.println(date);
}
// 示例2: 用户上下文传递
private static final ThreadLocal<String> userContext = new ThreadLocal<>();
public void processRequest(String userId) {
userContext.set(userId); // 设置当前用户
try {
serviceA(); // 所有方法都可以获取当前用户
serviceB();
} finally {
userContext.remove(); // 重要:用完清理
}
}
private void serviceA() {
String userId = userContext.get(); // 获取当前用户
System.out.println("Service A处理: " + userId);
}
// 示例3: 数据库连接管理
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = DriverManager.getConnection(url);
connectionHolder.set(conn);
}
return conn;
}
// 示例4: 传递调用链路ID
public class TraceIdContext {
private static final ThreadLocal<String> traceId = new ThreadLocal<>();
public static void setTraceId(String id) {
traceId.set(id);
}
public static String getTraceId() {
return traceId.get();
}
public static void clear() {
traceId.remove();
}
}
}
工作原理:
Thread1 ─────────────────────────────┐
├─ ThreadLocalMap │
│ ├─ ThreadLocal1 → Value1 │
│ └─ ThreadLocal2 → Value2 │
└─ ... │
│
Thread2 ─────────────────────────────┤
├─ ThreadLocalMap │
│ ├─ ThreadLocal1 → Value3 │ 每个线程有自己的Map
│ └─ ThreadLocal2 → Value4 │ 值互不影响
└─ ... │
│
ThreadLocal ────────────────────────┘
(作为ThreadLocalMap的key)
ThreadLocal源码核心:
// Thread.java
public class Thread implements Runnable {
// 每个线程持有一个ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals;
}
// ThreadLocal.java
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = t.threadLocals; // 获取线程的Map
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // this作为key
if (e != null) {
return (T) e.value;
}
}
return setInitialValue(); // 初始化
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null) {
map.set(this, value); // this作为key
} else {
createMap(t, value);
}
}
public void remove() {
ThreadLocalMap map = Thread.currentThread().threadLocals;
if (map != null) {
map.remove(this);
}
}
}
InheritableThreadLocal(可继承的ThreadLocal):
public class InheritableThreadLocalDemo {
private static final InheritableThreadLocal<String> inheritableLocal =
new InheritableThreadLocal<>();
public static void main(String[] args) {
inheritableLocal.set("父线程的值");
new Thread(() -> {
// 子线程可以继承父线程的ThreadLocal值
System.out.println("子线程: " + inheritableLocal.get());
}).start();
}
}
记忆要点:
- ThreadLocal = 每个线程独立的变量副本
- 原理:Thread持有ThreadLocalMap,ThreadLocal作为key
- 用途:线程隔离、上下文传递、资源持有
- 重要:使用完要remove(),防止内存泄漏
Q13: ThreadLocal的内存泄漏问题?
答案:
问题根源:
Thread ───→ ThreadLocalMap ───→ Entry ───→ ThreadLocal (WeakReference)
↓
Value (强引用)
问题:
- Entry的key(ThreadLocal)是弱引用,ThreadLocal被回收后key为null
- 但Entry的value是强引用,无法被GC回收
- 线程一直存活(如线程池),ThreadLocalMap也一直存在
- 导致value泄漏
代码示例:
public class ThreadLocalLeak {
private static final ThreadLocal<byte[]> local = new ThreadLocal<>();
private static final ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
// 分配大对象
local.set(new byte[10 * 1024 * 1024]); // 10MB
// 业务逻辑
doWork();
// ❌ 忘记remove()!
// local.remove();
// 线程放回线程池,继续复用
// ThreadLocalMap中的value(10MB)一直存在
// 100个任务 = 1000MB内存泄漏
});
}
}
}
解决方案:
// 方式1: 使用try-finally确保清理
public class CorrectUsage {
private static final ThreadLocal<byte[]> local = new ThreadLocal<>();
public void process() {
local.set(new byte[10 * 1024 * 1024]);
try {
doWork();
} finally {
local.remove(); // ✅ 确保清理
}
}
}
// 方式2: Java 8+ 使用withInitial()并配合remove
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// 方式3: 使用try-with-resources风格
public class AutoCleanThreadLocal<T> implements AutoCloseable {
private final ThreadLocal<T> threadLocal;
public AutoCleanThreadLocal(Supplier<T> supplier) {
this.threadLocal = ThreadLocal.withInitial(supplier);
}
public T get() {
return threadLocal.get();
}
public void set(T value) {
threadLocal.set(value);
}
@Override
public void close() {
threadLocal.remove();
}
}
// 使用
try (var local = new AutoCleanThreadLocal<>(() -> new Resource())) {
local.get().doSomething();
} // 自动清理
ThreadLocalMap的清理机制:
// ThreadLocalMap内部有清理逻辑
class ThreadLocalMap {
private void expungeStaleEntry(int staleSlot) {
// 清理key为null的Entry
for (int i = staleSlot; ; i = nextIndex(i, len)) {
Entry e = tab[i];
if (e == null)
break;
if (e.get() == null) // key被回收
expungeStaleEntry(i); // 清理entry
}
}
// set()/get()/remove()时都会触发清理
public T get(ThreadLocal<?> key) {
Entry e = getEntry(key);
if (e != null)
return (T) e.value;
// 没找到,触发清理
return getAfterMiss(key);
}
}
最佳实践:
public class ThreadLocalBestPractice {
// 1. 基础用法:配合try-finally
private static final ThreadLocal<Connection> connectionLocal = new ThreadLocal<>();
public Connection getConnection() {
Connection conn = connectionLocal.get();
if (conn == null) {
conn = createConnection();
connectionLocal.set(conn);
}
return conn;
}
public void releaseConnection() {
connectionLocal.remove(); // ✅ 手动清理
}
// 2. 封装成工具类
public class ThreadLocalUtils {
private static final ThreadLocal<Map<String, Object>> context =
ThreadLocal.withInitial(HashMap::new);
public static void put(String key, Object value) {
context.get().put(key, value);
}
public static <T> T get(String key) {
return (T) context.get().get(key);
}
public static void clear() {
context.remove(); // ✅ 统一清理入口
}
}
// 3. 在拦截器和过滤器中使用
public class RequestFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
try {
ThreadLocalUtils.put("requestId", generateId());
chain.doFilter(req, res);
} finally {
ThreadLocalUtils.clear(); // ✅ 请求结束清理
}
}
}
}
内存泄漏检测:
// 检测ThreadLocal内存泄漏
public class ThreadLocalLeakDetector {
public static void detect() {
Thread[] threads = getAllThreads();
for (Thread thread : threads) {
try {
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Object threadLocalMap = threadLocalsField.get(thread);
if (threadLocalMap != null) {
Field tableField = threadLocalMap.getClass().getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(threadLocalMap);
int leakCount = 0;
for (Object entry : table) {
if (entry != null) {
Field valueField = entry.getClass().getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(entry);
Field keyField = entry.getClass().getDeclaredField("referent");
keyField.setAccessible(true);
Object key = keyField.get(entry);
if (key == null && value != null) {
leakCount++;
}
}
}
if (leakCount > 0) {
System.out.println("线程 " + thread.getName() + " 有 " + leakCount + " 个泄漏的Entry");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
记忆要点:
- ThreadLocalMap的Entry.key是弱引用,value是强引用
- 线程池场景下,线程不销毁,value可能泄漏
- 必须在使用完调用remove()清理
- 配合try-finally确保清理
- get()/set()会触发部分清理,但不可靠
Q14: ThreadLocalMap的数据结构?
答案:
ThreadLocalMap结构:
Thread ───→ threadLocals (ThreadLocalMap)
│
▼
ThreadLocalMap {
Entry[] table; // 开放寻址哈希表
int size; // 当前大小
int threshold; // 扩容阈值
}
Entry {
ThreadLocal<?> key (WeakReference); // 弱引用
Object value; // 强引用
}
为什么使用开放寻址而不是链表?
| 特性 | 开放寻址 | 链表法 |
|---|---|---|
| 内存占用 | 连续内存,占用少 | 需要额外Node对象 |
| CPU缓存 | 更友好 | 较差 |
| 冲突处理 | 探测下一个位置 | 链表 |
| 删除 | 标记删除 | 真正删除 |
| 适用场景 | 空间敏感,冲突少 | 冲突多,动态扩容 |
源码分析:
static class ThreadLocalMap {
// 初始容量16,必须是2的幂
private static final int INITIAL_CAPACITY = 16;
// Entry数组
private Entry[] table;
// Entry继承WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key是弱引用
value = v;
}
}
// 哈希计算:ThreadLocal的hashCode
private int hash(ThreadLocal<?> key) {
return key.hashCode() & (table.length - 1); // 与运算
}
// 设置值
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1); // 哈希位置
// 开放寻址:线性探测
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // key相同,覆盖
e.value = value;
return;
}
if (k == null) { // key被回收,替换
replaceStaleEntry(key, value, i);
return;
}
}
// 没找到,创建新Entry
tab[i] = new Entry(key, value);
size++;
if (size >= threshold) { // 扩容
rehash();
}
}
// 获取值
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 线性探测
if (e != null && e.get() == key) {
return e; // 直接命中
}
// 没命中,继续探测
if (e != null) {
Entry e = tab[i = nextIndex(i, len)];
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
return getEntryAfterMiss(key, i, e);
e = tab[i = nextIndex(i, len)];
}
}
return null; // 没找到
}
// 删除
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear(); // 清除key引用
e.value = null; // 清除value引用
size--;
expungeStaleEntry(i); // 清理并探测
return;
}
}
}
// 清理并重新探测(开放寻址的核心)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除当前位置
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 重新探测后续元素(因为开放寻址,可能需要前移)
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // 位置不对,需要前移
tab[i] = null;
while (tab[h] != null) {
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
return i;
}
}
为什么初始容量是16?
1. 2的幂次方:哈希计算可以用位运算代替取模
hash & (length - 1) == hash % length
2. 不太大也不太小:
- 太小:冲突多,探测次数多
- 太大:内存浪费
3. 扩容阈值 = 2/3容量:
- 负载因子2/3,平衡空间和性能
- 大于HashMap的0.75,因为开放寻址需要更多空间
冲突处理示例:
假设容量=4,哈希值:
- ThreadLocal1: hashCode % 4 = 1
- ThreadLocal2: hashCode % 4 = 1 (冲突)
- ThreadLocal3: hashCode % 4 = 2
初始状态:
[null, null, null, null]
插入ThreadLocal1:
[null, Entry1, null, null]
插入ThreadLocal2 (位置1冲突,线性探测到2):
[null, Entry1, Entry2, null]
插入ThreadLocal3 (位置2冲突,线性探测到3):
[null, Entry1, Entry2, Entry3]
删除Entry1 (位置1):
[null, null, Entry2, Entry3]
重要:Entry2的原始哈希位置应该是1!
需要前移:
[null, Entry2, Entry3, null]
记忆要点:
- ThreadLocalMap = 开放寻址哈希表
- Entry.key = WeakReference,Entry.value = 强引用
- 初始容量16,扩容阈值2/3
- 开放寻址:冲突时线性探测下一个位置
- 删除后需要重新探测,因为开放寻址可能需要前移元素
Q15: InheritableThreadLocal是什么?
答案:
定义:
InheritableThreadLocal是ThreadLocal的子类,可以让子线程继承父线程的ThreadLocal值。
对比:
| 特性 | ThreadLocal | InheritableThreadLocal |
|---|---|---|
| 继承性 | 子线程不继承父线程的值 | 子线程继承父线程的值 |
| 使用场景 | 线程隔离 | 父子线程传递上下文 |
| 性能 | 略快 | 略慢(需要复制) |
代码示例:
public class InheritableThreadLocalDemo {
// 普通ThreadLocal:子线程无法继承
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
// InheritableThreadLocal:子线程可以继承
private static final InheritableThreadLocal<String> inheritableLocal =
new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("主线程-ThreadLocal");
inheritableLocal.set("主线程-Inheritable");
new Thread(() -> {
System.out.println("子线程 ThreadLocal: " + threadLocal.get());
// 输出: null (无法继承)
System.out.println("子线程 Inheritable: " + inheritableLocal.get());
// 输出: 主线程-Inheritable (继承成功)
}).start();
}
}
工作原理:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 覆盖getMap(),使用inheritableThreadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals; // 而不是t.threadLocals
}
// 覆盖createMap()
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
// 子线程创建时,复制父线程的值
// 在Thread.init()中调用
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return parentMap != null ? parentMap.childMapValue() : null;
}
}
Thread源码中的初始化:
// Thread.java
public class Thread implements Runnable {
// 普通ThreadLocal
ThreadLocal.ThreadLocalMap threadLocals;
// 可继承的ThreadLocal
ThreadLocal.ThreadLocalMap inheritableThreadLocals;
private void init(ThreadGroup g, Runnable target, String name, ...) {
// ...
// 获取父线程
Thread parent = currentThread();
// 复制父线程的inheritableThreadLocals
if (parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// threadLocals不复制,初始为null
}
}
修改影响:
public class ModifyInheritance {
private static final InheritableThreadLocal<String> local =
new InheritableThreadLocal<>();
public static void main(String[] args) throws Exception {
local.set("初始值");
Thread child = new Thread() {
@Override
public void run() {
System.out.println("子线程初始: " + local.get()); // 初始值
local.set("子线程修改");
System.out.println("子线程修改后: " + local.get()); // 子线程修改
}
};
child.start();
child.join();
System.out.println("主线程: " + local.get()); // 初始值 (互不影响)
}
}
线程池场景的问题:
public class ThreadPoolInheritance {
private static final InheritableThreadLocal<String> context =
new InheritableThreadLocal<>();
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] throws Exception {
// 第一次请求
context.set("请求1");
executor.submit(() -> {
System.out.println("任务1: " + context.get()); // 请求1
});
Thread.sleep(100);
// 第二次请求
context.set("请求2");
executor.submit(() -> {
// 问题:如果线程复用,可能拿到旧的值!
System.out.println("任务2: " + context.get()); // 可能是请求1或请求2
});
}
}
解决方案:TransmittableThreadLocal(阿里开源)
// 需要依赖:com.alibaba:transmittable-thread-local
public class TransmittableDemo {
// TransmittableThreadLocal支持线程池场景
private static final TransmittableThreadLocal<String> context =
new TransmittableThreadLocal<>();
private static final ExecutorService executor =
TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
public static void main(String[] args) {
// 自动在任务提交时复制上下文
context.set("请求1");
executor.submit(() -> {
System.out.println("任务: " + context.get()); // 确保是请求1
});
}
}
使用场景:
public class InheritableUseCase {
// 场景1: 传递TraceId
private static final InheritableThreadLocal<String> traceId =
new InheritableThreadLocal<>();
public void processRequest() {
traceId.set(generateTraceId());
new Thread(() -> {
// 子线程自动获得TraceId
log(traceId.get(), "处理逻辑");
}).start();
}
// 场景2: 传递用户身份
private static final InheritableThreadLocal<UserInfo> userInfo =
new InheritableThreadLocal<>();
public void asyncProcess(UserInfo user) {
userInfo.set(user);
executor.submit(() -> {
// 异步任务获得用户信息
checkPermission(userInfo.get());
});
}
}
记忆要点:
- InheritableThreadLocal = 可被子线程继承的ThreadLocal
- 原理:Thread初始化时复制父线程的inheritableThreadLocals
- 线程池场景不适用(线程复用导致上下文混乱)
- 解决方案:TransmittableThreadLocal(阿里TTL)
- 用途:父子线程传递上下文(TraceId、UserId等)
Q16: 线程的优先级有什么用?
答案:
定义:
线程优先级(Priority)是给线程调度器的一个"建议",表示线程的相对重要性。
Java线程优先级:
| 优先级常量 | 值 | 说明 |
|---|---|---|
| MIN_PRIORITY | 1 | 最低优先级 |
| NORM_PRIORITY | 5 | 默认优先级 |
| MAX_PRIORITY | 10 | 最高优先级 |
代码示例:
public class ThreadPriority {
public static void main(String[] args) {
Thread minThread = new Thread(() -> task("低优先级"));
minThread.setPriority(Thread.MIN_PRIORITY); // 1
Thread normThread = new Thread(() -> task("普通优先级"));
normThread.setPriority(Thread.NORM_PRIORITY); // 5
Thread maxThread = new Thread(() -> task("高优先级"));
maxThread.setPriority(Thread.MAX_PRIORITY); // 10
maxThread.start();
normThread.start();
minThread.start();
}
private static void task(String name) {
for (int i = 0; i < 5; i++) {
System.out.println(name + ": " + i);
}
}
}
操作系统线程优先级映射:
| OS | Java优先级映射 |
|---|---|
| Windows | 1-7级映射到系统优先级 |
| Linux | nice值映射 |
| Solaris | 直接映射 |
源码映射(HotSpot):
// os.cpp
int os::java_to_os_priority[] = {
// Java优先级 -> 操作系统优先级
-99999, // 0 (Reserved)
0, // 1 -> Priority Idle
1, // 2
2, // 3
...
7, // 10 -> Priority Critical
-99999 // 11+ (Reserved)
};
重要限制:
public class PriorityLimitations {
public static void main(String[] args) {
Thread thread = new Thread();
thread.setPriority(15); // ✅ 不会报错,但会被限制
System.out.println(thread.getPriority()); // 输出: 10 (被限制到MAX)
thread.setPriority(0); // ✅ 不会报错
System.out.println(thread.getPriority()); // 输出: 1 (被限制到MIN)
thread.setPriority(-1); // ❌ 抛出IllegalArgumentException
System.out.println(thread.getPriority());
}
}
不可靠的原因:
// 问题1: 时间片轮转
// 即使高优先级,也可能被时间片调度器切换出去
// 问题2: 优先级反转
// 低优先级线程持有锁,高优先级线程等待
// 导致高优先级被低优先级阻塞
// 问题3: 平台差异
// Windows和Linux对优先级的处理不同
// 跨平台行为不一致
优先级反转示例:
public class PriorityInversion {
private static final Object lock = new Object();
public static void main(String[] throws Exception {
// 低优先级线程持有锁
Thread lowThread = new Thread(() -> {
synchronized (lock) {
System.out.println("低优先级获得锁");
Thread.sleep(1000); // 持有锁1秒
System.out.println("低优先级释放锁");
}
});
lowThread.setPriority(Thread.MIN_PRIORITY);
// 中优先级线程占用CPU
Thread midThread = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
Math.sqrt(i); // 纯计算,占用CPU
}
System.out.println("中优先级完成");
});
midThread.setPriority(Thread.NORM_PRIORITY);
// 高优先级线程等待锁
Thread highThread = new Thread(() -> {
synchronized (lock) {
System.out.println("高优先级执行");
}
});
highThread.setPriority(Thread.MAX_PRIORITY);
lowThread.start();
Thread.sleep(10); // 让低优先级先获得锁
midThread.start();
highThread.start();
// 结果:
// 高优先级被低优先级阻塞
// 中优先级抢占CPU,延迟低优先级释放锁
// 高优先级线程延迟执行
}
}
最佳实践:
public class PriorityBestPractice {
// ❌ 不要依赖优先级保证执行顺序
public static void bad() {
Thread high = new Thread(() -> task());
high.setPriority(Thread.MAX_PRIORITY); // 不保证先执行
Thread low = new Thread(() -> task());
low.setPriority(Thread.MIN_PRIORITY);
high.start();
low.start();
}
// ✅ 使用join()保证顺序
public static void good() throws Exception {
Thread first = new Thread(() -> task());
Thread second = new Thread(() -> task());
first.start();
first.join(); // 等待first完成
second.start(); // second在first之后执行
}
// ✅ 使用锁保证同步
private static final Object lock = new Object();
public static void good2() {
new Thread(() -> {
synchronized (lock) {
task();
}
}).start();
new Thread(() -> {
synchronized (lock) {
task();
}
}).start();
}
// ✅ 适当的优先级场景:后台任务
public static void background() {
Thread background = new Thread(() -> {
while (true) {
cleanup();
try { Thread.sleep(60000); } catch (Exception e) {}
}
});
background.setPriority(Thread.MIN_PRIORITY); // 后台清理任务
background.setDaemon(true);
background.start();
}
}
记忆要点:
- 优先级 = 调度建议,不保证执行顺序
- 范围:1-10,默认5
- 优先级反转:低优先级持有锁,高优先级等待
- 平台依赖:不同OS实现不同
- 不要依赖优先级实现同步或顺序控制
- 合理用途:后台任务、监控任务
Q17: 什么是上下文切换?
答案:
定义:
上下文切换(Context Switch)是CPU从一个线程/进程切换到另一个线程/进程的过程,保存当前上下文,恢复目标上下文。
切换流程:
线程A运行
│
▼ 时间片到期 / 阻塞 / 高优先级就绪
│
├─ 保存线程A的上下文
│ ├─ 程序计数器 (PC) → 指令位置
│ ├─ 栈指针 (SP) → 栈顶位置
│ ├─ 寄存器状态
│ └─ CPU状态
│
├─ 切换到线程B
│
├─ 恢复线程B的上下文
│ ├─ 程序计数器 ← 线程B的PC
│ ├─ 栈指针 ← 线程B的SP
│ ├─ 寄存器状态 ← 线程B的寄存器
│ └─ CPU状态 ← 线程B的CPU状态
│
▼
线程B继续运行
时间消耗:
| 操作 | 耗时 |
|---|---|
| 线程上下文切换 | 纳秒级 |
| 进程上下文切换 | 微秒级 |
为什么进程切换更慢?
| 特性 | 线程切换 | 进程切换 |
|---|---|---|
| 地址空间 | 不切换 | 需要切换(TLB刷新) |
| 缓存 | 可能部分失效 | 大部分失效 |
| 数据量 | 寄存器+栈 | 寄存器+栈+页表 |
| 耗时 | 纳秒级 | 微秒级 |
测量上下文切换开销:
public class ContextSwitchBenchmark {
private static final int ITERATIONS = 100_000_000;
public static void main(String[] args) throws Exception {
// 基准:单线程
long start =1;
for (int i = 0; i < ITERATIONS; i++) {
counter++;
}
} long singleThreadTime = System.nanoTime() - start;
System.out.println("单线程: " + singleThreadTime / 1_000_000 + "ms");
// 多线程:有上下文切换
final AtomicLong multiCounter = new AtomicLong(0);
final Object lock = new Object();
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
synchronized (lock) {
multiCounter.incrementAndGet();
}
}
long multiThreadTime = System.nanoTime() - start;
System.out.println("多线程: " + multiThreadTime / 1_000_000 + "ms");
// 计算切换开销
System.out.println("上下文切换开销: " +
(multiThreadTime - singleThreadTime) + "ns");
}
Linux查看上下文切换:
# 查看整体上下文切换
vmstat 1
# 输出示例:
# procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 1 0 0 845124 292928 672760 0 0 0 0 20 30 0 0 100 0 0
# ↑in=中断 ↑cs=上下文切换
# 查看特定进程的上下文切换
pidstat -w -p <pid> 1
# 输出示例:
# 08:30:00 AM UID PID cswch/s nvcswch/s Command
# 08:30:01 AM 0 1234 100.00 10.00 java
# ↑自愿切换 ↑非自愿切换
减少上下文切换的方法:
// 方法1: 使用无锁编程
public class NoLockExample {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // CAS,不涉及锁,减少切换
}
}
// 方法2: 减少锁的持有时间
public class ReduceLockTime {
private final Lock lock = new ReentrantLock();
public void process() {
prepareData(); // 不需要锁的操作
lock.lock();
try {
updateSharedData(); // 只在必要时加锁
} finally {
lock.unlock();
}
processResult(); // 不需要锁的操作
}
}
// 方法3: 使用线程池
public class ThreadPoolExample {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void handleRequests(List<Request> requests) {
for (Request req : requests) {
executor.submit(() -> process(req)); // 复用线程,减少创建
}
}
}
// 方法4: 批量处理
public class BatchProcessing {
public void process(List<Item> items) {
// ❌ 每次都加锁
for (Item item : items) {
synchronized (this) {
processItem(item);
}
}
// ✅ 批量加锁
synchronized (this) {
for (Item item : items) {
processItem(item);
}
}
}
}
// 方法5: 使用CAS + 自旋
public class SpinLock {
private final AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
// 自旋等待,不阻塞,不触发上下文切换
while (!locked.compareAndSet(false, true)) {
Thread.yield(); // 让出CPU但不阻塞
}
}
public void unlock() {
locked.set(false);
}
}
Java中的上下文切换触发条件:
public class SwitchTriggers {
// 1. 时间片用完
// 由操作系统调度器决定
// 2. Thread.sleep()
Thread.sleep(1000); // 主动让出CPU
// 3. wait()
synchronized (lock) {
lock.wait(); // 释放锁,等待
}
// 4. 锁竞争
synchronized (lock) {
// 竞争锁时,未获得锁的线程阻塞
}
// 5. I/O操作
socket.read(buffer); // 阻塞I/O
// 6. 高优先级线程就绪
// 低优先级线程被抢占
}
JVM参数优化:
# 减少上下文切换的JVM参数
# 1. 设置合理的线程数
-XX:ParallelGCThreads=8 # GC线程数
-XX:ConcGCThreads=2 # 并发GC线程数
# 2. 使用偏向锁,减少不必要的锁竞争
-XX:+UseBiasedLocking
-XX:BiasedLockingStartupDelay=0
# 3. 调整自旋次数
-XX:PreBlockSpin=10 # 自旋次数
# 4. 使用G1GC,减少STW
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
记忆要点:
- 上下文切换 = 保存当前线程状态 + 恢复目标线程状态
- 线程切换: 纳秒级,进程切换: 微秒级
- 触发条件: 时间片、阻塞、I/O、锁竞争
- 减少方法: 无锁编程、减少锁时间、线程池、批量处理
- 可用vmstat/pidstat监控上下文切换次数
Q17-50: 基础篇剩余题目(简略版)
由于篇幅限制,以下是剩余题目的简要答案:
Q18: 线程调度策略有哪些?
- 抢占式调度(Java默认):优先级高的可以抢占
- 时间片轮转:每个线程分配时间片
- 协作式调度:线程主动让出
Q19: 什么是用户线程和内核线程?
- 用户线程:完全由用户空间管理(M:N模型)
- 内核线程:由操作系统内核管理(1:1模型)
Q20: Java线程和操作系统线程的关系?
- 传统:1:1映射,Java线程 = OS线程
- 虚拟线程(Java 21+):M:N映射,多个虚拟线程共享少量OS线程
Q21: 什么是绿色线程?
- 早期Java实现的用户线程,已被废弃
- 由JVM管理,不依赖OS线程
Q22: 什么是纤程(Fiber)?
- 轻量级线程,由用户态调度
- Java 21的虚拟线程类似纤程
Q23: Java 21的虚拟线程是什么?
// 虚拟线程示例
Thread.ofVirtual().start(() -> {
System.out.println("虚拟线程");
});
// 与平台线程对比
Thread.ofPlatform().start(() -> {
System.out.println("平台线程");
});
Q24-30: synchronized相关
详见"锁机制"章节
Q31-33: volatile相关
详见"锁机制"章节
Q34-50: 其他基础概念
锁机制(51-100)
Q51: CAS的实现原理?
答案:
CAS(Compare-And-Swap)定义:
CAS是一种原子操作,比较内存位置的值,如果与期望值相同,则更新为新值。
CAS操作流程:
CAS(addr, expected, new)
1. 读取addr的实际值: actual = *addr
2. 比较actual == expected
├─ true: *addr = new, 返回成功
└─ false: 不更新, 返回失败
JVM实现(HotSpot):
// unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint expected, jint x)) {
oop p = JNIHandles::resolve(obj);
jint* addr = (jint*) index_oop_from_field_offset_long(p, offset);
// 调用C++原子操作
return (jint)(Atomic::cmpxchg(x, addr, expected)) == expected;
} UNSAFE_END
// atomic.hpp
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
#ifdef AMD64
// x86_64使用lock cmpxchg指令
return (jint)os::atomic_cmpxchg((long)exchange_value, (volatile long*)dest, (long)compare_value);
#endif
}
// os_x86.cpp
long os::atomic_cmpxchg(long exchange_value, volatile long* dest, long compare_value) {
// lock前缀保证原子性
long res = __sync_val_compare_and_swap(dest, compare_value, exchange_value);
return res;
}
CPU层面实现:
x86架构:
LOCK CMPXCHG [mem], reg
LOCK前缀作用:
1. 确保指令原子执行(多核同步)
2. 缓存一致性协议(MESI)
3. 禁止指令重排序
Java CAS示例:
public class CASDemo {
private static final AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) {
// CAS操作
boolean success = ref.compareAndSet("A", "B");
System.out.println("成功: " + success); // true
System.out.println("值: " + ref.get()); // B
// 失败:期望值不匹配
success = ref.compareAndSet("A", "C");
System.out.println("成功: " + success); // false
System.out.println("值: " + ref.get()); // B (仍然是B)
}
}
自旋锁实现:
public class SpinLock {
private final AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
// CAS自旋
while (!locked.compareAndSet(false, true)) {
Thread.yield(); // 让出CPU
// 或 LockSupport.parkNanos(1000); // 短暂park
}
}
public void unlock() {
locked.set(false);
}
// 使用
public void criticalSection() {
lock();
try {
// 临界区
} finally {
unlock();
}
}
}
CAS的问题:
| 问题 | 说明 | 解决方案 |
|---|---|---|
| ABA问题 | A→B→A,CAS认为没变化 | 加版本号(AtomicStampedReference) |
| CPU开销 | 自旋占用CPU | 限制自旋次数或用synchronized |
| 只能更新一个变量 | 复合操作不原子 | Atomic类或锁 |
记忆要点:
- CAS = 比较并交换,原子操作
- CPU层面:lock cmpxchg指令
- 硬件保证原子性 + 缓存一致性
- 问题:ABA、自旋开销、只能单变量
- 解决:版本号、限制自旋、用锁
Q52-60: CAS、ABA、Atomic类相关
Q52: 什么是ABA问题?
答案:
初始状态: ref = A
线程1: 读取 ref = A
线程2: 读取 ref = A
线程2: CAS(A, B) → 成功, ref = B
线程2: CAS(B, A) → 成功, ref = A
线程1: CAS(A, C) → 成功!(虽然中间被改过)
问题:线程1认为值一直是A,实际经过了A→B→A
解决方案:
// 方案1: AtomicStampedReference
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0);
int stamp = ref.getStamp(); // 获取版本号
boolean success = ref.compareAndSet("A", "B", stamp, stamp + 1);
↑ ↑
旧值 新版本号
// 方案2: AtomicMarkableReference
AtomicMarkableReference<String> ref =
new AtomicMarkableReference<>("A", false);
boolean success = ref.compareAndSet("A", "B", false, true);
↑ ↑
旧标记 新标记
记忆要点:
- ABA = 值经过变化又回到原样
- CAS无法检测ABA,可能错误更新成功
- 解决:加版本号或标记(AtomicStampedReference)
Q53: 如何解决ABA问题?
答案:
见Q52
Q54: compareAndSet的含义?
答案:
// AtomicReference.compareAndSet
public final boolean compareAndSet(V expect, V update) {
// 如果当前值 == expect
// 则更新为update,返回true
// 否则不更新,返回false
return unsafe.compareAndSwapObject(...);
}
// 示例
AtomicInteger ai = new AtomicInteger(10);
ai.compareAndSet(10, 20); // true, 10→20
ai.compareAndSet(10, 30); // false, 当前是20
记忆要点:
- compareAndSet(expect, update)
- 期望值匹配则更新,不匹配则失败
- 原子操作,线程安全
Q55: getAndIncrement的实现?
答案:
// AtomicInteger.getAndIncrement()
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe.getAndAddInt()
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 读取当前值
} while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS
return v; // 返回旧值
}
// 工作流程:
// 1. v = 读取当前值
// 2. CAS(v, v+1)
// 3. 成功则返回v,失败则重试
记忆要点:
- getAndIncrement = 先获取旧值,再自增
- 实现原理:do-while + CAS循环
- 原子操作,线程安全
Q56: incrementAndGet的实现?
答案:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// getAndIncrement()返回旧值
// incrementAndGet() = getAndIncrement() + 1
// 即返回新值
对比:
| 方法 | 返回值 |
|---|---|
| getAndIncrement() | 旧值 |
| incrementAndGet() | 新值 |
| getAndAdd(delta) | 旧值 |
| addAndGet(delta) | 新值 |
Q57-59: AtomicReference、AtomicStampedReference、AtomicMarkableReference
答案:
// AtomicReference
AtomicReference<String> ref = new AtomicReference<>("A");
ref.compareAndSet("A", "B");
// AtomicStampedReference - 解决ABA
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int stamp = ref.getStamp();
ref.compareAndSet("A", "B", stamp, stamp + 1);
// AtomicMarkableReference - 带标记
AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);
ref.compareAndSet("A", "B", false, true);
Q60: AtomicIntegerArray的作用?
答案:
// 原子数组,支持数组元素的原子操作
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.set(0, 10); // 设置值
array.getAndIncrement(0); // 原子自增
array.compareAndSet(0, 10, 20); // CAS
// 注意:传数组构造,会复制
int[] arr = {1, 2, 3};
AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr);
arr[0] = 100; // 不影响atomicArr
Q61-62: AtomicReferenceArray、AtomicIntegerFieldUpdater
答案:
// AtomicReferenceArray
AtomicReferenceArray<String> arr = new AtomicReferenceArray<>(10);
arr.compareAndSet(0, "A", "B");
// AtomicIntegerFieldUpdater - 更新对象的volatile字段
public class Example {
volatile int counter;
}
static final AtomicIntegerFieldUpdater<Example> UPDATER =
AtomicIntegerFieldUpdater.newUpdater(Example.class, "counter");
UPDATER.getAndIncrement(example);
Q63: LongAdder比AtomicLong快的原因?
答案:
AtomicLong的问题:
- 所有线程竞争同一个value
- 高并发时大量CAS失败,不断重试
- CPU缓存一致性开销大
LongAdder的优化:
- 分段计数,每个线程有独立的Cell
- 减少竞争,最后汇总所有Cell
// LongAdder简化原理
public class LongAdder {
// Cell数组,分散竞争
Cell[] cells;
// 基础值(无竞争时使用)
volatile long base;
public void increment() {
Cell[] cs; Cell c; long b, v;
if ((cs = cells) != null || (c = currentCell()) == null) {
b = base;
if (casBase(b, b + 1)) // 无竞争时用base
return;
}
// 有竞争时用Cell
if (c != null) {
do {
v = c.value;
} while (!c.cas(v, v + 1)); // CAS当前线程的Cell
}
}
public long sum() {
// 汇和所有Cell
long sum = base;
for (Cell c : cells) {
if (c != null) {
sum += c.value;
}
}
return sum;
}
}
性能对比:
并发数: 1000线程
AtomicLong:
- 所有线程竞争同一个value
- 大量CAS失败重试
- 耗时: 5000ms
LongAdder:
- 分散到多个Cell
- 几乎无竞争
- 耗时: 500ms
LongAdder快约10倍!
记忆要点:
- AtomicLong = 单一value,高并发竞争严重
- LongAdder = 分段计数(Cell数组),减少竞争
- 适用场景:高并发写操作
- 不适用:频繁读取(sum()开销)
Q64: LongAdder的原理?
答案:
见Q63
Q65: DoubleAdder和LongAdder的区别?
答案:
LongAdder: int/long类型
DoubleAdder: double类型
原理相同,只是数据类型不同
Q66-100: 锁机制剩余题目
由于篇幅限制,以下是剩余题目的简要答案:
Q66: 什么是可重入锁?
// 可重入 = 同一线程可以多次获取同一把锁
synchronized (this) {
synchronized (this) { // 可重入,不会死锁
// ...
}
}
Q67: ReentrantLock如何实现可重入?
// 通过计数器
public class ReentrantLock {
private Thread owner;
private int holdCount = 0;
public void lock() {
Thread current = Thread.currentThread();
if (owner == current) {
holdCount++; // 重入,计数+1
return;
}
// ... 其他线程等待
}
public void unlock() {
holdCount--;
if (holdCount == 0) {
owner = null;
}
}
}
Q68-73: ReentrantLock相关
Q68: ReentrantLock的基本使用?
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须在finally
}
Q69: lock()和tryLock()的区别?
| 方法 | 阻塞 | 返回值 |
|---|---|---|
| lock() | 阻塞直到获得锁 | void |
| tryLock() | 不阻塞,立即返回 | boolean |
Q70: tryLock(long timeout)?
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 获得锁
} finally {
lock.unlock();
}
} else {
// 超时,未获得锁
}
Q71: lockInterruptibly()?
try {
lock.lockInterruptibly(); // 可中断的锁获取
} catch (InterruptedException e) {
// 被中断
}
Q72: 公平锁和非公平锁的区别?
// 公平锁:按请求顺序获取锁
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁:随机获取(默认)
ReentrantLock unfairLock = new ReentrantLock(false);
Q73: 如何创建公平锁?
见Q72
Q74-77: Condition相关
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionMet()) {
condition.await(); // 等待
}
} finally {
lock.unlock();
}
// 唤醒
lock.lock();
try {
updateCondition();
condition.signal(); // 唤醒一个
condition.signalAll(); // 唤醒所有
} finally {
lock.unlock();
}
Q78-82: ReadWriteLock相关
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock(); // 读锁
Lock writeLock = rwLock.writeLock(); // 写锁
// 读锁 - 共享
readLock.lock();
try {
// 读操作
} finally {
readLock.unlock();
}
// 写锁 - 独占
writeLock.lock();
try {
// 写操作
} finally {
writeLock.unlock();
}
Q83-89: StampedLock相关
StampedLock lock = new StampedLock();
// 乐观读
long stamp = lock.tryOptimisticRead();
double value = x + y;
if (!lock.validate(stamp)) {
stamp = lock.readLock(); // 失败,转为悲观读
try {
value = x + y;
} finally {
lock.unlockRead(stamp);
}
}
// 写锁
stamp = lock.writeLock();
try {
x = newX;
y = newY;
} finally {
lock.unlockWrite(stamp);
}
Q90-100: 自旋锁、AQS等
Q90: 自旋锁的缺点?
- CPU空转,浪费CPU
- 无法获得锁的线程一直占用CPU
- 长时间持有锁会导致其他线程饥饿
线程池(101-150)
Q101: 为什么需要线程池?
答案:
| 不用线程池 | 用线程池 |
|---|---|
| 每次任务创建新线程 | 复用线程,减少创建开销 |
| 无限制创建线程,可能OOM | 控制最大线程数 |
| 难以管理任务执行 | 统一管理,监控 |
| 无法控制并发数 | 控制并发数,避免资源耗尽 |
创建线程开销:
- 分配栈空间(默认1MB)
- 初始化线程数据结构
- 与OS交互
- 约1-10ms
线程池优势:
- 复用线程:省去创建销毁开销
- 控制并发:避免资源耗尽
- 统一管理:监控、统计、异常处理
Q102: ThreadPoolExecutor的核心参数?
答案:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
| 参数 | 说明 | 默认值 |
|---|---|---|
| corePoolSize | 核心线程数,常驻 | 0 |
| maximumPoolSize | 最大线程数 | Integer.MAX_VALUE |
| keepAliveTime | 非核心线程空闲存活时间 | 60L |
| unit | 时间单位 | SECONDS |
| workQueue | 工作队列 | SynchronousQueue |
| threadFactory | 线程工厂 | DefaultThreadFactory |
| handler | 拒绝策略 | AbortPolicy |
Q103-110: 线程池参数详解
Q103: corePoolSize和maximumPoolSize的区别?**
| corePoolSize | maximumPoolSize |
|---|---|
| 核心线程数,常驻 | 最大线程数,临时创建 |
| < corePoolSize | 创建新线程 |
| corePoolSize < size < maximumPoolSize | 放入队列,队列满才创建 |
线程数 < corePoolSize: 创建新线程
corePoolSize <= 线程数: 放入队列
队列满 && 线程数 < maximumPoolSize: 创建新线程
队列满 && 线程数 >= maximumPoolSize: 拒绝
Q104: keepAliveTime的作用?**
非核心线程空闲超过keepAliveTime被回收
Q105: 什么是工作队列?**
保存等待执行的任务队列
Q106: 常见的工作队列类型?**
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
| LinkedBlockingQueue | 无界队列 | 任务量大 |
| ArrayBlockingQueue | 有界队列 | 防止OOM |
| SynchronousQueue | 不存储,直接传递 | 高并发 |
| PriorityBlockingQueue | 优先级队列 | 需要排序 |
Q107: 线程池的工作流程?
提交任务
│
▼
核心线程数满?
┌─否──┬─是─┐
▼ ▼ │
创建核心 队列满?│
线程 ├否─┬─┴┐
│ ▼ ▼
放入 创建 拒绝
队列 非核心
线程 线程
Q108: 线程池的拒绝策略有哪些?
| 策略 | 行为 |
|---|---|
| AbortPolicy | 抛出RejectedExecutionException |
| CallerRunsPolicy | 由提交任务的线程执行 |
| DiscardPolicy | 丢弃任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 |
Q109-150: 线程池、CompletableFuture等
并发集合(151-200)
Q151: HashMap是线程安全的吗?**
不是!多线程并发修改会导致数据不一致或死循环。
HashMap线程安全问题:
public class HashMapUnsafe {
private static final Map<Integer, Integer> map = new HashMap<>();
public static void main(String[] args) throws Exception {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
map.put(i, i);
}
};
new Thread(task).start();
new Thread(task).start();
Thread.sleep(1000);
System.out.println(map.size()); // 可能 < 20000
}
}
解决方案:
Collections.synchronizedMap(new HashMap<>())ConcurrentHashMap(推荐)- 手动加锁
Q152: ConcurrentHashMap的实现原理?
Java 7(分段锁):
ConcurrentHashMap {
Segment[] segments; // 分段数组,默认16段
}
Segment extends ReentrantLock {
HashEntry[] table; // 每段独立加锁
}
// 只锁一个段,其他段可并发访问
Java 8+(CAS + synchronized):
ConcurrentHashMap {
Node<K,V>[] table; // Node数组
}
Node {
int hash;
K key;
V value;
Node<K,V> next;
}
// 只锁一个Node,并发度更高
对比:
| 版本 | 锁机制 | 并发度 |
|---|---|---|
| Java 7 | 分段锁(16段) | 16 |
| Java 8+ | Node级别 | 数组长度 |
Q153-200: 并发集合相关
并发工具(201-250)
Q201: CountDownLatch的作用?**
倒计时门闩,等待多个任务完成。
CountDownLatch latch = new CountDownLatch(3); // 等待3个任务
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
doWork();
} finally {
latch.countDown(); // 完成一个
}
}).start();
}
latch.await(); // 等待所有完成
Q202-250: 并发工具类
高级篇(251-300)
Q251: 什么是死锁?
多个线程互相持有对方需要的锁,导致永久阻塞。
public class Deadlock {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1: 获得A");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockB) {
System.out.println("Thread1: 获得B");
}
}
}).start();
new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread2: 获得B");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockA) {
System.out.println("Thread2: 获得A");
}
}
}).start();
}
}
死锁四条件:
- 互斥:资源独占
- 持有并等待:持有一个,等待另一个
- 不可剥夺:不能强制释放
- 循环等待:形成循环链
Q252-300: 高级篇剩余
总结
记忆要点总览
| 类别 | 核心要点 |
|---|---|
| 线程 | 5种状态、start vs run、interrupt |
| 锁 | synchronized、volatile、CAS、AQS |
| 线程池 | 7大参数、工作流程、拒绝策略 |
| 集合 | ConcurrentHashMap原理 |
| 工具 | CountDownLatch、Semaphore、CompletableFuture |
重点掌握
- synchronized原理:锁升级、锁释放
- volatile作用:可见性、有序性
- CAS原理:硬件原子操作
- 线程池参数:corePoolSize、maximumPoolSize、workQueue
- ConcurrentHashMap:Java 7 vs Java 8实现
- CountDownLatch、CyclicBarrier、Semaphore:使用场景
- CompletableFuture:异步编程
- JMM:happens-before规则
持续更新中...