Java并发编程面试题

38 阅读50分钟

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从一个线程切换到另一个线程执行。

上下文切换的过程:

  1. 保存当前线程的状态(寄存器、程序计数器等)
  2. 加载新线程的状态
  3. 切换到新线程执行

上下文切换的开销:

  • 需要保存和恢复线程状态
  • 需要刷新CPU缓存
  • 消耗CPU时间

如何减少上下文切换:

  • 减少线程数量
  • 使用线程池复用线程
  • 避免不必要的线程创建

1.1.11 什么是线程安全?

线程安全是指多个线程同时访问同一个对象时,不需要额外的同步机制,也能保证程序的正确性。

线程安全的实现方式:

  1. 不可变对象:对象创建后不能被修改
  2. 同步机制:使用synchronized、Lock等
  3. 无锁编程:使用CAS、原子类等
  4. 线程封闭:每个线程使用独立的对象

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()
所属类ThreadObject
释放锁不释放释放
唤醒方式时间到了自动唤醒需要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的区别?

特性synchronizedvolatile
原子性保证不保证(如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:数组长度(如果是数组)

锁的升级过程:

  1. 无锁偏向锁轻量级锁重量级锁
// 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有两个作用:

  1. 保证可见性:一个线程修改了volatile变量,其他线程能立即看到
  2. 禁止指令重排序:防止编译器优化导致的重排序
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规则定义了操作之间的可见性关系。

主要规则:

  1. 程序顺序规则:同一线程中,前面的操作happens-before后面的操作
  2. volatile规则:volatile写happens-before volatile读
  3. 传递性规则:如果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:确保从主内存读取最新值,不使用缓存

工作原理:

  1. 写操作后插入Store Barrier,立即刷新到主内存
  2. 读操作前插入Load Barrier,从主内存读取最新值
  3. 其他线程的缓存失效,强制从主内存读取

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的区别?

特性volatilesynchronized
原子性不保证保证
可见性保证保证
有序性保证保证
阻塞不阻塞会阻塞
粒度变量代码块或方法

3.3.2 什么时候使用volatile?

适合使用volatile的场景:

  1. 状态标志:简单的布尔标志
  2. 双重检查锁定:单例模式
  3. 独立观察:变量的写操作不依赖于当前值

不适合使用volatile的场景:

  1. 复合操作:如i++(需要原子性)
  2. 依赖当前值:如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的原子指令实现,保证操作的原子性。

工作流程:

  1. 读取内存中的当前值
  2. 比较当前值和期望值
  3. 如果相等,更新为新值
  4. 如果不相等,返回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的区别?

特性ReentrantLocksynchronized
可中断支持不支持
公平锁支持不支持
超时支持不支持
条件变量支持多个只支持一个
性能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的区别?

特性Conditionwait/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 为什么使用线程池?

原因:

  1. 降低资源消耗:复用线程,减少创建和销毁的开销
  2. 提高响应速度:任务到达时,线程已经准备好
  3. 提高可管理性:可以统一管理、监控和调优
  4. 控制并发数量:防止创建过多线程,耗尽系统资源

7.1.2 线程池的核心参数有哪些?

7个核心参数:

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数
  3. keepAliveTime:线程存活时间
  4. unit:时间单位
  5. workQueue:工作队列
  6. threadFactory:线程工厂
  7. 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 线程池中的线程是如何复用的?

线程通过循环从队列中获取任务并执行,实现复用。

工作流程:

  1. 线程启动后,执行runWorker()方法
  2. 循环调用getTask()从队列获取任务
  3. 执行任务:task.run()
  4. 继续循环,获取下一个任务
  5. 如果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()方法用于创建新的工作线程。

主要步骤:

  1. 检查线程池状态和线程数
  2. 创建Worker对象
  3. 将Worker加入workers集合
  4. 启动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()方法处理工作线程退出。

主要工作:

  1. 从workers集合中移除Worker
  2. 尝试终止线程池(如果所有线程都退出了)
  3. 可能需要创建新线程(如果线程数不足)
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 线程池的线程什么时候会被创建?

创建时机:

  1. 提交任务时,如果线程数 < 核心线程数,创建核心线程
  2. 队列满了,如果线程数 < 最大线程数,创建非核心线程
  3. 调用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 线程池的线程是如何退出的?

退出条件:

  1. getTask()返回null(队列为空且超时)
  2. 线程池状态变为SHUTDOWN或STOP
  3. 发生异常

退出流程:

// 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的方法:

  1. 使用有界队列,不要使用无界队列
  2. 合理设置最大线程数
  3. 使用合适的拒绝策略
  4. 及时关闭不用的线程池
// 不好的做法:无界队列,可能导致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的区别?

特性HashMapConcurrentHashMap
线程安全不安全安全
性能较好
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的区别?

特性CountDownLatchCyclicBarrier
计数只能减,不能重置可以重置,循环使用
等待一个或多个线程等待多个线程互相等待
用途等待多个任务完成多个线程到达屏障点

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个必要条件:

  1. 互斥条件:资源不能被多个线程同时使用
  2. 请求与保持条件:线程持有资源的同时请求其他资源
  3. 不剥夺条件:线程已获得的资源不能被强制释放
  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 如何保证多线程下的数据一致性?

方法:

  1. 使用synchronized
  2. 使用Lock
  3. 使用volatile(简单场景)
  4. 使用原子类
  5. 使用不可变对象

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 如何优化多线程性能?

  1. 减少锁的持有时间
  2. 减小锁的粒度
  3. 使用读写锁
  4. 使用无锁数据结构
  5. 合理设置线程数

12. 高级面试题

12.1 JMM相关

12.1.1 什么是JMM?

JMM(Java Memory Model)是Java内存模型,定义了线程如何与内存交互。

JMM的作用:

  • 屏蔽不同硬件和操作系统的内存访问差异
  • 保证Java程序在各种平台上都能正确执行

12.1.2 happens-before规则有哪些?

  1. 程序顺序规则:同一线程中,前面的操作happens-before后面的操作
  2. volatile规则:volatile写happens-before volatile读
  3. 锁规则:解锁happens-before加锁
  4. 传递性规则:如果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 如何优化锁的性能?

  1. 减少锁的持有时间
  2. 减小锁的粒度
  3. 使用读写锁
  4. 使用无锁数据结构

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;
}