1. 基础面试题
1.1 线程基础
1.1.1 进程和线程的区别?
进程:
- 进程是操作系统分配资源的基本单位
- 每个进程有独立的内存空间
- 进程之间相互独立,通信需要通过IPC(进程间通信)
线程:
- 线程是CPU调度的基本单位
- 线程共享进程的内存空间
- 线程之间可以直接访问共享数据
主要区别:
- 资源占用:进程占用资源多,线程占用资源少
- 切换开销:进程切换开销大,线程切换开销小
- 通信方式:进程需要IPC,线程可以直接共享内存
- 稳定性:一个进程崩溃不影响其他进程,一个线程崩溃可能影响整个进程
1.1.2 创建线程有几种方式?
方式1:继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 使用
MyThread thread = new MyThread();
thread.start(); // 启动线程
方式2:实现Runnable接口(推荐)
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 使用
Thread thread = new Thread(new MyTask());
thread.start();
方式3:实现Callable接口(有返回值)
class MyTask implements Callable<String> {
@Override
public String call() throws Exception {
return "结果";
}
}
// 使用
FutureTask<String> future = new FutureTask<>(new MyTask());
Thread thread = new Thread(future);
thread.start();
String result = future.get(); // 获取返回值
方式4:使用线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.execute(() -> System.out.println("线程执行"));
推荐: 实现Runnable接口,因为Java单继承,实现接口更灵活。
1.1.3 start()和run()方法的区别?
start()方法:
- 启动新线程,执行run()方法
- 只能调用一次,多次调用会抛出异常
- 异步执行,不阻塞当前线程
run()方法:
- 只是普通方法,在当前线程中执行
- 可以多次调用
- 同步执行,会阻塞当前线程
Thread thread = new Thread(() -> System.out.println("执行"));
thread.start(); // 启动新线程,异步执行
// thread.run(); // 在当前线程执行,同步执行
1.1.4 sleep()和wait()的区别?
sleep()方法:
- Thread类的静态方法
- 不释放锁
- 时间到了自动唤醒
- 可以在任何地方调用
wait()方法:
- Object类的实例方法
- 释放锁
- 需要notify()或notifyAll()唤醒
- 必须在synchronized块中调用
// sleep():不释放锁
synchronized (lock) {
Thread.sleep(1000); // 睡眠1秒,但不释放lock
}
// wait():释放锁
synchronized (lock) {
lock.wait(); // 等待,释放lock,其他线程可以获取锁
}
1.1.5 yield()和join()的区别?
yield()方法:
- 让出CPU时间片,让其他线程执行
- 不保证让出后立即执行其他线程
- 只是建议,JVM可能忽略
join()方法:
- 等待目标线程执行完成
- 当前线程会阻塞,直到目标线程结束
// yield():让出CPU
Thread.yield(); // 当前线程让出CPU,但不阻塞
// join():等待线程完成
Thread thread = new Thread(() -> {
// 执行任务
});
thread.start();
thread.join(); // 等待thread执行完成
1.1.6 如何停止一个线程?
不推荐的方式:
stop()方法:已废弃,不安全suspend()和resume()方法:已废弃,容易死锁
推荐的方式:使用标志位
class MyThread extends Thread {
private volatile boolean running = true; // 标志位
public void stopThread() {
running = false; // 设置标志位
}
@Override
public void run() {
while (running) { // 检查标志位
// 执行任务
}
}
}
使用interrupt()方法:
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
});
thread.start();
thread.interrupt(); // 中断线程
1.1.7 守护线程是什么?
守护线程是为其他线程服务的线程,当所有非守护线程结束时,守护线程会自动结束。
Thread thread = new Thread(() -> {
while (true) {
// 执行任务
}
});
thread.setDaemon(true); // 设置为守护线程
thread.start();
// 主线程结束后,守护线程会自动结束
特点:
- 守护线程不能阻止JVM退出
- 适合后台任务,如垃圾回收线程
1.1.8 线程的优先级有什么用?
线程优先级是给JVM的提示,优先级高的线程更可能被调度执行,但不保证。
Thread thread = new Thread(() -> {
// 执行任务
});
thread.setPriority(Thread.MAX_PRIORITY); // 最高优先级
thread.setPriority(Thread.MIN_PRIORITY); // 最低优先级
thread.setPriority(Thread.NORM_PRIORITY); // 普通优先级(默认)
注意: 不同操作系统对优先级的处理不同,不要依赖优先级来保证执行顺序。
1.1.9 线程的生命周期有哪些状态?
线程有6种状态:
1. NEW(新建)
- 线程对象已创建,但还没有调用start()方法
2. RUNNABLE(可运行)
- 线程正在JVM中执行,可能在等待CPU时间片
3. BLOCKED(阻塞)
- 线程等待获取监视器锁(synchronized)
4. WAITING(等待)
- 线程无限期等待,需要其他线程唤醒
- 调用wait()、join()、LockSupport.park()等方法
5. TIMED_WAITING(超时等待)
- 线程等待指定时间
- 调用sleep()、wait(timeout)、join(timeout)等方法
6. TERMINATED(终止)
- 线程执行完成或异常退出
Thread thread = new Thread(() -> {
// 执行任务
});
System.out.println(thread.getState()); // NEW
thread.start();
System.out.println(thread.getState()); // RUNNABLE
// 等待线程完成
thread.join();
System.out.println(thread.getState()); // TERMINATED
1.1.10 线程上下文切换是什么?
线程上下文切换是指CPU从一个线程切换到另一个线程执行。
上下文切换的过程:
- 保存当前线程的状态(寄存器、程序计数器等)
- 加载新线程的状态
- 切换到新线程执行
上下文切换的开销:
- 需要保存和恢复线程状态
- 需要刷新CPU缓存
- 消耗CPU时间
如何减少上下文切换:
- 减少线程数量
- 使用线程池复用线程
- 避免不必要的线程创建
1.1.11 什么是线程安全?
线程安全是指多个线程同时访问同一个对象时,不需要额外的同步机制,也能保证程序的正确性。
线程安全的实现方式:
- 不可变对象:对象创建后不能被修改
- 同步机制:使用synchronized、Lock等
- 无锁编程:使用CAS、原子类等
- 线程封闭:每个线程使用独立的对象
1.1.12 如何实现线程间通信?
方式1:共享变量
private volatile boolean flag = false;
// 线程1
flag = true;
// 线程2
if (flag) {
// 执行
}
方式2:wait/notify
synchronized (lock) {
lock.wait(); // 等待
lock.notify(); // 唤醒
}
方式3:Lock/Condition
Condition condition = lock.newCondition();
condition.await(); // 等待
condition.signal(); // 唤醒
方式4:BlockingQueue
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
queue.put("data"); // 生产者
String data = queue.take(); // 消费者
1.1.13 线程的run()方法可以多次调用吗?
可以。run()方法只是普通方法,可以多次调用。
Thread thread = new Thread(() -> System.out.println("执行"));
thread.run(); // 可以调用
thread.run(); // 可以再次调用
thread.run(); // 可以多次调用
注意: 但start()方法只能调用一次,多次调用会抛出异常。
1.1.14 线程的interrupt()方法有什么用?
interrupt()方法用于中断线程,但不会立即停止线程。
interrupt()的作用:
- 设置线程的中断标志位
- 如果线程在wait()、sleep()、join()等方法中,会抛出InterruptedException
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 检查中断标志
// 执行任务
}
});
thread.start();
thread.interrupt(); // 中断线程
1.1.15 如何判断线程是否被中断?
方式1:isInterrupted()方法
- 检查中断标志位,不清除标志位
方式2:interrupted()方法
- 检查中断标志位,并清除标志位(设置为false)
Thread thread = Thread.currentThread();
// 方式1:不清除标志位
if (thread.isInterrupted()) {
// 线程被中断
}
// 方式2:清除标志位
if (Thread.interrupted()) {
// 线程被中断,标志位已清除
}
1.1.16 线程的sleep()和wait()有什么区别?
| 特性 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 释放锁 | 不释放 | 释放 |
| 唤醒方式 | 时间到了自动唤醒 | 需要notify()唤醒 |
| 使用位置 | 任何地方 | synchronized块中 |
// sleep():不释放锁
synchronized (lock) {
Thread.sleep(1000); // 睡眠1秒,但不释放lock
}
// wait():释放锁
synchronized (lock) {
lock.wait(); // 等待,释放lock,其他线程可以获取锁
}
1.1.17 什么是虚假唤醒?
虚假唤醒是指线程在没有被notify()的情况下被唤醒。
原因: 操作系统层面的原因,可能导致wait()在没有notify()的情况下返回。
解决方案: 使用while循环而不是if判断。
// 错误的做法:使用if
synchronized (lock) {
if (condition) {
lock.wait(); // 可能虚假唤醒
}
}
// 正确的做法:使用while
synchronized (lock) {
while (condition) { // 循环检查,防止虚假唤醒
lock.wait();
}
}
1.1.18 线程的join()方法有什么用?
join()方法用于等待目标线程执行完成。
Thread thread = new Thread(() -> {
// 执行任务
});
thread.start();
thread.join(); // 等待thread执行完成
System.out.println("thread已执行完成");
join()的原理:
- 调用wait()方法等待目标线程
- 目标线程执行完成后,会调用notifyAll()唤醒等待的线程
1.1.19 如何实现线程的定时执行?
方式1:使用Timer(不推荐)
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("执行任务");
}
}, 1000, 2000); // 延迟1秒,每2秒执行一次
方式2:使用ScheduledExecutorService(推荐)
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
scheduler.scheduleAtFixedRate(() -> {
System.out.println("执行任务");
}, 1, 2, TimeUnit.SECONDS); // 延迟1秒,每2秒执行一次
1.1.20 线程的yield()方法有什么用?
yield()方法让出CPU时间片,让其他线程执行。
Thread.yield(); // 让出CPU,但不保证其他线程会立即执行
注意: yield()只是建议,JVM可能忽略。不要依赖yield()来保证执行顺序。
1.1.21 线程的上下文切换开销有多大?
线程上下文切换的开销包括:
- 保存和恢复线程状态(寄存器、程序计数器等)
- 刷新CPU缓存
- 调度器选择下一个线程
开销: 通常需要几微秒到几十微秒,频繁切换会影响性能。
如何减少:
- 减少线程数量
- 使用线程池复用线程
- 避免不必要的线程创建
1.1.22 什么是用户线程和内核线程?
用户线程:
- 在用户空间实现的线程
- 内核不知道用户线程的存在
- 切换开销小,但一个线程阻塞会影响所有线程
内核线程:
- 由操作系统内核管理的线程
- 内核负责线程的调度
- 切换开销大,但一个线程阻塞不影响其他线程
Java线程:
- Java线程是内核线程的映射
- 一个Java线程对应一个内核线程
- 线程的创建和调度由操作系统负责
1.1.23 线程的栈空间有多大?
默认栈大小:
- 不同操作系统和JVM版本不同
- 通常1MB左右
设置栈大小:
// 使用-Xss参数设置
// java -Xss2m MyClass
// 或者在创建线程时设置
Thread thread = new Thread(null, () -> {
// 任务
}, "ThreadName", 2 * 1024 * 1024); // 栈大小2MB
注意: 栈空间太小可能导致StackOverflowError,太大可能创建更少的线程。
1.1.24 如何实现线程的优先级调度?
Java的线程优先级只是给JVM的提示,不保证执行顺序。
Thread thread1 = new Thread(() -> System.out.println("线程1"));
Thread thread2 = new Thread(() -> System.out.println("线程2"));
thread1.setPriority(Thread.MAX_PRIORITY); // 最高优先级
thread2.setPriority(Thread.MIN_PRIORITY); // 最低优先级
thread1.start();
thread2.start();
// 不保证thread1一定先执行
注意: 不要依赖线程优先级来保证执行顺序,应该使用同步机制。
1.1.25 线程的ThreadGroup有什么用?
ThreadGroup用于管理线程组,可以对一组线程进行操作。
ThreadGroup group = new ThreadGroup("MyGroup");
Thread thread1 = new Thread(group, () -> System.out.println("线程1"));
Thread thread2 = new Thread(group, () -> System.out.println("线程2"));
// 中断整个线程组
group.interrupt();
// 获取线程组中的线程数
int count = group.activeCount();
注意: ThreadGroup已经不推荐使用,建议使用线程池管理线程。
1.1.26 线程的状态如何转换?
状态转换图:
NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
转换条件:
- NEW → RUNNABLE:调用start()方法
- RUNNABLE → BLOCKED:等待获取synchronized锁
- RUNNABLE → WAITING:调用wait()、join()等方法
- RUNNABLE → TIMED_WAITING:调用sleep()、wait(timeout)等方法
- 各种状态 → TERMINATED:线程执行完成或异常退出
1.1.27 线程的调度方式有哪些?
1. 抢占式调度(Java使用)
- 操作系统决定线程的执行时间
- 线程可能在任何时候被中断
- 适合多任务系统
2. 协作式调度
- 线程主动让出CPU
- 线程不会被强制中断
- 适合单任务系统
Java线程: 使用抢占式调度,由操作系统负责线程调度。
1.1.28 线程的本地变量是什么?
线程的本地变量是每个线程独立拥有的变量。
实现方式:
- ThreadLocal:每个线程有独立的副本
- 局部变量:每个线程的栈中有独立的副本
// ThreadLocal:每个线程有独立的副本
ThreadLocal<String> local = new ThreadLocal<>();
local.set("值"); // 当前线程设置
String value = local.get(); // 当前线程获取
// 局部变量:每个线程的栈中有独立的副本
public void method() {
String local = "值"; // 每个线程的栈中有独立的副本
}
1.1.29 线程的异常会传播到哪里?
未捕获的异常:
- 会传播到线程的UncaughtExceptionHandler
- 如果没有设置,会使用默认的处理器
// 设置异常处理器
Thread thread = new Thread(() -> {
throw new RuntimeException("异常");
});
thread.setUncaughtExceptionHandler((t, e) -> {
System.err.println("线程 " + t.getName() + " 发生异常: " + e.getMessage());
});
thread.start();
1.1.30 如何实现线程的定时任务?
使用ScheduledExecutorService:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
// 延迟执行
scheduler.schedule(() -> {
System.out.println("延迟执行");
}, 5, TimeUnit.SECONDS);
// 周期性执行(固定频率)
scheduler.scheduleAtFixedRate(() -> {
System.out.println("周期性执行");
}, 0, 1, TimeUnit.SECONDS);
// 周期性执行(固定间隔)
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("周期性执行");
}, 0, 1, TimeUnit.SECONDS);
1.2 线程安全
1.2.1 什么是线程安全?
线程安全是指多个线程同时访问同一个对象时,不需要额外的同步机制,也能保证程序的正确性。
// 线程不安全的例子
public class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子操作,线程不安全
}
}
// 线程安全的例子
public class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作,线程安全
}
}
1.2.2 如何保证线程安全?
方式1:使用synchronized
public synchronized void method() {
// 同步方法
}
public void method() {
synchronized (this) {
// 同步代码块
}
}
方式2:使用Lock
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
方式3:使用volatile
private volatile boolean flag = false; // 保证可见性
方式4:使用原子类
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子操作
方式5:使用不可变对象
public final class ImmutablePoint {
private final int x;
private final int y;
// 不可变对象天然线程安全
}
1.2.3 什么是竞态条件?
竞态条件是指多个线程访问共享资源时,执行结果依赖于线程的执行顺序。
// 竞态条件的例子
public class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子操作
// 线程1读取count=0,线程2也读取count=0
// 线程1执行count=1,线程2也执行count=1
// 结果应该是2,但实际是1(丢失更新)
}
}
1.2.4 什么是数据竞争?
数据竞争是指多个线程在没有同步的情况下,同时访问同一个变量,且至少有一个是写操作。
// 数据竞争的例子
private int count = 0;
// 线程1
count = 1; // 写操作
// 线程2
int value = count; // 读操作
// 没有同步,可能读到旧值
1.2.5 可见性、原子性、有序性分别是什么?
可见性:
- 一个线程修改了共享变量,其他线程能立即看到
- volatile保证可见性
private volatile boolean flag = false;
// 线程1
flag = true; // 修改
// 线程2
if (flag) { // 能立即看到修改
// 执行
}
原子性:
- 操作要么全部执行,要么全部不执行
- synchronized、Lock、原子类保证原子性
// 不是原子操作
count++; // 分为:读取、加1、写入
// 原子操作
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子操作
有序性:
- 程序执行的顺序按照代码的先后顺序
- volatile、synchronized保证有序性
// 可能重排序
int a = 1;
int b = 2;
// 可能先执行b=2,再执行a=1
// 保证有序性
volatile int a = 1;
int b = 2; // 不会在a=1之前执行
2. synchronized面试题
2.1 基础问题
2.1.1 synchronized的作用是什么?
synchronized用于保证线程安全,可以保证:
- 原子性:同一时刻只有一个线程能执行同步代码
- 可见性:线程修改后,其他线程能立即看到
- 有序性:防止指令重排序
// 同步方法
public synchronized void method() {
// 同一时刻只有一个线程能执行
}
// 同步代码块
public void method() {
synchronized (this) {
// 同一时刻只有一个线程能执行
}
}
2.1.2 synchronized的三种用法?
1. 修饰实例方法
public synchronized void method() {
// 锁的是当前对象(this)
}
2. 修饰静态方法
public static synchronized void method() {
// 锁的是类对象(Class对象)
}
3. 修饰代码块
public void method() {
synchronized (obj) {
// 锁的是指定对象
}
}
2.1.3 synchronized锁的是什么?
- 修饰实例方法:锁的是当前对象(this)
- 修饰静态方法:锁的是类对象(Class对象)
- 修饰代码块:锁的是指定对象
// 锁的是this对象
public synchronized void method1() {
// 代码
}
// 锁的是MyClass.class对象
public static synchronized void method2() {
// 代码
}
// 锁的是obj对象
public void method3() {
synchronized (obj) {
// 代码
}
}
2.1.4 synchronized和volatile的区别?
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证(如i++) |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 阻塞 | 会阻塞 | 不会阻塞 |
| 粒度 | 代码块或方法 | 变量 |
// synchronized:保证原子性
private int count = 0;
public synchronized void increment() {
count++; // 原子操作
}
// volatile:不保证原子性
private volatile int count = 0;
public void increment() {
count++; // 不是原子操作,需要配合synchronized
}
2.2 原理问题
2.2.1 synchronized的实现原理?
synchronized通过对象头中的Mark Word来实现锁机制。
对象头结构:
- Mark Word:存储锁信息、GC信息等
- Class Pointer:指向类元数据
- Array Length:数组长度(如果是数组)
锁的升级过程:
- 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
// synchronized的实现
public void method() {
synchronized (this) {
// 1. 检查对象头的Mark Word
// 2. 如果是无锁状态,升级为偏向锁
// 3. 如果有竞争,升级为轻量级锁
// 4. 如果竞争激烈,升级为重量级锁
}
}
2.2.2 什么是对象头?
对象头是Java对象的一部分,存储对象的元数据信息。
对象头包含:
- Mark Word:存储锁信息、GC信息、哈希码等
- Class Pointer:指向类元数据
- Array Length:数组长度(如果是数组)
Mark Word的结构(64位JVM):
| 锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
|--------|-------|-------|------|------|------|------|
| 无锁 | 未使用| 哈希码| 未使用| 分代年龄| 0 | 01 |
| 偏向锁 | 线程ID| Epoch | 未使用| 分代年龄| 1 | 01 |
| 轻量级锁| 指向栈中锁记录的指针 | 00 |
| 重量级锁| 指向互斥量的指针 | 10 |
2.2.3 什么是Mark Word?
Mark Word是对象头的一部分,存储对象的锁信息、GC信息、哈希码等。
Mark Word的作用:
- 存储锁的状态(无锁、偏向锁、轻量级锁、重量级锁)
- 存储GC分代年龄
- 存储对象的哈希码
- 存储线程ID(偏向锁时)
2.2.4 锁的升级过程?
锁升级路径:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1. 无锁状态
- 对象刚创建时,处于无锁状态
- 任何线程都可以获取锁
2. 偏向锁
- 只有一个线程访问时,升级为偏向锁
- 在Mark Word中记录线程ID
- 同一线程再次访问时,无需加锁
3. 轻量级锁
- 有多个线程竞争时,升级为轻量级锁
- 使用CAS操作获取锁
- 如果CAS失败,说明有竞争,升级为重量级锁
4. 重量级锁
- 竞争激烈时,升级为重量级锁
- 使用操作系统互斥量(Mutex)
- 线程会阻塞,等待唤醒
2.2.5 偏向锁、轻量级锁、重量级锁的区别?
| 锁类型 | 适用场景 | 性能 | 实现方式 |
|---|---|---|---|
| 偏向锁 | 只有一个线程访问 | 最好 | Mark Word记录线程ID |
| 轻量级锁 | 少量线程竞争 | 较好 | CAS操作 |
| 重量级锁 | 大量线程竞争 | 较差 | 操作系统互斥量 |
2.2.6 什么时候会升级为重量级锁?
情况1:CAS失败多次
- 轻量级锁使用CAS获取锁
- 如果CAS失败多次,说明竞争激烈,升级为重量级锁
情况2:调用wait()方法
- wait()方法需要重量级锁支持
- 会自动升级为重量级锁
情况3:锁竞争激烈
- 多个线程同时竞争锁
- JVM检测到竞争激烈,升级为重量级锁
2.2.7 锁消除和锁粗化是什么?
锁消除:
- JVM检测到不可能存在共享数据竞争
- 自动消除锁,提高性能
// 锁消除的例子
public void method() {
String str = new String("test"); // 局部变量,不可能被其他线程访问
synchronized (str) { // JVM会消除这个锁
// 代码
}
}
锁粗化:
- 将多个连续的锁操作合并为一个
- 减少锁的获取和释放次数
// 锁粗化的例子
public void method() {
for (int i = 0; i < 100; i++) {
synchronized (lock) { // JVM可能将100次加锁合并为1次
// 代码
}
}
}
2.3 性能问题
2.3.1 synchronized的性能如何?
JDK 1.6之前:
- 性能较差,直接使用重量级锁
- 线程会阻塞,上下文切换开销大
JDK 1.6之后:
- 引入了锁升级机制
- 无竞争时性能很好(偏向锁)
- 少量竞争时性能较好(轻量级锁)
- 大量竞争时性能较差(重量级锁)
优化建议:
- 减少锁的持有时间
- 减小锁的粒度
- 使用读写锁(读多写少场景)
2.3.2 如何优化synchronized的性能?
1. 减少锁的持有时间
// 不好的做法
public void method() {
synchronized (lock) {
doSomething1(); // 不需要同步
doSomething2(); // 不需要同步
count++; // 只有这里需要同步
}
}
// 好的做法
public void method() {
doSomething1();
doSomething2();
synchronized (lock) {
count++; // 只在需要的地方加锁
}
}
2. 减小锁的粒度
// 使用细粒度锁
private final Object lock1 = new Object();
private final Object lock2 = new Object();
3. 使用读写锁
ReadWriteLock lock = new ReentrantReadWriteLock();
// 读操作使用读锁,可以并发
// 写操作使用写锁,独占
3. volatile面试题
3.1 基础问题
3.1.1 volatile的作用是什么?
volatile有两个作用:
- 保证可见性:一个线程修改了volatile变量,其他线程能立即看到
- 禁止指令重排序:防止编译器优化导致的重排序
private volatile boolean flag = false;
// 线程1
flag = true; // 修改volatile变量
// 线程2
if (flag) { // 能立即看到修改
// 执行
}
3.1.2 volatile能保证原子性吗?
不能。 volatile只能保证可见性和有序性,不能保证原子性。
private volatile int count = 0;
public void increment() {
count++; // 不是原子操作
// 分为:读取count、加1、写入count
// volatile只能保证读取和写入的可见性,不能保证整个操作的原子性
}
如果需要原子性,应该使用:
- synchronized
- Lock
- 原子类(AtomicInteger等)
3.1.3 volatile的使用场景?
场景1:状态标志
private volatile boolean running = true;
public void stop() {
running = false; // 一个线程修改
}
public void run() {
while (running) { // 另一个线程读取
// 执行任务
}
}
场景2:双重检查锁定(DCL)
public class Singleton {
private static volatile Singleton instance; // 必须volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile防止重排序
}
}
}
return instance;
}
}
3.2 原理问题
3.2.1 volatile的实现原理?
volatile通过内存屏障(Memory Barrier)来实现。
内存屏障的作用:
- Load Barrier:确保读操作完成
- Store Barrier:确保写操作完成
- Full Barrier:确保所有操作完成
private volatile int value = 0;
public void write() {
value = 1; // Store Barrier:确保写操作完成,刷新到主内存
}
public int read() {
return value; // Load Barrier:确保从主内存读取最新值
}
3.2.2 什么是内存屏障?
内存屏障是CPU指令,用于控制内存操作的顺序和可见性。
内存屏障的类型:
- Load Barrier:读屏障,确保读操作完成
- Store Barrier:写屏障,确保写操作完成
- Full Barrier:全屏障,确保所有操作完成
volatile的内存语义:
- 写操作:在写操作后插入Store Barrier
- 读操作:在读操作前插入Load Barrier
3.2.3 happens-before规则?
happens-before规则定义了操作之间的可见性关系。
主要规则:
- 程序顺序规则:同一线程中,前面的操作happens-before后面的操作
- volatile规则:volatile写happens-before volatile读
- 传递性规则:如果A happens-before B,B happens-before C,则A happens-before C
// volatile规则
private volatile int x = 0;
private int y = 0;
// 线程1
x = 1; // volatile写
y = 2; // 普通写
// 线程2
if (x == 1) { // volatile读
// 能保证看到y=2(因为x=1 happens-before x的读)
}
3.2.4 volatile如何保证可见性?
volatile通过内存屏障保证可见性。
写操作:
private volatile int value = 0;
value = 1; // 写操作
// Store Barrier:确保写操作完成,立即刷新到主内存
读操作:
int result = value; // 读操作
// Load Barrier:确保从主内存读取最新值,不使用缓存
工作原理:
- 写操作后插入Store Barrier,立即刷新到主内存
- 读操作前插入Load Barrier,从主内存读取最新值
- 其他线程的缓存失效,强制从主内存读取
3.2.5 volatile如何禁止重排序?
volatile通过内存屏障禁止重排序。
private volatile int x = 0;
private int y = 0;
// 可能的执行顺序(没有volatile)
y = 1; // 可能被重排序到x=1之后
x = 1;
// 有volatile保证顺序
x = 1; // volatile写,后面的操作不能重排序到前面
y = 1; // 普通写,不能重排序到x=1之前
3.3 对比问题
3.3.1 volatile和synchronized的区别?
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 阻塞 | 不阻塞 | 会阻塞 |
| 粒度 | 变量 | 代码块或方法 |
3.3.2 什么时候使用volatile?
适合使用volatile的场景:
- 状态标志:简单的布尔标志
- 双重检查锁定:单例模式
- 独立观察:变量的写操作不依赖于当前值
不适合使用volatile的场景:
- 复合操作:如i++(需要原子性)
- 依赖当前值:如count = count + 1
4. CAS面试题
4.1 基础问题
4.1.1 什么是CAS?
CAS(Compare-And-Swap)是原子操作,用于实现无锁编程。
CAS操作:
// CAS的伪代码
boolean compareAndSwap(内存地址, 期望值, 新值) {
if (内存地址的值 == 期望值) {
内存地址的值 = 新值;
return true;
} else {
return false;
}
}
Java中的CAS:
AtomicInteger count = new AtomicInteger(0);
// CAS操作:如果当前值是0,则更新为1
boolean success = count.compareAndSet(0, 1);
4.1.2 CAS的原理是什么?
CAS通过CPU的原子指令实现,保证操作的原子性。
工作流程:
- 读取内存中的当前值
- 比较当前值和期望值
- 如果相等,更新为新值
- 如果不相等,返回false
底层实现:
- 使用CPU的
LOCK CMPXCHG指令 - 保证操作的原子性
4.1.3 CAS的优缺点?
优点:
- 无锁:不需要加锁,性能好
- 无阻塞:不会导致线程阻塞
- 可扩展性好:不会因为锁竞争导致性能下降
缺点:
- ABA问题:值从A变成B再变成A,CAS认为没有变化
- 自旋开销:如果CAS失败,会一直重试,消耗CPU
- 只能保证一个变量:不能保证多个变量的原子性
4.2 ABA问题
4.2.1 什么是ABA问题?
ABA问题是指值从A变成B再变成A,CAS认为没有变化,但实际上已经变化了。
示例:
AtomicInteger count = new AtomicInteger(10);
// 线程1:期望值是10,新值是20
count.compareAndSet(10, 20);
// 线程2:先将10变成30,再变成10
count.set(30);
count.set(10);
// 线程1的CAS会成功,但实际上值已经被修改过了
4.2.2 如何解决ABA问题?
使用版本号机制:
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10, 0);
// 获取值和版本号
int[] stamp = new int[1];
int value = ref.get(stamp);
// 更新时检查版本号
ref.compareAndSet(10, 20, stamp[0], stamp[0] + 1);
使用AtomicStampedReference:
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
// 更新:值从A变成B,版本号从0变成1
ref.compareAndSet("A", "B", 0, 1);
// 即使值又变回A,版本号不同,CAS也会失败
4.2.3 AtomicStampedReference的作用?
AtomicStampedReference通过版本号解决ABA问题。
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
// 获取值和版本号
int[] stamp = new int[1];
String value = ref.get(stamp); // value="A", stamp[0]=0
// 更新:检查值和版本号
ref.compareAndSet("A", "B", 0, 1); // 成功:值=A,版本号=0
ref.compareAndSet("B", "A", 1, 2); // 值又变回A,但版本号=2
ref.compareAndSet("A", "C", 0, 1); // 失败:版本号不匹配(当前是2,期望是0)
4.3 实现问题
4.3.1 CAS的底层实现?
CAS通过Unsafe类调用CPU的原子指令实现。
// Unsafe类的CAS方法
public final native boolean compareAndSwapInt(
Object obj, // 对象
long offset, // 字段偏移量
int expect, // 期望值
int update // 新值
);
底层实现:
- 使用CPU的
LOCK CMPXCHG指令 - 保证操作的原子性
4.3.2 Unsafe类的作用?
Unsafe类提供了直接操作内存的能力,包括CAS操作。
主要功能:
- CAS操作:compareAndSwapInt/Long/Object
- 内存操作:直接分配内存、释放内存
- 对象操作:获取字段偏移量、设置字段值
注意: Unsafe类是内部API,不建议直接使用。
5. AQS面试题
5.1 基础问题
5.1.1 什么是AQS?
AQS(AbstractQueuedSynchronizer)是Java并发包的基础框架,用于实现锁和同步器。
AQS的作用:
- 提供了实现锁和同步器的基础框架
- 许多并发工具类都基于AQS实现
基于AQS实现的类:
- ReentrantLock
- ReentrantReadWriteLock
- Semaphore
- CountDownLatch
- CyclicBarrier
5.1.2 AQS的核心思想?
AQS使用一个volatile的int变量(state)表示同步状态,通过CLH队列管理等待线程。
核心组件:
- state变量:表示同步状态
- CLH队列:管理等待线程的队列
- CAS操作:原子地更新state
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态
private Node head; // 队列头
private Node tail; // 队列尾
}
5.1.3 AQS的实现原理?
1. state变量
- 表示同步状态
- 不同的同步器有不同的含义
2. CLH队列
- 双向链表,管理等待线程
- 每个节点代表一个等待的线程
3. 获取锁的流程
// 尝试获取锁(由子类实现)
protected boolean tryAcquire(int arg) {
// 子类实现
}
// 获取锁(AQS实现)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
5.2 实现问题
5.2.1 CLH队列是什么?
CLH队列是AQS中管理等待线程的队列,是一个双向链表。
队列结构:
head -> node1 -> node2 -> node3 -> tail
节点结构:
static final class Node {
volatile int waitStatus; // 等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待的线程
}
5.2.2 state变量的作用?
state变量表示同步状态,不同的同步器有不同的含义。
ReentrantLock:
- state = 0:无锁
- state > 0:有锁,值表示重入次数
Semaphore:
- state表示可用许可证数量
CountDownLatch:
- state表示计数器值
5.2.3 独占模式和共享模式的区别?
独占模式(EXCLUSIVE):
- 同一时刻只有一个线程能获取锁
- 如ReentrantLock
共享模式(SHARED):
- 同一时刻可以有多个线程获取锁
- 如Semaphore、CountDownLatch
5.3 应用问题
5.3.1 哪些类基于AQS实现?
- ReentrantLock:可重入锁
- ReentrantReadWriteLock:读写锁
- Semaphore:信号量
- CountDownLatch:倒计时门闩
- CyclicBarrier:循环屏障
5.3.2 如何基于AQS实现自定义锁?
public class MyLock {
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) { // 如果state=0,设置为1
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
6. Lock面试题
6.1 ReentrantLock
6.1.1 ReentrantLock和synchronized的区别?
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可中断 | 支持 | 不支持 |
| 公平锁 | 支持 | 不支持 |
| 超时 | 支持 | 不支持 |
| 条件变量 | 支持多个 | 只支持一个 |
| 性能 | JDK 1.6后差不多 | JDK 1.6后差不多 |
// ReentrantLock:可中断、可超时
ReentrantLock lock = new ReentrantLock();
try {
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
// 获取锁成功
}
} catch (InterruptedException e) {
// 被中断
}
// synchronized:不可中断
synchronized (lock) {
// 无法中断
}
6.1.2 什么是可重入锁?
可重入锁是指同一个线程可以多次获取同一把锁。
ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
method2(); // 调用method2,可以再次获取锁
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock(); // 同一个线程,可以再次获取锁
try {
// 代码
} finally {
lock.unlock();
}
}
6.1.3 公平锁和非公平锁的区别?
公平锁:
- 按照线程等待的顺序获取锁
- 先等待的线程先获取锁
- 性能较差
非公平锁:
- 不按照等待顺序,可能后到的线程先获取锁
- 性能较好(默认)
// 公平锁
ReentrantLock lock = new ReentrantLock(true);
// 非公平锁(默认)
ReentrantLock lock = new ReentrantLock(false);
6.1.4 如何选择公平锁和非公平锁?
选择非公平锁(默认):
- 性能更好
- 大多数场景使用
选择公平锁:
- 需要保证公平性
- 对性能要求不高
6.1.5 ReentrantLock的实现原理?
ReentrantLock基于AQS实现,通过state变量表示锁的状态。
获取锁:
// 非公平锁
final void lock() {
if (compareAndSetState(0, 1)) { // 尝试直接获取
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1); // 获取失败,加入队列
}
}
释放锁:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
6.2 ReadWriteLock
6.2.1 读写锁的作用?
读写锁允许多个线程同时读取,但写操作是独占的。
ReadWriteLock lock = new ReentrantReadWriteLock();
// 读操作:多个线程可以并发
lock.readLock().lock();
try {
// 读取数据
} finally {
lock.readLock().unlock();
}
// 写操作:独占
lock.writeLock().lock();
try {
// 写入数据
} finally {
lock.writeLock().unlock();
}
6.2.2 读锁和写锁的关系?
- 读锁和读锁:可以并发(共享)
- 读锁和写锁:互斥
- 写锁和写锁:互斥
// 多个线程可以同时获取读锁
lock.readLock().lock(); // 线程1
lock.readLock().lock(); // 线程2,可以并发
// 写锁是独占的
lock.writeLock().lock(); // 线程1
lock.writeLock().lock(); // 线程2,必须等待
6.2.3 什么是锁降级?
锁降级是指先获取写锁,再获取读锁,然后释放写锁,保留读锁。
ReadWriteLock lock = new ReentrantReadWriteLock();
// 锁降级
lock.writeLock().lock(); // 获取写锁
try {
// 修改数据
data = newData;
// 降级为读锁
lock.readLock().lock();
} finally {
lock.writeLock().unlock(); // 释放写锁
}
// 继续持有读锁
try {
// 读取数据
return data;
} finally {
lock.readLock().unlock();
}
6.2.4 为什么不能锁升级?
锁升级是指先获取读锁,再获取写锁。这会导致死锁。
// 锁升级(会导致死锁)
lock.readLock().lock(); // 线程1获取读锁
// 线程2也获取读锁
lock.readLock().lock();
// 线程1尝试获取写锁(需要等待所有读锁释放)
lock.writeLock().lock(); // 阻塞,等待线程2释放读锁
// 但线程2也在等待,导致死锁
6.3 Condition
6.3.1 Condition的作用?
Condition用于替代wait/notify,提供更灵活的线程等待和唤醒机制。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待
lock.lock();
try {
condition.await(); // 等待
} finally {
lock.unlock();
}
// 唤醒
lock.lock();
try {
condition.signal(); // 唤醒一个
// condition.signalAll(); // 唤醒所有
} finally {
lock.unlock();
}
6.3.2 Condition和wait/notify的区别?
| 特性 | Condition | wait/notify |
|---|---|---|
| 锁类型 | 需要Lock | 需要synchronized |
| 多个条件 | 支持 | 不支持 |
| 可中断 | 支持 | 支持 |
| 超时 | 支持 | 支持 |
// Condition:可以有多个条件
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// wait/notify:只有一个条件
synchronized (lock) {
lock.wait(); // 只有一个等待条件
}
7. 线程池面试题
7.1 基础问题
7.1.1 为什么使用线程池?
原因:
- 降低资源消耗:复用线程,减少创建和销毁的开销
- 提高响应速度:任务到达时,线程已经准备好
- 提高可管理性:可以统一管理、监控和调优
- 控制并发数量:防止创建过多线程,耗尽系统资源
7.1.2 线程池的核心参数有哪些?
7个核心参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:线程存活时间
- unit:时间单位
- workQueue:工作队列
- threadFactory:线程工厂
- handler:拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, TimeUnit.SECONDS, // 线程存活时间
new LinkedBlockingQueue<>(100), // 工作队列
new CustomThreadFactory(), // 线程工厂
new AbortPolicy() // 拒绝策略
);
7.1.3 线程池的执行流程?
提交任务
↓
当前线程数 < 核心线程数?
├─ 是 → 创建新线程执行任务
└─ 否 → 工作队列未满?
├─ 是 → 将任务加入队列
└─ 否 → 当前线程数 < 最大线程数?
├─ 是 → 创建新线程执行任务
└─ 否 → 执行拒绝策略
7.1.4 线程池的拒绝策略有哪些?
1. AbortPolicy(默认)
- 直接抛出异常
2. CallerRunsPolicy
- 由调用线程执行任务
3. DiscardPolicy
- 直接丢弃任务
4. DiscardOldestPolicy
- 丢弃队列中最老的任务
7.1.5 如何合理配置线程池参数?
CPU密集型:
int threadCount = Runtime.getRuntime().availableProcessors() + 1;
IO密集型:
int threadCount = Runtime.getRuntime().availableProcessors() * 2;
7.2 原理问题
7.2.1 ThreadPoolExecutor的实现原理?
ThreadPoolExecutor通过Worker类封装工作线程,通过队列管理任务。
核心组件:
- Worker:封装工作线程
- workQueue:工作队列
- ctl:状态和线程数的组合
7.2.2 线程池的状态有哪些?
5种状态:
- RUNNING:运行中,接受新任务
- SHUTDOWN:关闭,不接受新任务,但处理队列任务
- STOP:停止,不接受新任务,不处理队列任务
- TIDYING:整理,所有任务已终止
- TERMINATED:终止
7.2.3 线程是如何复用的?
线程通过循环从队列中获取任务并执行,实现复用。
// runWorker方法的核心循环
while (task != null || (task = getTask()) != null) {
task.run(); // 执行任务
task = null; // 清空任务
}
// 一个线程可以执行多个任务
7.3 实践问题
7.3.1 为什么不推荐使用Executors?
Executors提供的预定义线程池参数设置不合理:
- newFixedThreadPool:使用无界队列,可能导致OOM
- newCachedThreadPool:最大线程数无限制,可能导致创建过多线程
推荐: 手动创建ThreadPoolExecutor,根据实际场景设置参数。
7.3.2 如何优雅关闭线程池?
executor.shutdown(); // 停止接受新任务
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 强制关闭
}
7.3.3 CPU密集型和IO密集型任务如何设置线程数?
CPU密集型: 线程数 = CPU核心数 + 1 IO密集型: 线程数 = CPU核心数 * 2
7.3.4 线程池中线程异常了怎么办?
问题: 线程池中的任务如果抛出异常,默认会被吞掉。
解决方案:
// 方式1:在任务内部捕获异常
executor.execute(() -> {
try {
// 执行任务
} catch (Exception e) {
// 处理异常
System.err.println("任务异常: " + e.getMessage());
}
});
// 方式2:使用Future获取异常
Future<?> future = executor.submit(() -> {
throw new RuntimeException("异常");
});
try {
future.get(); // 会抛出ExecutionException
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 获取原始异常
System.err.println("任务异常: " + cause.getMessage());
}
7.3.5 如何监控线程池的状态?
ThreadPoolExecutor executor = ...;
// 获取线程池状态
int poolSize = executor.getPoolSize(); // 当前线程数
int activeCount = executor.getActiveCount(); // 活跃线程数
long completedTaskCount = executor.getCompletedTaskCount(); // 已完成任务数
int queueSize = executor.getQueue().size(); // 队列大小
// 判断线程池是否健康
if (queueSize > 1000) {
System.out.println("警告:队列积压严重");
}
if (activeCount == poolSize && queueSize > 0) {
System.out.println("警告:所有线程都在工作,队列有积压");
}
7.3.6 线程池的submit()和execute()的区别?
execute()方法:
- 提交Runnable任务
- 没有返回值
- 异常会被吞掉
submit()方法:
- 可以提交Runnable或Callable任务
- 返回Future对象
- 异常会被包装在Future中
// execute():没有返回值
executor.execute(() -> {
System.out.println("执行");
});
// submit():返回Future
Future<?> future = executor.submit(() -> {
System.out.println("执行");
});
// submit():提交Callable,有返回值
Future<String> future2 = executor.submit(() -> {
return "结果";
});
String result = future2.get();
7.3.7 线程池的shutdown()和shutdownNow()的区别?
shutdown()方法:
- 停止接受新任务
- 等待已提交的任务执行完成
- 不会中断正在执行的任务
shutdownNow()方法:
- 停止接受新任务
- 尝试停止所有正在执行的任务
- 返回等待执行的任务列表
// shutdown():优雅关闭
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
// shutdownNow():强制关闭
List<Runnable> pendingTasks = executor.shutdownNow();
System.out.println("未执行的任务数: " + pendingTasks.size());
7.3.8 线程池中的线程是如何复用的?
线程通过循环从队列中获取任务并执行,实现复用。
工作流程:
- 线程启动后,执行runWorker()方法
- 循环调用getTask()从队列获取任务
- 执行任务:task.run()
- 继续循环,获取下一个任务
- 如果getTask()返回null,线程退出
// runWorker()的核心逻辑
while (task != null || (task = getTask()) != null) {
task.run(); // 执行任务
task = null; // 清空任务,继续获取下一个
}
// 一个线程可以执行多个任务,这就是线程复用
7.3.9 线程池的拒绝策略如何选择?
1. AbortPolicy(默认)
- 直接抛出异常
- 适合:需要知道任务被拒绝的场景
2. CallerRunsPolicy
- 由调用线程执行任务
- 适合:任务执行速度快的场景
3. DiscardPolicy
- 直接丢弃任务
- 适合:可以容忍任务丢失的场景
4. DiscardOldestPolicy
- 丢弃队列中最老的任务
- 适合:新任务比老任务重要的场景
// 根据业务场景选择拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy() // 由调用线程执行
);
7.3.10 如何动态调整线程池参数?
ThreadPoolExecutor executor = ...;
// 动态调整核心线程数
executor.setCorePoolSize(10);
// 动态调整最大线程数
executor.setMaximumPoolSize(20);
// 允许核心线程超时
executor.allowCoreThreadTimeOut(true);
7.3.11 线程池中的任务执行顺序?
execute()方法:
- 先创建核心线程执行
- 核心线程满了,任务加入队列
- 队列满了,创建非核心线程执行
- 非核心线程也满了,执行拒绝策略
注意: 队列中的任务不一定按提交顺序执行,取决于队列类型。
7.3.12 线程池的keepAliveTime是什么?
keepAliveTime是非核心线程的空闲存活时间。
工作原理:
- 当线程数超过核心线程数时,多余的空闲线程会等待新任务
- 如果超过keepAliveTime时间还没有任务,线程会被回收
- 核心线程默认不会超时(除非设置allowCoreThreadTimeOut=true)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS, // keepAliveTime=60秒
new LinkedBlockingQueue<>(100)
);
// 允许核心线程超时
executor.allowCoreThreadTimeOut(true);
7.3.13 线程池的工作队列满了会怎样?
情况1:当前线程数 < 最大线程数
- 创建新线程执行任务
情况2:当前线程数 >= 最大线程数
- 执行拒绝策略
// 示例:核心线程数=5,最大线程数=10,队列容量=10
// 如果同时提交20个任务:
// - 5个任务由核心线程执行
// - 10个任务加入队列
// - 5个任务由非核心线程执行(创建5个新线程)
// 如果提交第21个任务,会执行拒绝策略
7.3.14 如何实现线程池的优雅关闭?
public void shutdownGracefully(ThreadPoolExecutor executor) {
// 1. 停止接受新任务
executor.shutdown();
try {
// 2. 等待已提交的任务完成
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 3. 超时,强制关闭
executor.shutdownNow();
// 4. 再等待30秒
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
7.3.15 线程池的线程命名有什么作用?
线程命名有助于调试和监控。
// 使用自定义线程工厂
ThreadFactory factory = new ThreadFactory() {
private int number = 1;
public Thread newThread(Runnable r) {
return new Thread(r, "MyPool-" + number++);
}
};
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
factory // 使用自定义线程工厂
);
// 线程名称:MyPool-1, MyPool-2...
// 在jstack中可以看到有意义的线程名称,方便排查问题
7.3.16 线程池的addWorker()方法做了什么?
addWorker()方法用于创建新的工作线程。
主要步骤:
- 检查线程池状态和线程数
- 创建Worker对象
- 将Worker加入workers集合
- 启动Worker中的线程
// addWorker()的简化流程
private boolean addWorker(Runnable firstTask, boolean core) {
// 1. 检查是否可以创建线程
// 2. 创建Worker对象
Worker w = new Worker(firstTask);
Thread t = w.thread;
// 3. 加入workers集合
workers.add(w);
// 4. 启动线程
t.start();
return true;
}
7.3.17 线程池的ctl变量是什么?
ctl是一个原子整型变量,同时表示线程池状态和线程数量。
结构:
- 高3位:线程池状态(RUNNING、SHUTDOWN等)
- 低29位:线程数量
// ctl = (runState << 29) | workerCount
// 高3位:状态
// 低29位:线程数
// 获取状态
int state = ctl & ~CAPACITY;
// 获取线程数
int count = ctl & CAPACITY;
优势: 使用一个变量同时表示状态和数量,保证原子性。
7.3.18 线程池的prestartAllCoreThreads()方法有什么用?
prestartAllCoreThreads()方法预启动所有核心线程。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
// 预启动所有核心线程(5个)
executor.prestartAllCoreThreads();
// 线程池创建后,立即创建5个核心线程,而不是等到任务来了才创建
适用场景: 需要快速响应的场景,可以提前创建线程。
7.3.19 线程池的allowCoreThreadTimeOut()方法有什么用?
allowCoreThreadTimeOut()方法允许核心线程超时。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
// 允许核心线程超时
executor.allowCoreThreadTimeOut(true);
// 核心线程空闲60秒后也会被回收
适用场景: 任务到达不规律,需要节省资源的场景。
7.3.20 线程池的getTask()方法如何工作?
getTask()方法从工作队列中获取任务。
核心逻辑:
- 核心线程:使用take()方法,一直阻塞等待
- 非核心线程:使用poll()方法,超时后返回null
private Runnable getTask() {
boolean timed = allowCoreThreadTimeOut || workerCount > corePoolSize;
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : // 超时等待
workQueue.take(); // 一直等待
return r;
}
7.3.21 线程池的processWorkerExit()方法有什么用?
processWorkerExit()方法处理工作线程退出。
主要工作:
- 从workers集合中移除Worker
- 尝试终止线程池(如果所有线程都退出了)
- 可能需要创建新线程(如果线程数不足)
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 1. 从workers集合中移除
workers.remove(w);
// 2. 如果线程数不足,可能需要创建新线程
if (workerCount < corePoolSize) {
addWorker(null, false);
}
}
7.3.22 线程池的beforeExecute()和afterExecute()方法有什么用?
这两个方法可以重写,用于监控和日志记录。
public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("线程 " + t.getName() + " 开始执行任务");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) {
System.err.println("任务执行异常: " + t.getMessage());
} else {
System.out.println("任务执行完成");
}
}
}
7.3.23 线程池的线程是如何被回收的?
非核心线程:
- 空闲时间超过keepAliveTime
- getTask()返回null
- runWorker()退出循环
- processWorkerExit()处理退出
核心线程:
- 默认不会被回收
- 如果设置了allowCoreThreadTimeOut(true),也会被回收
7.3.24 线程池的队列满了,线程数也达到最大值,会怎样?
会执行拒绝策略。
// 示例:核心线程数=5,最大线程数=10,队列容量=10
// 如果同时提交25个任务:
// - 5个任务由核心线程执行
// - 10个任务加入队列
// - 5个任务由非核心线程执行
// - 剩余5个任务执行拒绝策略
7.3.25 如何自定义线程池的拒绝策略?
public class CustomRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志
System.err.println("任务被拒绝: " + r.toString());
// 可以保存到数据库,稍后重试
// saveTaskToDatabase(r);
}
}
// 使用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
new CustomRejectedHandler()
);
7.3.26 线程池的线程数如何动态调整?
ThreadPoolExecutor executor = ...;
// 根据系统负载动态调整
if (executor.getQueue().size() > 1000) {
// 队列积压严重,增加核心线程数
executor.setCorePoolSize(executor.getCorePoolSize() + 5);
executor.setMaximumPoolSize(executor.getMaximumPoolSize() + 10);
} else if (executor.getQueue().size() < 100) {
// 队列空闲,减少线程数
executor.setCorePoolSize(Math.max(5, executor.getCorePoolSize() - 2));
}
7.3.27 线程池的线程是如何创建的?
线程通过ThreadFactory创建。
默认ThreadFactory:
// Executors.defaultThreadFactory()
Thread thread = new Thread(r, "pool-" + poolNumber + "-thread-" + threadNumber);
自定义ThreadFactory:
ThreadFactory factory = r -> {
Thread t = new Thread(r, "MyPool-" + number++);
t.setDaemon(false);
t.setPriority(Thread.NORM_PRIORITY);
return t;
};
7.3.28 线程池的线程什么时候会被创建?
创建时机:
- 提交任务时,如果线程数 < 核心线程数,创建核心线程
- 队列满了,如果线程数 < 最大线程数,创建非核心线程
- 调用prestartAllCoreThreads(),预启动所有核心线程
// 预启动所有核心线程
executor.prestartAllCoreThreads();
// 立即创建5个核心线程,而不是等到任务来了才创建
7.3.29 线程池的线程什么时候会被回收?
非核心线程:
- 空闲时间超过keepAliveTime
- getTask()返回null(超时)
- runWorker()退出循环
- 线程结束
核心线程:
- 默认不会被回收
- 如果allowCoreThreadTimeOut=true,也会被回收
7.3.30 线程池的队列选择有什么讲究?
ArrayBlockingQueue:
- 有界队列,容量固定
- 适合任务数量可预估的场景
LinkedBlockingQueue:
- 可以是有界或无界
- 吞吐量通常更高
SynchronousQueue:
- 不存储元素
- 适合任务处理速度快的场景
PriorityBlockingQueue:
- 按优先级排序
- 适合需要优先处理的场景
7.3.31 线程池的线程名称有什么作用?
线程名称有助于:
- 调试:在日志中识别线程
- 监控:在jstack中查看线程状态
- 排查问题:快速定位问题线程
// 使用有意义的线程名称
ThreadFactory factory = r -> new Thread(r, "订单处理-" + number++);
// 在jstack中可以看到:订单处理-1, 订单处理-2...
7.3.32 线程池的异常处理最佳实践?
// 方式1:在任务内部捕获(推荐)
executor.execute(() -> {
try {
processTask();
} catch (Exception e) {
logger.error("任务执行异常", e);
// 发送告警
}
});
// 方式2:使用Future获取异常
Future<?> future = executor.submit(() -> {
throw new RuntimeException("异常");
});
try {
future.get();
} catch (ExecutionException e) {
logger.error("任务异常", e.getCause());
}
7.3.33 线程池的监控指标有哪些?
关键指标:
- 当前线程数(poolSize)
- 活跃线程数(activeCount)
- 已完成任务数(completedTaskCount)
- 总任务数(taskCount)
- 队列大小(queue.size())
- 队列剩余容量(queue.remainingCapacity())
// 监控线程池
System.out.println("当前线程数: " + executor.getPoolSize());
System.out.println("活跃线程数: " + executor.getActiveCount());
System.out.println("队列大小: " + executor.getQueue().size());
System.out.println("已完成任务: " + executor.getCompletedTaskCount());
7.3.34 线程池的线程复用原理?
线程通过循环执行任务实现复用。
核心代码:
// runWorker()方法
while (task != null || (task = getTask()) != null) {
task.run(); // 执行任务
task = null; // 清空,继续获取下一个任务
}
// 一个线程可以执行多个任务,这就是线程复用
优势: 避免频繁创建和销毁线程,提高性能。
7.3.35 线程池的线程是如何退出的?
退出条件:
- getTask()返回null(队列为空且超时)
- 线程池状态变为SHUTDOWN或STOP
- 发生异常
退出流程:
// getTask()返回null
// → runWorker()退出循环
// → processWorkerExit()处理退出
// → 从workers集合中移除Worker
// → 线程结束
7.3.36 线程池的线程是如何被调度的?
线程池中的线程由操作系统调度,线程池只负责:
- 创建线程
- 分配任务
- 管理线程生命周期
线程调度:
- 由操作系统的线程调度器负责
- JVM无法控制线程的调度
- 只能通过优先级给操作系统提示
7.3.37 线程池的线程数设置公式?
通用公式:
线程数 = CPU核心数 * (1 + IO等待时间 / CPU计算时间)
简化公式:
- CPU密集型:线程数 = CPU核心数 + 1
- IO密集型:线程数 = CPU核心数 * 2
int cpuCount = Runtime.getRuntime().availableProcessors();
// CPU密集型
int threadCount = cpuCount + 1;
// IO密集型
int threadCount = cpuCount * 2;
7.3.38 线程池的线程如何避免OOM?
避免OOM的方法:
- 使用有界队列,不要使用无界队列
- 合理设置最大线程数
- 使用合适的拒绝策略
- 及时关闭不用的线程池
// 不好的做法:无界队列,可能导致OOM
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>() // 无界队列,危险!
);
// 好的做法:有界队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100) // 有界队列
);
7.3.39 线程池的线程如何实现任务优先级?
方式1:使用PriorityBlockingQueue
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new PriorityBlockingQueue<>(100, (r1, r2) -> {
Task t1 = (Task) r1;
Task t2 = (Task) r2;
return t2.getPriority() - t1.getPriority(); // 优先级高的先执行
})
);
方式2:使用多个线程池
// 高优先级任务池
ThreadPoolExecutor highPriorityPool = new ThreadPoolExecutor(...);
// 低优先级任务池
ThreadPoolExecutor lowPriorityPool = new ThreadPoolExecutor(...);
7.3.40 线程池的线程如何实现任务超时?
使用Future.get()设置超时:
Future<?> future = executor.submit(() -> {
// 执行任务
});
try {
future.get(30, TimeUnit.SECONDS); // 30秒超时
} catch (TimeoutException e) {
future.cancel(true); // 取消任务
System.out.println("任务超时,已取消");
}
7.3.41 线程池的线程如何实现任务重试?
public void executeWithRetry(Runnable task, int maxRetries) {
executor.execute(() -> {
int retries = 0;
while (retries < maxRetries) {
try {
task.run();
break; // 成功,退出循环
} catch (Exception e) {
retries++;
if (retries >= maxRetries) {
System.err.println("重试失败: " + e.getMessage());
} else {
try {
Thread.sleep(1000); // 等待1秒后重试
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
});
}
7.3.42 线程池的线程如何实现任务去重?
// 使用Set记录已执行的任务
private Set<String> executedTasks = ConcurrentHashMap.newKeySet();
public void executeOnce(String taskId, Runnable task) {
if (executedTasks.add(taskId)) { // 如果添加成功,说明未执行过
executor.execute(task);
} else {
System.out.println("任务 " + taskId + " 已执行过,跳过");
}
}
7.3.43 线程池的线程如何实现任务依赖?
// 使用CompletableFuture实现任务依赖
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
return "结果1";
}, executor);
CompletableFuture<String> future2 = future1.thenApply(result1 -> {
// 依赖future1的结果
return "结果2";
});
String result = future2.get();
7.3.44 线程池的线程如何实现任务链式执行?
// 使用CompletableFuture实现链式执行
CompletableFuture.supplyAsync(() -> "步骤1", executor)
.thenApply(result -> result + " -> 步骤2")
.thenApply(result -> result + " -> 步骤3")
.thenAccept(result -> System.out.println(result));
7.3.45 线程池的线程如何实现任务并行执行?
// 使用CompletableFuture实现并行执行
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "任务1", executor);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "任务2", executor);
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "任务3", executor);
// 等待所有任务完成
CompletableFuture.allOf(future1, future2, future3).join();
7.3.46 线程池的线程如何实现任务结果聚合?
// 使用CompletableFuture聚合结果
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "结果";
}, executor);
futures.add(future);
}
// 等待所有任务完成并聚合结果
List<String> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
7.3.47 线程池的线程如何实现任务限流?
// 使用Semaphore实现限流
Semaphore semaphore = new Semaphore(10); // 最多10个任务同时执行
executor.execute(() -> {
try {
semaphore.acquire(); // 获取许可
try {
// 执行任务
} finally {
semaphore.release(); // 释放许可
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
7.3.48 线程池的线程如何实现任务超时取消?
Future<?> future = executor.submit(() -> {
// 长时间运行的任务
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
});
try {
future.get(30, TimeUnit.SECONDS); // 30秒超时
} catch (TimeoutException e) {
future.cancel(true); // 取消任务(中断线程)
System.out.println("任务超时,已取消");
}
7.3.49 线程池的线程如何实现任务结果缓存?
// 使用ConcurrentHashMap缓存结果
private ConcurrentHashMap<String, Future<String>> cache = new ConcurrentHashMap<>();
public String executeWithCache(String key, Callable<String> task) {
return cache.computeIfAbsent(key, k -> {
return executor.submit(task);
}).get();
}
7.3.50 线程池的线程如何实现任务批量执行?
// 批量提交任务
List<Callable<String>> tasks = new ArrayList<>();
for (int i = 0; i < 100; i++) {
final int index = i;
tasks.add(() -> "任务" + index);
}
// 执行所有任务
List<Future<String>> futures = executor.invokeAll(tasks);
// 获取结果
for (Future<String> future : futures) {
String result = future.get();
System.out.println(result);
}
8. 并发集合面试题
8.1 ConcurrentHashMap
8.1.1 ConcurrentHashMap和HashMap的区别?
| 特性 | HashMap | ConcurrentHashMap |
|---|---|---|
| 线程安全 | 不安全 | 安全 |
| 性能 | 好 | 较好 |
| null值 | 允许 | 不允许 |
| 迭代器 | 快速失败 | 弱一致性 |
8.1.2 ConcurrentHashMap的实现原理?
JDK 1.8:
- 使用CAS + synchronized
- 每个桶(数组元素)可以独立加锁
- 锁粒度小,性能好
// put操作
if (桶为空) {
使用CAS插入 // 无锁
} else {
synchronized (链表头节点) { // 只锁一个桶
// 插入或更新
}
}
8.1.3 JDK 1.7和JDK 1.8的实现区别?
JDK 1.7:
- 使用分段锁(Segment)
- 锁粒度:段级别
JDK 1.8:
- 使用CAS + synchronized
- 锁粒度:桶级别(更细)
8.1.4 ConcurrentHashMap如何保证线程安全?
方式1:CAS操作
- 桶为空时,使用CAS无锁插入
方式2:synchronized锁
- 桶不为空时,锁住链表头节点
方式3:volatile
- 数组和节点使用volatile,保证可见性
8.1.5 ConcurrentHashMap的size()方法如何实现?
size()方法通过累加各个CounterCell的值来计算,返回的是近似值。
// 使用CounterCell数组统计
long sum = baseCount;
for (CounterCell cell : counterCells) {
sum += cell.value;
}
return sum;
8.2 其他并发集合
8.2.1 CopyOnWriteArrayList的原理?
CopyOnWriteArrayList使用写时复制机制。
读操作: 直接读取,不需要加锁 写操作: 复制整个数组,在副本上修改,然后替换原数组
// 写操作
public boolean add(E e) {
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制数组
newElements[len] = e;
setArray(newElements); // 替换原数组
}
8.2.2 CopyOnWriteArrayList的适用场景?
适合: 读多写少的场景 不适合: 写操作频繁的场景
9. 并发工具类面试题
9.1 CountDownLatch
9.1.1 CountDownLatch的作用?
CountDownLatch用于等待多个线程完成。
CountDownLatch latch = new CountDownLatch(3);
// 线程1、2、3
latch.countDown(); // 计数减1
// 主线程
latch.await(); // 等待计数为0
9.1.2 CountDownLatch和CyclicBarrier的区别?
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 计数 | 只能减,不能重置 | 可以重置,循环使用 |
| 等待 | 一个或多个线程等待 | 多个线程互相等待 |
| 用途 | 等待多个任务完成 | 多个线程到达屏障点 |
9.2 CyclicBarrier
9.2.1 CyclicBarrier的作用?
CyclicBarrier用于多个线程到达屏障点后,一起继续执行。
CyclicBarrier barrier = new CyclicBarrier(3);
// 线程1、2、3
barrier.await(); // 等待其他线程到达
// 所有线程到达后,一起继续执行
9.3 Semaphore
9.3.1 Semaphore的作用?
Semaphore用于控制同时访问资源的线程数量。
Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问
semaphore.acquire(); // 获取许可
try {
// 访问资源
} finally {
semaphore.release(); // 释放许可
}
10. 死锁面试题
10.1 基础问题
10.1.1 什么是死锁?
死锁是指两个或多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。
10.1.2 死锁产生的条件?
4个必要条件:
- 互斥条件:资源不能被多个线程同时使用
- 请求与保持条件:线程持有资源的同时请求其他资源
- 不剥夺条件:线程已获得的资源不能被强制释放
- 循环等待条件:存在一个循环等待链
10.1.3 如何避免死锁?
方法1:使用锁顺序
- 所有线程按照相同的顺序获取锁
方法2:使用超时锁
- 使用tryLock()设置超时
方法3:避免嵌套锁
- 尽量避免在一个锁内获取另一个锁
10.1.4 如何检测死锁?
使用jstack:
jstack <pid>
# 查找死锁信息
使用代码:
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = bean.findDeadlockedThreads();
10.2 实践问题
10.2.1 死锁和活锁的区别?
死锁: 线程被阻塞,无法继续执行 活锁: 线程没有被阻塞,但不断重试,无法继续执行
11. 综合面试题
11.1 设计题
11.1.1 如何实现一个线程安全的单例?
推荐方式:静态内部类
public class Singleton {
private static class Holder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return Holder.instance;
}
}
11.1.2 如何实现一个线程池?
基于ThreadPoolExecutor实现,设置合理的参数。
11.1.3 如何实现一个阻塞队列?
使用Lock和Condition实现。
public class MyBlockingQueue<E> {
private Queue<E> queue = new LinkedList<>();
private int capacity;
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public void put(E e) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await();
}
queue.offer(e);
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
11.2 场景题
11.2.1 如何保证多线程下的数据一致性?
方法:
- 使用synchronized
- 使用Lock
- 使用volatile(简单场景)
- 使用原子类
- 使用不可变对象
11.2.2 如何实现多线程顺序执行?
使用join()方法或CountDownLatch。
Thread t1 = new Thread(() -> System.out.println("1"));
Thread t2 = new Thread(() -> System.out.println("2"));
Thread t3 = new Thread(() -> System.out.println("3"));
t1.start();
t1.join(); // 等待t1完成
t2.start();
t2.join(); // 等待t2完成
t3.start();
11.3 性能题
11.3.1 如何优化多线程性能?
- 减少锁的持有时间
- 减小锁的粒度
- 使用读写锁
- 使用无锁数据结构
- 合理设置线程数
12. 高级面试题
12.1 JMM相关
12.1.1 什么是JMM?
JMM(Java Memory Model)是Java内存模型,定义了线程如何与内存交互。
JMM的作用:
- 屏蔽不同硬件和操作系统的内存访问差异
- 保证Java程序在各种平台上都能正确执行
12.1.2 happens-before规则有哪些?
- 程序顺序规则:同一线程中,前面的操作happens-before后面的操作
- volatile规则:volatile写happens-before volatile读
- 锁规则:解锁happens-before加锁
- 传递性规则:如果A happens-before B,B happens-before C,则A happens-before C
12.1.3 内存可见性如何保证?
通过volatile、synchronized、final等关键字保证。
12.2 无锁编程
12.2.1 什么是无锁编程?
无锁编程是不使用传统锁机制,通过CAS操作实现线程安全。
优势: 性能好,可扩展性好 挑战: 实现复杂,需要处理ABA问题
12.3 性能优化
12.3.1 如何优化锁的性能?
- 减少锁的持有时间
- 减小锁的粒度
- 使用读写锁
- 使用无锁数据结构
13. 核心类源码分析
13.1 Thread源码分析
13.1.1 Thread类的结构
Thread类实现了Runnable接口,封装了线程的创建、启动、状态管理等。
核心方法:
start():启动线程run():线程执行的方法sleep():线程睡眠join():等待线程完成
13.1.2 线程创建流程
Thread thread = new Thread(() -> {
// 任务
});
thread.start(); // 启动线程
// start()方法会调用native方法start0()
// start0()会创建系统线程,然后调用run()方法
13.1.3 线程状态管理
线程有6种状态:
- NEW:新建
- RUNNABLE:可运行
- BLOCKED:阻塞
- WAITING:等待
- TIMED_WAITING:超时等待
- TERMINATED:终止
13.2 ThreadPoolExecutor源码分析
13.2.1 核心数据结构
ctl变量: 同时表示线程池状态和线程数量 workers集合: 存储Worker对象 workQueue: 工作队列
13.2.2 execute()方法源码
public void execute(Runnable command) {
if (workerCountOf(c) < corePoolSize) {
addWorker(command, true); // 创建核心线程
return;
}
if (isRunning(c) && workQueue.offer(command)) {
// 加入队列
} else if (!addWorker(command, false)) {
reject(command); // 拒绝策略
}
}
13.2.3 runWorker()方法源码
final void runWorker(Worker w) {
while (task != null || (task = getTask()) != null) {
task.run(); // 执行任务
task = null;
}
}
13.3 ReentrantLock源码分析
13.3.1 锁的获取流程
// 非公平锁
final void lock() {
if (compareAndSetState(0, 1)) { // 尝试直接获取
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1); // 获取失败,加入队列
}
}
13.3.2 锁的释放流程
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (c == 0) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
return false;
}
13.4 ConcurrentHashMap源码分析
13.4.1 put()方法源码
public V put(K key, V value) {
// 1. 计算hash值
// 2. 如果桶为空,使用CAS插入
// 3. 如果桶不为空,使用synchronized锁住链表头节点
// 4. 插入或更新
}
13.4.2 get()方法源码
public V get(Object key) {
// 1. 计算hash值
// 2. 查找对应的桶
// 3. 遍历链表或红黑树
// 4. 返回找到的值
}
13.5 AQS源码分析
13.5.1 acquire()方法源码
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
13.5.2 release()方法源码
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) {
unparkSuccessor(h); // 唤醒下一个节点
}
return true;
}
return false;
}