本文写于 2015 年 6 月——JDK 8 刚普及,Lambda 表达式 + Stream API 改变 Java 并发编程范式。
一、如何保证线程安全
1.1 多线程安全定义
多线程安全是指在并发环境下,多个线程访问共享资源时,程序能够正确地执行,而不会出现数据不一致或竞争条件等问题。反之,如果程序出现了数据不一致、死锁、饥饿等问题,就称为线程不安全。
1.2 6 大线程安全方案
| 方案 | 适用场景 | 示例 |
|---|
synchronized | 单体应用同步 | synchronized(lock) { ... } |
ReentrantLock | 需要可中断/公平锁 | lock.lock(); try { ... } finally { lock.unlock(); } |
Atomic 类 | 简单变量原子操作 | AtomicInteger count = new AtomicInteger() |
| 线程安全容器 | 集合操作 | ConcurrentHashMap、CopyOnWriteArrayList |
ThreadLocal | 每线程独立变量 | ThreadLocal<User> currentUser |
volatile | 状态标志可见性 | volatile boolean running = true |
1.3 选型决策
graph TD
A[需要线程安全] --> B{操作类型?}
B -->|互斥| C{需要高级功能?}
C -->|是| D[ReentrantLock]
C -->|否| E[synchronized]
B -->|原子操作| F[Atomic 类]
B -->|集合| G[ConcurrentHashMap]
B -->|每线程独立| H[ThreadLocal]
B -->|标志位| I[volatile]二、面试场景:JSON 字段并发修改
2.1 场景描述
有一个 key 对应的 value 是一个 json 结构,json 当中有好几个子任务,这些子任务如果对 key 进行修改的话,会不会存在线程安全的问题?如何解决?如果是多个节点的情况,应该怎么加锁?
2.2 单节点方案:ReentrantLock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| class KeyManager {
private final ReentrantLock lock = new ReentrantLock();
private String key = "{\"tasks\": [\"task1\", \"task2\"]}";
public String readKey() {
lock.lock();
try {
return key;
} finally {
lock.unlock();
}
}
public void updateKey(String newKey) {
lock.lock();
try {
this.key = newKey;
} finally {
lock.unlock();
}
}
}
|
2.3 多节点方案:Redisson 分布式锁
1
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
| class DistributedKeyManager {
private final RedissonClient redisson;
public DistributedKeyManager() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
this.redisson = Redisson.create(config);
}
public void updateKey(String key, String newValue) {
RLock lock = redisson.getLock(key);
lock.lock();
try {
// 模拟读取和更新操作
String currentValue = readFromDatabase(key);
String updatedValue = modifyJson(currentValue, newValue);
writeToDatabase(key, updatedValue);
} finally {
lock.unlock();
}
}
private String readFromDatabase(String key) {
return "{\"tasks\": [\"task1\", \"task2\"]}";
}
private String modifyJson(String json, String newValue) {
return json.replace("task1", newValue);
}
private void writeToDatabase(String key, String value) {
// 写回 DB
}
}
|
为什么用 Redisson:
- 内置看门狗自动续期(防止业务执行慢导致锁过期)
- 支持公平锁/可重入锁/读写锁
- Redlock 算法实现多节点安全
三、ThreadLocal 实战
3.1 什么是 ThreadLocal
ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本,而不会影响其他线程的副本,确保了线程安全。
1
2
3
4
5
6
7
8
9
10
11
| // 错误:SimpleDateFormat 是非线程安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 sdf.parse() 可能 NumberFormatException
// 正确:使用 ThreadLocal
private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public Date parse(String dateStr) {
return sdfThreadLocal.get().parse(dateStr);
}
|
生动比喻:
比如饭店要做一道菜,但是有 5 个厨师一起做,这样的话就很乱了,因为如果一个厨师已经放过盐了,假如其他厨师都不知道,于是都各自放了一次盐,导致最后的菜很咸。这就好比多线程的情况,线程不安全。我们用了 ThreadLocal 之后,相当于每个厨师只负责自己的一道菜,一共有 5 道菜,这样的话就非常清晰明了,不会出现问题。
3.3 ThreadLocal 内存泄漏
Q:ThreadLocal 为什么会导致内存泄漏?
原理:
- ThreadLocalMap 的 Entry 继承
WeakReference<ThreadLocal<?>> - 当 ThreadLocal 失去强引用后,Entry 的 key 被 GC 回收
- 但 value 仍然被 Thread → ThreadLocalMap → Entry 强引用
- 线程不结束(如线程池核心线程)→ value 永远不释放
解决:
1
2
3
4
5
6
| try {
threadLocal.set(value);
// 业务逻辑
} finally {
threadLocal.remove(); // 必须 remove
}
|
四、Future 与 Callable
4.1 为什么需要 Future
线程执行完要有返回值的,使用 Callable 接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
| ExecutorService executor = Executors.newFixedThreadPool(5);
// Runnable 没有返回值
Runnable runnable = () -> System.out.println("hello");
// Callable 有返回值 + 抛异常
Callable<Integer> callable = () -> {
Thread.sleep(1000);
return 42;
};
Future<Integer> future = executor.submit(callable);
Integer result = future.get(); // 阻塞等结果
|
4.2 Future 的局限性
| 问题 | 描述 |
|---|
| 阻塞 | get() 阻塞,无法链式 |
| 异常 | 异常被包装在 ExecutionException |
| 多个 Future | 需要手动编排 |
4.3 解决方案:CompletableFuture(JDK 8+)
1
2
3
4
5
6
7
| CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> {
// 异步任务
return 42;
})
.thenApply(v -> v * 2) // 链式处理
.thenAccept(System.out::println); // 最终消费
|
五、ConcurrentHashMap 原理
5.1 JDK 7 vs JDK 8 实现
| 维度 | JDK 7 | JDK 8 |
|---|
| 数据结构 | Segment + HashEntry 数组 | Node 数组 + 链表 + 红黑树 |
| 锁粒度 | Segment(16 个) | 桶(每个头节点) |
| 锁类型 | ReentrantLock | CAS + synchronized |
| 读 | 不加锁 | 不加锁 |
| 写 | 锁 Segment | 锁桶头节点 |
5.2 JDK 8 关键优化
- CAS + synchronized:锁粒度更细
- 红黑树化:链表长度 > 8 时转红黑树
- volatile + 协调:保证内存可见性
- 扩容协助:多线程协同扩容
5.3 size() 实现
1
2
3
| // JDK 8 通过 baseCount + CounterCell[] 数组实现
// 每个线程更新时用 CAS 累加到自己的 CounterCell
// 总数 = baseCount + sum(CounterCell.values)
|
六、AQS(AbstractQueuedSynchronizer)
6.1 AQS 是什么
AQS 是 JUC(java.util.concurrent)包的核心,用于构建锁和同步器。
6.2 核心思想
1
| AQS = state 状态 + CLH 等待队列 + 模板方法
|
- state:同步状态(0 表示未占用,1 表示已占用)
- CLH 队列:等待线程的 FIFO 队列
- 模板方法:
tryAcquire / tryRelease / tryAcquireShared / tryReleaseShared
6.3 基于 AQS 的同步器
| 同步器 | 类型 | 用途 |
|---|
| ReentrantLock | 独占 | 可重入互斥锁 |
| ReentrantReadWriteLock | 独占+共享 | 读写锁 |
| Semaphore | 共享 | 信号量 |
| CountDownLatch | 共享 | 倒计时门闩 |
| CyclicBarrier | 共享 | 循环屏障 |
| ThreadPool Executor | 共享 | 线程池 |
七、synchronized vs ReentrantLock
7.1 对比
| 维度 | synchronized | ReentrantLock |
|---|
| 实现 | JVM 内置(monitor) | JDK 代码(基于 AQS) |
| 锁升级 | 无锁 → 偏向锁 → 轻量锁 → 重量锁 | 直接 CAS + LockSupport |
| 中断 | 不可中断 | lockInterruptibly() |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | wait/notify(1 个) | Condition(多个) |
| 性能 | JDK 6+ 优化后接近 | 略低 |
| 锁粒度 | 对象/类 | 显式 lock/unlock |
7.2 synchronized 锁升级(JDK 6+ 优化)
| 状态 | 描述 | 适用 |
|---|
| 无锁 | 无线程竞争 | 初始状态 |
| 偏向锁 | 单线程反复进入(CAS 替换 ThreadID) | 始终单线程 |
| 轻量锁 | 短时间竞争(CAS 自旋) | 低竞争 |
| 重量锁 | 操作系统互斥量 | 高竞争 |
八、写在最后
并发编程面试要点:
- 基础概念:6 大线程安全方案选型
- 原理:ConcurrentHashMap 实现、AQS 原理、synchronized 锁升级
- 实战:ThreadLocal 内存泄漏、Future vs CompletableFuture
- 进阶:多节点分布式锁、Reactor 模式、协程(Project Loom)
参考资料