Java 锁机制全解析:从 synchronized 到 AQS,彻底搞懂并发编程的底层原理

4 阅读18分钟

前言:并发编程是 Java 面试的高频考点,也是写出高质量代码的必备技能。很多同学在面试时能背出"synchronized 是悲观锁、ReentrantLock 是可重入锁",但问到底层原理就开始含糊。本文从最基础的线程安全问题出发,一步步深入 synchronized、volatile、ReentrantLock、AQS 的底层实现,配合完整可运行的代码,帮你真正搞懂 Java 并发中的锁。


目录

  1. 为什么需要锁?先看一个经典 Bug
  2. synchronized:最熟悉的陌生人
  3. volatile:轻量级可见性保证
  4. 深入 synchronized 底层:对象头与 Monitor
  5. 锁升级机制:偏向锁 → 轻量级锁 → 重量级锁
  6. ReentrantLock:比 synchronized 更灵活
  7. AQS:锁的底层框架,一文看懂
  8. 读写锁 ReentrantReadWriteLock:读多写少的最佳方案
  9. 死锁:如何产生与如何避免
  10. 实战:手写一个线程安全的 LRU 缓存
  11. 总结:如何选择合适的锁?

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(48字节)        │   │  ← 指向类元数据
│  └──────────────────────────────────┘   │
├─────────────────────────────────────────┤
│              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 更灵活

ReentrantLockjava.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 对比

特性synchronizedReentrantLock
实现方式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)是 ReentrantLockCountDownLatchSemaphore 等并发工具的底层骨架,搞懂它,就搞懂了 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

核心知识点回顾

  1. 竞态条件:多线程对共享变量的非原子操作导致数据不一致
  2. synchronized:基于 Monitor 实现,支持可重入;JDK6 后引入锁升级(偏向锁→轻量级锁→重量级锁)
  3. volatile:保证可见性和禁止指令重排,但不保证原子性
  4. ReentrantLock:基于 AQS 实现,比 synchronized 更灵活,支持超时、中断、公平锁
  5. AQS:Java 并发的骨架,通过 CAS + CLH 队列实现高效的锁竞争
  6. 读写锁:读读不互斥,适合读多写少场景
  7. 死锁:四个必要条件,解决方案:固定锁顺序或 tryLock 超时

参考资料


如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ 评论 💬,你的支持是我持续创作的最大动力!

有任何疑问或者文章有误,欢迎在评论区指出,互相学习进步!