Java 面试高频问题指南
# 📚 学习路径
# 面试准备建议
| 阶段 | 重点内容 | 准备时长 |
|---|---|---|
| 基础阶段 | Java 基础语法、集合、异常处理 | 1-2 周 |
| 进阶阶段 | 多线程、JVM、设计模式 | 2-3 周 |
| 高级阶段 | 并发编程、性能优化、分布式 | 3-4 周 |
| 实战阶段 | 项目经验总结、场景题 | 1-2 周 |
# 1️⃣ Java 多线程与并发编程
参考资料:https://www.cnblogs.com/java1024/p/13390538.html (opens new window) | 相关笔记
# 1.1 线程顺序执行问题
面试题:现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?
考察点:线程协调机制、join() 方法的理解
# 解法一:使用 join() 方法
public class ThreadOrderDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("T1 执行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1 执行完毕");
}, "T1");
Thread t2 = new Thread(() -> {
System.out.println("T2 执行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T2 执行完毕");
}, "T2");
Thread t3 = new Thread(() -> {
System.out.println("T3 执行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T3 执行完毕");
}, "T3");
// 启动 T1
t1.start();
// 等待 T1 完成
t1.join();
// 启动 T2
t2.start();
// 等待 T2 完成
t2.join();
// 启动 T3
t3.start();
// 等待 T3 完成
t3.join();
System.out.println("所有线程执行完毕");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
输出:
T1 执行中...
T1 执行完毕
T2 执行中...
T2 执行完毕
T3 执行中...
T3 执行完毕
所有线程执行完毕
2
3
4
5
6
7
# 解法二:使用 CountDownLatch
import java.util.concurrent.CountDownLatch;
public class ThreadOrderWithLatch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
System.out.println("T1 执行");
latch1.countDown(); // T1 完成,释放信号
});
Thread t2 = new Thread(() -> {
try {
latch1.await(); // 等待 T1 完成
System.out.println("T2 执行");
latch2.countDown(); // T2 完成,释放信号
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
latch2.await(); // 等待 T2 完成
System.out.println("T3 执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 解法三:使用 CompletableFuture(推荐)
import java.util.concurrent.CompletableFuture;
public class ThreadOrderWithFuture {
public static void main(String[] args) {
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println("T1 执行");
});
CompletableFuture<Void> future2 = future1.thenRun(() -> {
System.out.println("T2 执行");
});
CompletableFuture<Void> future3 = future2.thenRun(() -> {
System.out.println("T3 执行");
});
// 等待所有任务完成
future3.join();
System.out.println("所有任务完成");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| join() | 简单直观,JDK 自带 | 阻塞主线程,灵活性差 | 简单的顺序执行 |
| CountDownLatch | 灵活,支持多线程协调 | 代码稍复杂,需要手动管理 | 多线程等待场景 |
| CompletableFuture | 异步非阻塞,支持链式调用 | JDK 8+ | 现代异步编程 |
# 1.2 Lock vs synchronized
面试题:在 Java 中 Lock 接口比 synchronized 块的优势是什么?如何实现一个高效的读写缓存?
考察点:锁机制的深入理解、读写锁的应用
# synchronized vs Lock 对比
| 特性 | synchronized | Lock |
|---|---|---|
| 使用方式 | 关键字,自动加锁/释放 | 接口,手动 lock()/unlock() |
| 锁类型 | 可重入、非公平 | 可重入、可公平、可中断 |
| 性能 | JDK 6+ 优化后相当 | 高并发下略优 |
| 灵活性 | 低(无法中断、超时) | 高(tryLock、lockInterruptibly) |
| 读写分离 | ❌ 不支持 | ✅ ReadWriteLock |
| 条件变量 | 只有 wait/notify | 支持多个 Condition |
# Lock 的核心优势
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class LockAdvantages {
private final Lock lock = new ReentrantLock();
// 1. 可中断锁
public void interruptibleLock() throws InterruptedException {
lock.lockInterruptibly(); // 可以被 interrupt() 打断
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
// 2. 尝试获取锁(非阻塞)
public boolean tryLock() {
if (lock.tryLock()) {
try {
// 获取到锁,执行业务
return true;
} finally {
lock.unlock();
}
}
return false; // 未获取到锁,直接返回
}
// 3. 超时获取锁
public boolean tryLockWithTimeout() throws InterruptedException {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 3 秒内获取到锁
return true;
} finally {
lock.unlock();
}
}
return false; // 超时未获取到锁
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 实战:实现高效读写缓存
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 高性能读写缓存
* - 多个线程可以同时读
* - 只有一个线程可以写
* - 写线程会阻塞所有读线程
*/
public class ReadWriteCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作(共享锁)
public V get(K key) {
rwLock.readLock().lock(); // 多个线程可以同时持有读锁
try {
System.out.println(Thread.currentThread().getName() + " 读取数据");
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
// 写操作(排他锁)
public void put(K key, V value) {
rwLock.writeLock().lock(); // 只有一个线程可以持有写锁
try {
System.out.println(Thread.currentThread().getName() + " 写入数据");
Thread.sleep(100); // 模拟写入耗时
cache.put(key, value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
rwLock.writeLock().unlock();
}
}
// 测试读写性能
public static void main(String[] args) {
ReadWriteCache<String, String> cache = new ReadWriteCache<>();
// 1 个写线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
cache.put("key" + i, "value" + i);
}
}, "Writer").start();
// 10 个读线程(可以并发执行)
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
cache.get("key" + j);
}
}, "Reader-" + i).start();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
输出示例:
Reader-0 读取数据
Reader-1 读取数据 // 多个读线程并发执行
Reader-2 读取数据
Writer 写入数据 // 写线程阻塞所有读线程
Reader-3 读取数据
...
2
3
4
5
6
# 锁升级:从 synchronized 到 ReadWriteLock
// ❌ 低效方案:所有操作都互斥
public class SynchronizedCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
public synchronized V get(K key) {
return cache.get(key); // 读操作也需要等待
}
public synchronized void put(K key, V value) {
cache.put(key, value);
}
}
// ✅ 高效方案:读写分离
public class RWLockCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public V get(K key) {
lock.readLock().lock();
try {
return cache.get(key); // 多个线程可以同时读
} finally {
lock.readLock().unlock();
}
}
public void put(K key, V value) {
lock.writeLock().lock();
try {
cache.put(key, value); // 写操作独占
} finally {
lock.writeLock().unlock();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 1.3 wait() vs sleep()
面试题:在 Java 中 wait 和 sleep 方法的不同?
考察点:线程状态管理、锁机制
# 核心区别对比
| 维度 | wait() | sleep() |
|---|---|---|
| 所属类 | Object 类的方法 | Thread 类的静态方法 |
| 锁释放 | ✅ 释放锁(其他线程可获取) | ❌ 不释放锁 |
| 使用场景 | 线程间通信(配合 notify) | 暂停当前线程 |
| 调用位置 | 必须在 synchronized 块内 | 任何地方都可调用 |
| 唤醒方式 | notify()/notifyAll() | 时间到自动唤醒 |
| 异常 | InterruptedException | InterruptedException |
# 代码示例对比
public class WaitVsSleep {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 示例 1:wait() 会释放锁
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("T1: 持有锁");
try {
System.out.println("T1: 调用 wait(),释放锁");
lock.wait(); // 释放锁,其他线程可以获取
System.out.println("T1: 被唤醒,重新获取锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("T2: 获取到锁(因为 T1 释放了)");
lock.notify(); // 唤醒 T1
System.out.println("T2: 唤醒 T1,但仍持有锁");
try {
Thread.sleep(2000); // T2 sleep 不释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T2: 释放锁");
} // T2 退出 synchronized 块,释放锁
});
t1.start();
Thread.sleep(100); // 确保 T1 先执行
t2.start();
t1.join();
t2.join();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
输出:
T1: 持有锁
T1: 调用 wait(),释放锁
T2: 获取到锁(因为 T1 释放了)
T2: 唤醒 T1,但仍持有锁
T2: 释放锁
T1: 被唤醒,重新获取锁
2
3
4
5
6
# sleep() 示例
public class SleepDemo {
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("T1: 持有锁");
try {
System.out.println("T1: 调用 sleep(2000)");
Thread.sleep(2000); // 不释放锁!
System.out.println("T1: sleep 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("T2: 获取到锁");
}
});
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start(); // T2 需要等待 T1 的 sleep 结束
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
输出:
T1: 持有锁
T1: 调用 sleep(2000)
// 等待 2 秒...
T1: sleep 结束
T2: 获取到锁 // T2 必须等 T1 释放锁
2
3
4
5
# 面试加分项
典型使用场景:
// wait/notify 场景:生产者-消费者
class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 10;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait(); // 队列满,等待消费
}
queue.add(item);
notifyAll(); // 通知消费者
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 队列空,等待生产
}
int item = queue.poll();
notifyAll(); // 通知生产者
return item;
}
}
// sleep 场景:定时任务
class ScheduledTask {
public void execute() {
while (true) {
try {
performTask();
Thread.sleep(5000); // 每 5 秒执行一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private void performTask() {
System.out.println("执行定时任务");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
记忆口诀
- wait:我在等(wait)别人通知(notify),所以要释放锁让别人进来通知我
- sleep:我只是睡(sleep)一会儿,锁还在我手上,别人别想拿
# 1.4 实现阻塞队列
面试题:用 Java 实现一个阻塞队列
考察点:并发编程能力、wait/notify 机制、JUC 并发工具
# 方案一:使用 wait/notify 实现
import java.util.LinkedList;
import java.util.Queue;
/**
* 手写阻塞队列
* - put:队列满时阻塞
* - take:队列空时阻塞
*/
public class MyBlockingQueue<E> {
private final Queue<E> queue = new LinkedList<>();
private final int capacity;
public MyBlockingQueue(int capacity) {
this.capacity = capacity;
}
// 放入元素(队列满时阻塞)
public synchronized void put(E element) throws InterruptedException {
// 注意:必须用 while 而不是 if(防止虚假唤醒)
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() + " 队列已满,等待...");
wait(); // 释放锁,等待消费
}
queue.add(element);
System.out.println(Thread.currentThread().getName() + " 放入元素:" + element);
notifyAll(); // 唤醒所有等待的消费者
}
// 取出元素(队列空时阻塞)
public synchronized E take() throws InterruptedException {
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " 队列为空,等待...");
wait(); // 释放锁,等待生产
}
E element = queue.poll();
System.out.println(Thread.currentThread().getName() + " 取出元素:" + element);
notifyAll(); // 唤醒所有等待的生产者
return element;
}
public synchronized int size() {
return queue.size();
}
// 测试
public static void main(String[] args) {
MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(3);
// 生产者
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Producer").start();
// 消费者
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.take();
Thread.sleep(300); // 消费慢于生产
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Consumer").start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 方案二:使用 Lock 和 Condition
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用 Lock 和 Condition 实现阻塞队列
* - 性能优于 synchronized
* - 可以分别唤醒生产者和消费者
*/
public class LockBlockingQueue<E> {
private final Queue<E> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
// 两个条件变量:分别控制生产者和消费者
private final Condition notFull = lock.newCondition(); // 队列未满
private final Condition notEmpty = lock.newCondition(); // 队列非空
public LockBlockingQueue(int capacity) {
this.capacity = capacity;
}
public void put(E element) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
System.out.println(Thread.currentThread().getName() + " 队列已满,等待...");
notFull.await(); // 等待队列未满
}
queue.add(element);
System.out.println(Thread.currentThread().getName() + " 放入:" + element);
notEmpty.signal(); // 唤醒一个消费者
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " 队列为空,等待...");
notEmpty.await(); // 等待队列非空
}
E element = queue.poll();
System.out.println(Thread.currentThread().getName() + " 取出:" + element);
notFull.signal(); // 唤醒一个生产者
return element;
} finally {
lock.unlock();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 方案三:使用 JDK 自带的 BlockingQueue
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class JDKBlockingQueueDemo {
public static void main(String[] args) {
// JDK 提供的阻塞队列
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// 生产者
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put(i); // 队列满时自动阻塞
System.out.println("生产:" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
int val = queue.take(); // 队列空时自动阻塞
System.out.println("消费:" + val);
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
为什么用 while 而不是 if?
// ❌ 错误写法
if (queue.size() == capacity) {
wait(); // 虚假唤醒后不会再检查条件
}
// ✅ 正确写法
while (queue.size() == capacity) {
wait(); // 唤醒后会重新检查条件
}
2
3
4
5
6
7
8
9
虚假唤醒:线程可能在没有被 notify 的情况下自动唤醒(底层操作系统原因),使用 while 可以确保条件仍然满足。
# 1.5 生产者-消费者模式
面试题:用 Java 写代码来解决生产者-消费者问题
考察点:经典并发模式、线程协作
# 完整实现:生产者-消费者
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 生产者-消费者模式
* - 多个生产者
* - 多个消费者
* - 共享缓冲区
*/
public class ProducerConsumerDemo {
// 生产者
static class Producer implements Runnable {
private final BlockingQueue<Integer> queue;
private final int id;
public Producer(BlockingQueue<Integer> queue, int id) {
this.queue = queue;
this.id = id;
}
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) {
int product = id * 100 + i;
queue.put(product);
System.out.println("生产者-" + id + " 生产:" + product +
" [队列大小:" + queue.size() + "]");
Thread.sleep((int) (Math.random() * 1000));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 消费者
static class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
private final int id;
public Consumer(BlockingQueue<Integer> queue, int id) {
this.queue = queue;
this.id = id;
}
@Override
public void run() {
try {
while (true) {
Integer product = queue.take();
System.out.println(" 消费者-" + id + " 消费:" + product +
" [队列大小:" + queue.size() + "]");
Thread.sleep((int) (Math.random() * 1500));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
// 3 个生产者
for (int i = 1; i <= 3; i++) {
new Thread(new Producer(queue, i), "Producer-" + i).start();
}
// 2 个消费者
for (int i = 1; i <= 2; i++) {
new Thread(new Consumer(queue, i), "Consumer-" + i).start();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
输出示例:
生产者-1 生产:100 [队列大小:1]
生产者-2 生产:200 [队列大小:2]
消费者-1 消费:100 [队列大小:1]
生产者-3 生产:300 [队列大小:2]
消费者-2 消费:200 [队列大小:1]
生产者-1 生产:101 [队列大小:2]
...
2
3
4
5
6
7
# 拓展:哲学家进餐问题
import java.util.concurrent.Semaphore;
/**
* 哲学家进餐问题
* - 5 个哲学家,5 支筷子
* - 每个哲学家需要 2 支筷子才能进餐
* - 防止死锁:最多允许 4 个哲学家同时拿筷子
*/
public class DiningPhilosophers {
static class Philosopher extends Thread {
private final int id;
private final Semaphore leftChopstick;
private final Semaphore rightChopstick;
private final Semaphore maxDiners; // 限制同时进餐人数
public Philosopher(int id, Semaphore left, Semaphore right, Semaphore maxDiners) {
this.id = id;
this.leftChopstick = left;
this.rightChopstick = right;
this.maxDiners = maxDiners;
}
@Override
public void run() {
try {
for (int i = 0; i < 3; i++) {
think();
eat();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void think() throws InterruptedException {
System.out.println("哲学家-" + id + " 正在思考...");
Thread.sleep((int) (Math.random() * 1000));
}
private void eat() throws InterruptedException {
// 限制同时进餐人数,防止死锁
maxDiners.acquire();
// 拿左边筷子
leftChopstick.acquire();
System.out.println("哲学家-" + id + " 拿起左边筷子");
// 拿右边筷子
rightChopstick.acquire();
System.out.println("哲学家-" + id + " 拿起右边筷子,开始进餐");
Thread.sleep((int) (Math.random() * 1000));
// 放下筷子
System.out.println("哲学家-" + id + " 进餐完毕,放下筷子");
rightChopstick.release();
leftChopstick.release();
maxDiners.release();
}
}
public static void main(String[] args) {
int n = 5;
Semaphore[] chopsticks = new Semaphore[n];
for (int i = 0; i < n; i++) {
chopsticks[i] = new Semaphore(1); // 每支筷子只能被一个人拿
}
// 最多 4 个哲学家同时进餐(防止死锁)
Semaphore maxDiners = new Semaphore(n - 1);
for (int i = 0; i < n; i++) {
Semaphore left = chopsticks[i];
Semaphore right = chopsticks[(i + 1) % n];
new Philosopher(i, left, right, maxDiners).start();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 1.6 死锁问题
面试题:用 Java 编程一个会导致死锁的程序,你将怎么解决?
考察点:死锁的四个必要条件、死锁预防与避免
# 死锁的四个必要条件
- 互斥条件:资源只能被一个线程占用
- 请求与保持:线程持有资源的同时请求新资源
- 不剥夺条件:资源不能被强制剥夺
- 循环等待:多个线程形成环路等待资源
# 死锁示例代码
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// 线程 1:先获取 lock1,再获取 lock2
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("T1: 持有 lock1,等待 lock2...");
try {
Thread.sleep(100); // 增加死锁概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("T1: 获取 lock2");
}
}
}, "T1");
// 线程 2:先获取 lock2,再获取 lock1(顺序相反)
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("T2: 持有 lock2,等待 lock1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("T2: 获取 lock1");
}
}
}, "T2");
t1.start();
t2.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
输出(死锁):
T1: 持有 lock1,等待 lock2...
T2: 持有 lock2,等待 lock1...
// 程序卡住,T1 等 lock2,T2 等 lock1,形成循环等待
2
3
# 解决方案
方案一:统一加锁顺序
public class DeadlockFree1 {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// ✅ 两个线程都按照相同的顺序获取锁
Thread t1 = new Thread(() -> {
synchronized (lock1) { // 先 lock1
System.out.println("T1: 持有 lock1");
synchronized (lock2) { // 再 lock2
System.out.println("T1: 获取 lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock1) { // 也是先 lock1
System.out.println("T2: 持有 lock1");
synchronized (lock2) { // 再 lock2
System.out.println("T2: 获取 lock2");
}
}
});
t1.start();
t2.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
方案二:使用 tryLock 超时机制
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockFree2 {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 尝试获取锁,超时则放弃
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println("T1: 获取 lock1");
Thread.sleep(50);
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println("T1: 获取 lock2");
} finally {
lock2.unlock();
}
} else {
System.out.println("T1: 获取 lock2 超时,放弃");
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// T2 同理...
t1.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 1.7 volatile 关键字
面试题:Java 中的 volatile 关键字有什么作用?它跟 synchronized 有什么不同?
考察点:Java 内存模型、可见性、有序性、原子性
# volatile 的作用
- 保证可见性:一个线程修改后,其他线程立即看到
- 禁止指令重排序:保证有序性
- ❌ 不保证原子性:
i++等操作仍然不是线程安全的
# volatile vs synchronized
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 保证(禁止重排序) | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 适用场景 | 单个变量读写 | 复合操作 |
| 性能 | 轻量级(无锁) | 重量级(加锁) |
| 阻塞 | 不会阻塞 | 可能阻塞 |
# 代码示例
public class VolatileDemo {
// ❌ 没有 volatile:可能看不到修改
private static boolean flag = false;
// ✅ 使用 volatile:立即可见
private static volatile boolean volatileFlag = false;
public static void main(String[] args) throws InterruptedException {
// 测试 1:没有 volatile(可能死循环)
new Thread(() -> {
while (!flag) {
// 可能一直看不到 flag 的修改
}
System.out.println("线程 1:flag 变为 true");
}).start();
Thread.sleep(100);
flag = true; // 主线程修改
System.out.println("主线程:flag 设为 true");
// 测试 2:使用 volatile(正常退出)
new Thread(() -> {
while (!volatileFlag) {
// volatile 保证可见性
}
System.out.println("线程 2:volatileFlag 变为 true");
}).start();
Thread.sleep(100);
volatileFlag = true;
System.out.println("主线程:volatileFlag 设为 true");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# volatile 不能保证原子性
public class VolatileNotAtomic {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++; // ❌ 非原子操作,volatile 无法保证
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("count = " + count); // 结果可能小于 10000
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
正确方案:使用 AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count.incrementAndGet(); // ✅ 原子操作
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("count = " + count.get()); // 结果一定是 10000
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1.8 其他高频问题速查
# start() vs run()
| 方法 | 效果 | 说明 |
|---|---|---|
start() | ✅ 创建新线程 | 调用 native 方法启动线程 |
run() | ❌ 不创建线程 | 只是普通方法调用 |
Thread t = new Thread(() -> System.out.println("执行中"));
t.start(); // ✅ 在新线程中执行
t.run(); // ❌ 在当前线程中执行(相当于普通方法调用)
2
3
4
# CountDownLatch vs CyclicBarrier
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 重用性 | ❌ 一次性 | ✅ 可重用 |
| 等待方式 | await() 等待计数归零 | await() 等待所有线程到达 |
| 典型场景 | 主线程等待子线程完成 | 多线程互相等待 |
// CountDownLatch:主线程等待 N 个子线程完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("任务完成");
latch.countDown(); // 计数 -1
}).start();
}
latch.await(); // 主线程等待计数归零
System.out.println("所有任务完成");
// CyclicBarrier:N 个线程互相等待
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达,开始下一阶段");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println("阶段 1 完成");
barrier.await(); // 等待其他线程
System.out.println("阶段 2 开始");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 不可变对象
定义:创建后状态不能改变的对象
优势:
- ✅ 线程安全(无需同步)
- ✅ 可以安全共享
- ✅ 适合作为 HashMap 的 key
// String 是不可变的
public final class String {
private final char[] value; // final 修饰
// 没有提供修改方法,所有"修改"都返回新对象
public String toUpperCase() {
return new String(upperCaseChars);
}
}
// 自定义不可变类
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
// ❌ 不提供 setter 方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 2️⃣ 回调 vs 订阅模式
# 核心对比
相比于传统的回调函数(Callback),订阅机制(Subscription)在状态管理和事件处理方面具有以下关键优势:
| 维度 | 回调函数 | 订阅机制 |
|---|---|---|
| 耦合性 | 紧耦合 | 松散耦合 |
| 多订阅者 | 需手动管理列表 | 天生支持 |
| 生命周期管理 | 手动注册/移除 | 自动清理 |
| 可组合性 | 线性逻辑,难组合 | 支持复杂组合 |
| 异步流处理 | 容易陷入"回调地狱" | 流式处理,优雅 |
| 响应式编程 | 不支持 | 完全支持 |
# 1. 解耦合性(Decoupling)
# 回调函数
// ❌ 回调函数:紧耦合
public class UserService {
private Callback callback;
public void setCallback(Callback callback) {
this.callback = callback; // 紧密绑定
}
public void updateUser(User user) {
// 业务逻辑
if (callback != null) {
callback.onUserUpdated(user); // 直接调用
}
}
}
// 使用方需要显式注入回调
UserService service = new UserService();
service.setCallback(new Callback() {
@Override
public void onUserUpdated(User user) {
System.out.println("用户更新:" + user);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 订阅机制
// ✅ 订阅:松散耦合
public class UserService {
private final List<Subscriber<User>> subscribers = new ArrayList<>();
public void subscribe(Subscriber<User> subscriber) {
subscribers.add(subscriber);
}
public void updateUser(User user) {
// 业务逻辑
notifySubscribers(user); // 发布事件
}
private void notifySubscribers(User user) {
for (Subscriber<User> subscriber : subscribers) {
subscriber.onNext(user);
}
}
}
// 订阅者可以独立存在
userService.subscribe(user -> System.out.println("订阅者 1:" + user));
userService.subscribe(user -> System.out.println("订阅者 2:" + user));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2. 管理多个订阅者
# 回调函数:手动管理列表
// ❌ 回调函数需要手动管理多个回调
public class EventEmitter {
private final List<Callback> callbacks = new ArrayList<>();
public void addCallback(Callback callback) {
callbacks.add(callback);
}
public void removeCallback(Callback callback) {
callbacks.remove(callback); // 需要手动移除
}
public void emit(String event) {
for (Callback callback : callbacks) {
callback.handle(event);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 订阅机制:原生支持
// ✅ 订阅机制原生支持多订阅者
import io.reactivex.rxjava3.core.Observable;
Observable<String> observable = Observable.just("Event 1", "Event 2");
// 多个订阅者自动管理
observable.subscribe(event -> System.out.println("订阅者 A:" + event));
observable.subscribe(event -> System.out.println("订阅者 B:" + event));
observable.subscribe(event -> System.out.println("订阅者 C:" + event));
2
3
4
5
6
7
8
9
# 3. 自动清理和管理
# 回调函数:容易内存泄漏
// ❌ 回调函数需要手动清理
public class Activity {
private DataService service = new DataService();
public void onCreate() {
service.setCallback(data -> {
updateUI(data); // Activity 销毁后仍可能被调用
});
}
public void onDestroy() {
// 忘记移除回调 → 内存泄漏!
// service.setCallback(null);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 订阅机制:自动管理
// ✅ 订阅机制支持自动清理
import io.reactivex.rxjava3.disposables.Disposable;
public class Activity {
private Disposable disposable;
public void onCreate() {
disposable = observable.subscribe(data -> {
updateUI(data);
});
}
public void onDestroy() {
disposable.dispose(); // 自动取消订阅,防止泄漏
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4. 可组合性(Composability)
# 回调函数:难以组合
// ❌ 回调地狱
getUserById(userId, user -> {
getOrdersByUser(user, orders -> {
getOrderDetails(orders.get(0), details -> {
getPaymentInfo(details, payment -> {
// 嵌套4层,难以维护
processPayment(payment);
});
});
});
});
2
3
4
5
6
7
8
9
10
11
# 订阅机制:流式组合
// ✅ 响应式链式调用
getUserById(userId)
.flatMap(user -> getOrdersByUser(user))
.flatMap(orders -> getOrderDetails(orders.get(0)))
.flatMap(details -> getPaymentInfo(details))
.subscribe(
payment -> processPayment(payment),
error -> handleError(error),
() -> System.out.println("完成")
);
2
3
4
5
6
7
8
9
10
# 5. 异步流处理
# 回调函数:回调地狱
// ❌ 多个异步操作嵌套
fetchData1(result1 -> {
fetchData2(result2 -> {
fetchData3(result3 -> {
// 嵌套太深,难以阅读
});
});
});
2
3
4
5
6
7
8
# 订阅机制:优雅处理
// ✅ 使用 RxJava 处理异步流
Observable.zip(
fetchData1(),
fetchData2(),
fetchData3(),
(result1, result2, result3) -> {
return combineResults(result1, result2, result3);
}
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
finalResult -> updateUI(finalResult),
error -> showError(error)
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6. 响应式编程
# 回调函数:不支持响应式
// ❌ 手动管理状态更新
public class Counter {
private int count = 0;
private Callback callback;
public void increment() {
count++;
if (callback != null) {
callback.onCountChanged(count); // 手动通知
}
}
}
2
3
4
5
6
7
8
9
10
11
12
# 订阅机制:自动响应
// ✅ 响应式自动更新
import io.reactivex.rxjava3.subjects.BehaviorSubject;
public class Counter {
private final BehaviorSubject<Integer> countSubject = BehaviorSubject.createDefault(0);
public void increment() {
int newCount = countSubject.getValue() + 1;
countSubject.onNext(newCount); // 自动通知所有订阅者
}
public Observable<Integer> getCount() {
return countSubject;
}
}
// 使用
Counter counter = new Counter();
counter.getCount().subscribe(count -> {
System.out.println("当前计数:" + count); // 自动响应变化
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7. 实战对比:状态管理
# 回调方式(传统)
public class UserStore {
private User currentUser;
private final List<UserChangeCallback> callbacks = new ArrayList<>();
public void addListener(UserChangeCallback callback) {
callbacks.add(callback);
}
public void removeListener(UserChangeCallback callback) {
callbacks.remove(callback);
}
public void setUser(User user) {
this.currentUser = user;
notifyCallbacks();
}
private void notifyCallbacks() {
for (UserChangeCallback callback : callbacks) {
callback.onUserChanged(currentUser);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 订阅方式(现代)
import io.reactivex.rxjava3.subjects.PublishSubject;
public class UserStore {
private final PublishSubject<User> userSubject = PublishSubject.create();
public void setUser(User user) {
userSubject.onNext(user); // 一行代码,自动通知所有订阅者
}
public Observable<User> observeUser() {
return userSubject;
}
}
// 使用
userStore.observeUser()
.filter(user -> user.getAge() > 18) // 支持转换
.map(User::getName)
.distinctUntilChanged() // 去重
.subscribe(name -> System.out.println("成年用户:" + name));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 总结
回调函数适用场景:
- 简单的单次事件响应
- 不需要取消操作
- 订阅者数量固定且少
订阅机制适用场景:
- 复杂的状态管理
- 多个订阅者
- 需要自动清理资源
- 异步数据流处理
- 响应式编程
推荐实践
在现代 Java 开发中,推荐使用订阅机制(如 RxJava、Reactor)来处理复杂的异步场景和状态管理,它能显著提高代码的可读性和可维护性。
# 📖 参考资源
# 官方文档
- Java Concurrency in Practice (opens new window)
- Java SE Concurrency (opens new window)
- RxJava Documentation (opens new window)
# 推荐书籍
- 《Java 并发编程实战》(Java Concurrency in Practice)
- 《深入理解 Java 虚拟机》(周志明)
- 《Effective Java》(Joshua Bloch)
# 在线资源
# 🎯 面试建议
# 回答技巧
先说原理,再举例子
- 例:wait() 会释放锁,因为它需要让其他线程进来通知(举生产者-消费者例子)
对比说明,加深印象
- 例:对比 volatile 和 synchronized 的异同
结合实战,展示经验
- 例:在项目中遇到的死锁问题如何排查和解决
知识延伸,展现深度
- 例:从 synchronized 引申到 JVM 的锁优化(偏向锁、轻量级锁、重量级锁)
# 常见追问
| 基础问题 | 可能追问 |
|---|---|
| wait() vs sleep() | 为什么 wait() 必须在 synchronized 块内? |
| volatile | volatile 如何保证可见性?(提示:内存屏障) |
| 死锁 | 如何排查生产环境的死锁?(提示:jstack) |
| ThreadLocal | ThreadLocal 可能导致什么问题?(提示:内存泄漏) |
# 面试准备清单
- [ ] 掌握 Java 内存模型(JMM)
- [ ] 熟悉 JUC 并发包(java.util.concurrent)
- [ ] 能手写生产者-消费者、死锁代码
- [ ] 理解 CAS、AQS 原理
- [ ] 了解线程池的核心参数和工作原理
- [ ] 掌握常见并发容器(ConcurrentHashMap、CopyOnWriteArrayList)
- [ ] 熟悉分布式锁(Redis、Zookeeper)
祝你面试顺利!🚀