前言:并发编程是 Java 面试的高频考点,也是写出高质量代码的必备技能。很多同学在面试时能背出"synchronized 是悲观锁、ReentrantLock 是可重入锁",但问到底层原理就开始含糊。本文从最基础的线程安全问题出发,一步步深入 synchronized、volatile、ReentrantLock、AQS 的底层实现,配合完整可运行的代码,帮你真正搞懂 Java 并发中的锁。
目录
- 为什么需要锁?先看一个经典 Bug
- synchronized:最熟悉的陌生人
- volatile:轻量级可见性保证
- 深入 synchronized 底层:对象头与 Monitor
- 锁升级机制:偏向锁 → 轻量级锁 → 重量级锁
- ReentrantLock:比 synchronized 更灵活
- AQS:锁的底层框架,一文看懂
- 读写锁 ReentrantReadWriteLock:读多写少的最佳方案
- 死锁:如何产生与如何避免
- 实战:手写一个线程安全的 LRU 缓存
- 总结:如何选择合适的锁?
1. 为什么需要锁?先看一个经典 Bug
我们先来看一个最简单的例子:多个线程同时对一个计数器进行累加。
public class CounterBug {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 创建 10 个线程,每个线程累加 1000 次
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++; // ⚠️ 这里有问题!
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread t : threads) {
t.join();
}
System.out.println("期望结果: 10000");
System.out.println("实际结果: " + count); // 结果每次都不一样!
}
}
运行结果(每次都不同):
期望结果: 10000
实际结果: 8742 // 可能是 7831、9012...
为什么结果不对?
count++ 看起来是一条语句,但在 JVM 底层实际上是 3 个步骤:
1. 从内存读取 count 的值到寄存器 → 读
2. 将寄存器中的值加 1 → 改
3. 将寄存器的值写回内存 → 写
当多个线程同时执行这 3 步时,就会出现"覆盖"问题:
时间线: 线程A读取count=100 → 线程B读取count=100
线程A计算100+1=101 → 线程B计算100+1=101
线程A写入count=101 → 线程B写入count=101 ← 两次加法只加了一次!
这就是竞态条件(Race Condition),锁的作用就是解决这类问题。
2. synchronized:最熟悉的陌生人
synchronized 是 Java 最基础的锁,用法简单但原理不简单。
2.1 三种使用方式
public class SynchronizedDemo {
private int count = 0;
private static int staticCount = 0;
// 方式一:修饰实例方法(锁住的是当前对象 this)
public synchronized void instanceMethod() {
count++;
}
// 方式二:修饰静态方法(锁住的是 Class 对象)
public static synchronized void staticMethod() {
staticCount++;
}
// 方式三:修饰代码块(锁住的是括号内的对象)
public void blockMethod() {
synchronized (this) {
count++;
}
}
// 也可以用任意对象作为锁
private final Object lock = new Object();
public void blockWithCustomLock() {
synchronized (lock) {
count++;
}
}
}
2.2 用 synchronized 修复上面的 Bug
public class CounterFixed {
private static int count = 0;
private static final Object LOCK = new Object();
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++) {
synchronized (LOCK) { // 加锁!同一时刻只有一个线程能进来
count++;
}
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("实际结果: " + count); // 稳定输出 10000
}
}
2.3 synchronized 的可重入性
同一个线程可以重复获取同一把锁,不会被自己阻塞:
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("进入 methodA");
methodB(); // 在已持有锁的情况下,调用同样需要这把锁的方法
}
public synchronized void methodB() {
System.out.println("进入 methodB"); // 不会死锁,因为 synchronized 是可重入的
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.methodA();
// 输出:
// 进入 methodA
// 进入 methodB
}
}
💡 可重入性的实现原理:每个锁内部有一个计数器和一个持有线程字段。同一线程再次获取锁时,计数器加 1;每次释放锁时,计数器减 1;计数器为 0 时锁才真正释放。
3. volatile:轻量级可见性保证
volatile 不是锁,但它解决了多线程中的可见性和指令重排问题。
3.1 可见性问题
public class VisibilityDemo {
// 没有 volatile 时,线程 B 可能永远看不到 flag 的变化
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 线程 A:等待 flag 变为 true
Thread threadA = new Thread(() -> {
System.out.println("线程A 开始等待...");
while (!flag) {
// 循环等待,什么也不做
}
System.out.println("线程A 检测到 flag 变化,退出循环");
});
threadA.start();
Thread.sleep(100); // 确保线程A先启动
// 线程 B:修改 flag
flag = true;
System.out.println("线程B 已修改 flag = true");
}
}
⚠️ 问题:由于 CPU 缓存的存在,线程 A 可能一直读取的是自己 CPU 缓存中的旧值,永远检测不到
flag的变化,导致程序死循环。
3.2 加上 volatile 解决问题
public class VisibilityFixed {
// volatile 保证对 flag 的写操作立即刷新到主内存
// 并且其他线程每次读取都从主内存读取最新值
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
System.out.println("线程A 开始等待...");
while (!flag) { }
System.out.println("线程A 检测到 flag 变化,退出循环"); // 一定会执行到
});
threadA.start();
Thread.sleep(100);
flag = true;
System.out.println("线程B 已修改 flag = true");
}
}
3.3 volatile 不能保证原子性
public class VolatileAtomicDemo {
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); // 结果依然不是 10000
}
}
volatile 的适用场景:
- ✅ 一个线程写,多个线程读(如开关标志位)
- ✅ 作为单例模式的双重检查锁(DCL)的一部分
- ❌ 不适合
count++这类复合操作(需用 AtomicInteger 或 synchronized)
4. 深入 synchronized 底层:对象头与 Monitor
要理解锁的本质,必须先了解 Java 对象的内存布局。
4.1 Java 对象的内存结构
每个 Java 对象在内存中由三部分组成:
┌─────────────────────────────────────────┐
│ Object Header(对象头) │
│ ┌──────────────────────────────────┐ │
│ │ Mark Word(8字节) │ │ ← 存储锁状态、hashCode、GC信息
│ │ Class Pointer(4或8字节) │ │ ← 指向类元数据
│ └──────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ Instance Data(实例数据) │ ← 字段值
├─────────────────────────────────────────┤
│ Padding(对齐填充) │ ← 保证对象大小是8字节的倍数
└─────────────────────────────────────────┘
4.2 Mark Word 的状态变化
Mark Word(64位JVM) 在不同锁状态下存储不同的信息:
无锁状态: [hashCode(31位) | 分代年龄(4位) | 偏向锁标志(1位)=0 | 锁标志(2位)=01]
偏向锁状态: [线程ID(54位) | epoch(2位) | 分代年龄(4位) | 偏向标志(1位)=1 | 锁标志(2位)=01]
轻量级锁: [指向栈中锁记录的指针(62位) | 锁标志(2位)=00]
重量级锁: [指向Monitor的指针(62位) | 锁标志(2位)=10]
GC标记: [空(62位) | 锁标志(2位)=11]
4.3 Monitor 的结构
当锁膨胀为重量级锁时,对象头指向一个 Monitor(监视器) 对象,其核心结构如下:
Monitor {
_owner → 当前持有锁的线程
_EntryList → 等待获取锁的线程队列(阻塞状态)
_WaitSet → 调用了 wait() 的线程队列(等待状态)
_count → 锁的重入次数
}
线程竞争锁的过程:
线程尝试获取锁
↓
检查 _owner 是否为空
↓ 为空
将 _owner 设为当前线程 → 执行同步代码
↓ 不为空(有人持有)
进入 _EntryList 阻塞等待
↓
锁持有者释放锁(_owner = null,_count = 0)
↓
唤醒 _EntryList 中的线程重新竞争
5. 锁升级机制:偏向锁 → 轻量级锁 → 重量级锁
Java 6 引入了锁升级机制,让 synchronized 不再一上来就用最"重"的重量级锁。
无锁
↓ 第一个线程来了(且无竞争)
偏向锁(Biased Lock)
↓ 第二个线程来竞争
轻量级锁(Thin Lock)
↓ 竞争激烈,CAS 自旋失败
重量级锁(Fat Lock)
5.1 偏向锁
适用场景:锁始终只被同一个线程访问(无竞争)
// 偏向锁的工作原理(伪代码)
if (markWord.biasedToCurrentThread()) {
// 直接进入,无需任何同步操作,性能极高
executeCode();
} else if (markWord.isUnbiased()) {
// CAS 尝试将 Mark Word 中的线程ID改为当前线程ID
if (CAS(markWord.threadId, null, currentThread)) {
executeCode(); // 成功,下次直接进入
} else {
// 竞争,升级为轻量级锁
upgradeTo(LightweightLock);
}
}
5.2 轻量级锁与 CAS 自旋
适用场景:多个线程交替访问,竞争不激烈
// 模拟 CAS 操作(Java 中实际由 Unsafe 类的 native 方法实现)
public class CASDemo {
private static AtomicInteger atomicCount = 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++) {
// incrementAndGet() 底层就是 CAS 自旋
// 相当于:
// int old, newVal;
// do {
// old = count;
// newVal = old + 1;
// } while (!CAS(count, old, newVal)); // 失败就重试
atomicCount.incrementAndGet();
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("结果: " + atomicCount.get()); // 稳定输出 10000
}
}
💡 CAS(Compare And Swap) 是一个硬件级原子指令:比较内存中的值与期望值,如果相等则更新,否则失败返回。整个操作不可被打断,不需要加锁。
5.3 锁升级演示
// 通过 JOL 工具可以观察对象头的变化(这里用注释说明)
public class LockUpgradeDemo {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
// 此时:无锁状态 (lock bits = 01)
// 线程1单独访问
synchronized (obj) {
// 此时:偏向锁状态 (lock bits = 01, biased = 1)
System.out.println("线程1持有锁");
}
// 此时:仍是偏向锁(偏向线程1)
Thread t2 = new Thread(() -> {
synchronized (obj) {
// 线程2来了,发现偏向锁偏向的是线程1
// → 撤销偏向锁 → 升级为轻量级锁 (lock bits = 00)
System.out.println("线程2持有锁");
}
});
t2.start();
t2.join();
// 模拟激烈竞争
Thread t3 = new Thread(() -> {
synchronized (obj) {
// 自旋多次失败 → 升级为重量级锁 (lock bits = 10)
System.out.println("线程3持有锁");
}
});
t3.start();
t3.join();
}
}
6. ReentrantLock:比 synchronized 更灵活
ReentrantLock 是 java.util.concurrent.locks 包中的显式锁,提供了更丰富的功能。
6.1 基本用法
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 手动加锁
try {
count++;
} finally {
lock.unlock(); // ⚠️ 必须在 finally 中释放,否则异常时锁永远不释放!
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("结果: " + demo.count); // 10000
}
}
6.2 synchronized vs ReentrantLock 对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM 内置关键字 | Java API |
| 锁的释放 | 自动释放 | 必须手动 unlock() |
| 可中断等待 | ❌ 不支持 | ✅ lockInterruptibly() |
| 尝试获取锁 | ❌ 不支持 | ✅ tryLock() |
| 超时获取锁 | ❌ 不支持 | ✅ tryLock(time, unit) |
| 公平锁 | ❌ 非公平 | ✅ 可选公平/非公平 |
| 条件变量 | wait/notify(单个) | Condition(多个) |
| 性能(JDK6+) | 差不多 | 差不多 |
6.3 ReentrantLock 的高级功能
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockAdvanced {
private final ReentrantLock lock = new ReentrantLock(true); // true = 公平锁
// ① tryLock:非阻塞尝试获取锁
public boolean tryIncrement(int count) {
if (lock.tryLock()) { // 立刻返回,不等待
try {
return true;
} finally {
lock.unlock();
}
}
return false; // 锁被占用,直接返回 false
}
// ② tryLock(timeout):带超时的获取锁
public boolean tryIncrementWithTimeout(int count) throws InterruptedException {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { // 最多等 500ms
try {
return true;
} finally {
lock.unlock();
}
}
return false;
}
// ③ lockInterruptibly:可被中断的加锁
public void interruptibleIncrement(int count) throws InterruptedException {
lock.lockInterruptibly(); // 如果等待时被 interrupt(),会抛出 InterruptedException
try {
// do something
} finally {
lock.unlock();
}
}
}
6.4 Condition:精准的线程通信
Condition 可以实现比 wait/notify 更精准的线程唤醒:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用 ReentrantLock + Condition 实现生产者-消费者模式
*/
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
private final ReentrantLock lock = new ReentrantLock();
// 分别创建两个条件变量,实现精准唤醒
private final Condition notFull = lock.newCondition(); // 队列不满的条件
private final Condition notEmpty = lock.newCondition(); // 队列不空的条件
// 生产者
public void produce(int item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
System.out.println("队列满了,生产者等待...");
notFull.await(); // 等待"不满"条件,同时释放锁
}
queue.offer(item);
System.out.println("生产: " + item + ",队列大小: " + queue.size());
notEmpty.signal(); // 精准唤醒消费者(不是 notifyAll,只唤醒消费者)
} finally {
lock.unlock();
}
}
// 消费者
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("队列空了,消费者等待...");
notEmpty.await(); // 等待"不空"条件
}
int item = queue.poll();
System.out.println("消费: " + item + ",队列大小: " + queue.size());
notFull.signal(); // 精准唤醒生产者
return item;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
// 3 个生产者
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
pc.produce(id * 10 + j);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer-" + i).start();
}
// 2 个消费者
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 8; j++) {
pc.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer-" + i).start();
}
}
}
7. AQS:锁的底层框架,一文看懂
AbstractQueuedSynchronizer(AQS)是 ReentrantLock、CountDownLatch、Semaphore 等并发工具的底层骨架,搞懂它,就搞懂了 Java 并发的核心。
7.1 AQS 的核心思想
AQS 维护了两个关键字段:
// AQS 核心字段(简化版)
public abstract class AbstractQueuedSynchronizer {
// 同步状态:0=未锁定,>0=已锁定(值等于重入次数)
private volatile int state;
// CLH 队列的头节点和尾节点
private transient volatile Node head;
private transient volatile Node tail;
// 队列节点
static final class Node {
volatile int waitStatus; // 等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 关联的线程
}
}
7.2 AQS 的 CLH 队列结构
head(哨兵节点)
↓
Node(Thread-1) ← Node(Thread-2) ← Node(Thread-3) ← tail
[持有锁] [等待] [等待]
7.3 AQS 加锁流程(以非公平锁为例)
// 以下是 ReentrantLock 非公平锁的加锁逻辑(简化注释版)
final void lock() {
// 第一步:直接 CAS 尝试将 state 从 0 改为 1
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread()); // 成功,设置当前线程为锁持有者
} else {
acquire(1); // 失败,走完整的获取流程
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 再次尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { // 失败则入队等待
selfInterrupt();
}
}
// 非公平锁的 tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁空闲
if (compareAndSetState(0, acquires)) { // 直接抢(不管队列里有没有等待者)
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 可重入
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
7.4 手写一个简单的独占锁(基于 AQS)
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 自定义独占锁:基于 AQS 实现
* state = 0:未锁定
* state = 1:已锁定
*/
public class MySimpleLock implements Lock {
// 内部同步器(继承 AQS)
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
// 尝试加锁:CAS 将 state 从 0 改为 1
@Override
protected boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁:将 state 从 1 改为 0
@Override
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 是否被当前线程持有
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
sync.acquire(1); // AQS 模板方法,会调用我们重写的 tryAcquire
}
@Override
public void unlock() {
sync.release(1); // AQS 模板方法,会调用我们重写的 tryRelease
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
// 测试
public static void main(String[] args) throws InterruptedException {
MySimpleLock lock = new MySimpleLock();
int[] count = {0};
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
lock.lock();
try {
count[0]++;
} finally {
lock.unlock();
}
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("结果: " + count[0]); // 10000
}
}
8. 读写锁 ReentrantReadWriteLock:读多写少的最佳方案
在实际业务中,很多场景是读多写少的(如缓存、配置读取)。此时如果对读操作也加独占锁,性能会非常差。
8.1 读写锁的规则
读锁(共享锁):多个线程可以同时持有读锁
写锁(独占锁):只有一个线程可以持有写锁,持有写锁时其他人不能读也不能写
情况 是否允许
读锁 + 读锁 ✅ 允许(读读不互斥)
读锁 + 写锁 ❌ 不允许
写锁 + 读锁 ❌ 不允许
写锁 + 写锁 ❌ 不允许
8.2 代码实现
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 使用读写锁实现线程安全的缓存
*/
public class ReadWriteCache {
private final Map<String, Object> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final java.util.concurrent.locks.Lock readLock = rwLock.readLock();
private final java.util.concurrent.locks.Lock writeLock = rwLock.writeLock();
// 读操作:多线程可并发执行
public Object get(String key) {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 读取 " + key);
// 模拟耗时操作
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写操作:独占,同时只能一个线程执行
public void put(String key, Object value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 写入 " + key);
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteCache cache = new ReadWriteCache();
cache.put("init", "value0"); // 初始化
// 5 个读线程 + 2 个写线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 3; j++) {
cache.get("init");
}
}, "Reader-" + i).start();
}
for (int i = 0; i < 2; i++) {
final int id = i;
new Thread(() -> {
cache.put("key" + id, "value" + id);
}, "Writer-" + i).start();
}
}
}
9. 死锁:如何产生与如何避免
9.1 经典死锁示例
public class DeadLockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
// 线程1:先锁A,再锁B
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("线程1 获得锁A");
try { Thread.sleep(100); } catch (InterruptedException e) { }
System.out.println("线程1 尝试获取锁B...");
synchronized (LOCK_B) { // ← 阻塞!线程2持有锁B
System.out.println("线程1 获得锁B");
}
}
});
// 线程2:先锁B,再锁A
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("线程2 获得锁B");
try { Thread.sleep(100); } catch (InterruptedException e) { }
System.out.println("线程2 尝试获取锁A...");
synchronized (LOCK_A) { // ← 阻塞!线程1持有锁A
System.out.println("线程2 获得锁A");
}
}
});
t1.start();
t2.start();
// 两个线程互相等待,程序永远卡住
}
}
9.2 死锁的四个必要条件
| 条件 | 说明 |
|---|---|
| 互斥 | 资源一次只能被一个线程占用 |
| 不可剥夺 | 线程已获得的资源不能被强行剥夺 |
| 请求与保持 | 线程在持有资源的同时,还在等待其他资源 |
| 循环等待 | 线程之间形成环形等待链 |
9.3 解决方案
// 方案一:按固定顺序加锁(破坏循环等待条件)
public class DeadLockSolution1 {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
// 线程1和线程2都按 A→B 的顺序加锁
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
synchronized (LOCK_B) {
System.out.println("线程1 执行");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK_A) { // 先竞争 A,不再出现循环等待
synchronized (LOCK_B) {
System.out.println("线程2 执行");
}
}
});
t1.start();
t2.start();
}
}
// 方案二:使用 tryLock 超时获取(破坏不可剥夺条件)
public class DeadLockSolution2 {
private static final ReentrantLock LOCK_A = new ReentrantLock();
private static final ReentrantLock LOCK_B = new ReentrantLock();
public static void transfer(ReentrantLock from, ReentrantLock to)
throws InterruptedException {
while (true) {
if (from.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (to.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 执行转账");
return;
} finally {
to.unlock();
}
}
} finally {
from.unlock();
}
}
// 获取锁失败,随机等待一段时间后重试(避免活锁)
Thread.sleep((long) (Math.random() * 10));
}
}
}
10. 实战:手写一个线程安全的 LRU 缓存
综合运用本文的知识,实现一个线程安全的 LRU(最近最少使用)缓存:
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 线程安全的 LRU 缓存
* - 使用 LinkedHashMap 实现 LRU 逻辑
* - 使用 ReentrantReadWriteLock 保证线程安全
*/
public class ThreadSafeLRUCache<K, V> {
private final int capacity;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// accessOrder=true:按访问顺序排列(LRU 的关键)
private final LinkedHashMap<K, V> cache;
public ThreadSafeLRUCache(int capacity) {
this.capacity = capacity;
this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
// 当缓存满时,自动移除最久未使用的条目
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
}
public V get(K key) {
// 注意:get 操作会更新访问顺序,LinkedHashMap 不是线程安全的
// 所以 get 也要用写锁(因为会修改内部链表结构)
writeLock.lock();
try {
return cache.getOrDefault(key, null);
} finally {
writeLock.unlock();
}
}
public void put(K key, V value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
public int size() {
readLock.lock();
try {
return cache.size();
} finally {
readLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadSafeLRUCache<Integer, String> lru = new ThreadSafeLRUCache<>(3);
// 测试基本 LRU 功能
lru.put(1, "one");
lru.put(2, "two");
lru.put(3, "three");
System.out.println("get(1): " + lru.get(1)); // 访问1,1变成最近使用
lru.put(4, "four"); // 容量满,淘汰最久未使用的(此时是2)
System.out.println("get(2): " + lru.get(2)); // null,2已被淘汰
// 测试并发安全性
ThreadSafeLRUCache<Integer, Integer> concurrentCache = new ThreadSafeLRUCache<>(100);
Thread[] threads = new Thread[20];
for (int i = 0; i < 20; i++) {
final int id = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < 50; j++) {
concurrentCache.put(id * 50 + j, id * 50 + j);
concurrentCache.get((int)(Math.random() * 100));
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("并发测试完成,缓存大小: " + concurrentCache.size()); // ≤ 100
}
}
11. 总结:如何选择合适的锁?
场景 推荐方案
────────────────────────────────────────────────────────
简单的计数器、整数运算 → AtomicInteger(无锁 CAS)
简单开关标志位(一写多读) → volatile
代码块/方法互斥(简单场景) → synchronized(JDK6+ 已优化,够用)
需要超时/可中断/公平性控制 → ReentrantLock
读多写少(缓存、配置) → ReentrantReadWriteLock
等待多个任务完成 → CountDownLatch
控制并发数量(连接池、限流) → Semaphore
多个线程到达后再一起出发 → CyclicBarrier
核心知识点回顾
- 竞态条件:多线程对共享变量的非原子操作导致数据不一致
- synchronized:基于 Monitor 实现,支持可重入;JDK6 后引入锁升级(偏向锁→轻量级锁→重量级锁)
- volatile:保证可见性和禁止指令重排,但不保证原子性
- ReentrantLock:基于 AQS 实现,比 synchronized 更灵活,支持超时、中断、公平锁
- AQS:Java 并发的骨架,通过 CAS + CLH 队列实现高效的锁竞争
- 读写锁:读读不互斥,适合读多写少场景
- 死锁:四个必要条件,解决方案:固定锁顺序或 tryLock 超时
参考资料
- 《Java 并发编程实战》— Brian Goetz
- 《深入理解 Java 虚拟机(第3版)》— 周志明
- JDK 源码 - java.util.concurrent.locks
- 掘金:深入分析 synchronized 原理
如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ 评论 💬,你的支持是我持续创作的最大动力!
有任何疑问或者文章有误,欢迎在评论区指出,互相学习进步!