前言

在Java并发编程中,除了大家熟知的synchronized关键字,java.util.concurrent.locks包还提供了一套强大的显式锁体系。如果说synchronized是自动挡汽车,简单易用但功能有限,那么ReentrantLock就是手动挡——你拥有完全控制权,但一旦忘了换挡(忘记解锁),车子就会熄火(死锁)。

JDK 5引入了java.util.concurrent.locks包,以Lock接口为核心,ReentrantLock为主要实现,底层依托AQS(抽象队列同步器)和CAS两大核心技术,实现了更灵活、高效的同步机制。

本文将从「使用场景→API细节→底层原理→源码解析→扩展锁→实战避坑」全方位拆解Java显式锁体系,帮你彻底掌握多种锁的使用方法,应对面试高频考点和实际开发难题。

一、Lock接口:JDK层面的锁规范

1.1 Lock接口的核心方法

方法核心作用使用场景注意事项
void lock()获取锁,若锁被占用则阻塞基础同步场景,无需中断、超时不可中断,若线程被阻塞只能等待锁释放
void lockInterruptibly()可响应中断地获取锁避免线程永久阻塞,如超时任务必须捕获InterruptedException
boolean tryLock()非阻塞尝试获取锁,立即返回无锁竞争时快速获取,避免阻塞获取失败不阻塞,需手动判断是否重试
boolean tryLock(long time, TimeUnit unit)超时获取锁防止线程无限等待,如网络请求时间单位需明确,支持中断
void unlock()释放锁,必须手动调用所有获取锁的场景必须在finally中调用,防止锁泄漏
Condition newCondition()创建条件变量,实现线程精准唤醒多条件等待/唤醒(如生产者-消费者模型)Condition的await()/signal()需在锁范围内调用

1.2 Lock相比synchronized的优势

特性synchronizedLock(ReentrantLock)
锁实现层面JVM内置,依赖对象头JDK层面,依赖AQS
释放方式自动释放手动释放,必须在finallyunlock()
可中断性不可中断支持lockInterruptibly()
超时获取不支持支持tryLock(timeout)
公平锁不支持可通过构造参数指定公平/非公平
条件等待单隐式等待队列(wait/notify)支持多个Condition对象,更灵活

二、ReentrantLock:可重入锁的典范

2.1 什么是“可重入”?

“Reentrant”(可重入)的意思是:如果一个线程已经持有这把锁,它可以再次获取同一把锁而不被阻塞。这对递归调用或类内部方法互相调用非常关键。

2.2 基本使用

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock();  // 加锁
        try {
            count++;  // 访问共享资源
        } finally {
            lock.unlock();  // 解锁——必须在finally中执行
        }
    }
}

⚠️ 铁律:永远用try-finally块,这是代码评审中的铁律。如果在持有锁的情况下发生异常,必须确保锁被正确释放,否则可能导致死锁。

2.3 构造器与公平性

// 默认非公平锁(性能更高)
ReentrantLock lock = new ReentrantLock();

// 指定公平锁(true=公平,false=非公平)
ReentrantLock fairLock = new ReentrantLock(true);

ReentrantLock有两个构造器,默认实现的是非公平锁,可以在构造函数参数中指定公平锁。

所谓锁是否公平,简单理解就是一系列线程获取到锁的顺序是否遵循「先来后到」。即,如果先申请锁的线程先获取到锁,就是公平锁;否则就是非公平锁。

  • 公平锁:严格按FIFO顺序分配锁,避免线程饥饿,但吞吐量较低
  • 非公平锁:允许插队,性能更高,但可能导致某些线程长时间等待

2.4 高级特性使用示例

tryLock:非阻塞式获取
public void tryDoSomething() {
    if (lock.tryLock()) {  // 立即返回,不会阻塞
        try {
            // 执行操作
        } finally {
            lock.unlock();
        }
    } else {
        // 锁被占用,执行备选方案
        System.out.println("锁被占用,稍后重试");
    }
}
tryLock + 超时机制
public boolean tryDoWithTimeout() {
    try {
        if (lock.tryLock(2, TimeUnit.SECONDS)) {  // 最多等待2秒
            try {
                // 执行操作
                return true;
            } finally {
                lock.unlock();
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    }
    return false;
}
lockInterruptibly:可响应中断的获取锁
public void doInterruptibly() throws InterruptedException {
    lock.lockInterruptibly();  // 等待过程中可被中断
    try {
        // 执行操作
    } finally {
        lock.unlock();
    }
}

2.5 可重入性演示

public class ReentrantDemo {
    private final Lock lock = new ReentrantLock();
    
    public void outerMethod() {
        lock.lock();
        try {
            System.out.println("外层方法持有锁");
            innerMethod();  // 再次获取同一把锁,不会死锁
        } finally {
            lock.unlock();
        }
    }
    
    public void innerMethod() {
        lock.lock();  // 同一线程第二次获取锁
        try {
            System.out.println("内层方法也持有锁");
        } finally {
            lock.unlock();
        }
    }
}

三、Condition:多条件队列的精准唤醒

3.1 为什么需要Condition?

传统的synchronized只能配合Object.wait()/notify()/notifyAll()使用,所有等待线程共享同一个隐式等待队列,notifyAll()会唤醒所有等待线程,造成“惊群效应”。

ReentrantLock通过newCondition()可以为同一把锁创建多个独立的Condition实例,每个Condition对应一个独立的等待队列。这让你能按不同业务逻辑或状态对线程进行分组等待和唤醒。

3.2 典型场景:生产者-消费者模式

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.LinkedList;
import java.util.Queue;

public class BoundedBuffer<T> {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 队列不满条件
    private final Condition notEmpty = lock.newCondition();  // 队列不空条件
    private final Queue<T> buffer = new LinkedList<>();
    private final int capacity;
    
    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }
    
    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (buffer.size() == capacity) {
                notFull.await();  // 队列满时,生产者等待
            }
            buffer.offer(item);
            notEmpty.signal();    // 唤醒等待的消费者
        } finally {
            lock.unlock();
        }
    }
    
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (buffer.isEmpty()) {
                notEmpty.await();  // 队列空时,消费者等待
            }
            T item = buffer.poll();
            notFull.signal();      // 唤醒等待的生产者
            return item;
        } finally {
            lock.unlock();
        }
    }
}

3.3 使用Condition的注意事项

  • 必须在持有锁时调用await()/signal(),否则抛出IllegalMonitorStateException
  • await()会自动释放锁,被唤醒后重新获取锁后才返回
  • await()必须在循环中调用,防止虚假唤醒
  • signal()唤醒一个等待线程,signalAll()唤醒全部

四、底层原理:AQS(AbstractQueuedSynchronizer)

4.1 AQS是什么?

AQS是Java提供的一个用于构建锁和同步器的框架,ReentrantLockSemaphoreCountDownLatch等并发工具都是基于AQS实现的。

AQS的核心由三部分组成:

  1. state属性:表示同步状态,使用volatile修饰并通过CAS更新
  2. FIFO等待队列(CLH队列改良版):管理等待获取锁的线程
  3. ConditionObject:支持多条件等待队列

4.2 state:同步状态

private volatile int state;

AQS使用一个volatileint类型成员变量state来表示同步状态,通过CAS完成对state值的修改来获得锁。

不同子类对state的语义定义不同:

同步器state的含义
ReentrantLock0=未加锁,1=加锁一次,>1=同一线程重入次数
ReentrantReadWriteLock高16位代表读锁状态,低16位代表写锁状态
Semaphore可用信号的个数
CountDownLatch计数器的值

4.3 CLH等待队列

当线程抢不到锁时,会被封装成一个Node节点加入FIFO队列。队列是双向链表,入队遵循FIFO原则,解锁时唤醒队头节点继续竞争。

// Node节点的关键字段
volatile int waitStatus;      // 节点状态
volatile Node prev;           // 前驱节点
volatile Node next;           // 后继节点
volatile Thread thread;       // 当前线程
Node nextWaiter;              // 条件队列中的后继节点

waitStatus的几个重要取值:

常量含义
CANCELLED1线程已取消(等待超时或被中断)
SIGNAL-1后继线程需要被唤醒
CONDITION-2节点在Condition等待队列中
PROPAGATE-3共享模式需传播
00初始状态

4.4 公平锁与非公平锁的实现差异

ReentrantLock通过内部抽象类Sync继承AQS,再由NonfairSyncFairSync两个子类分别实现公平和非公平策略。

非公平锁逻辑(默认)
线程请求锁时,不管有没有线程等待,先CAS抢一次锁;抢不到才入队排队等待。性能更高,因为减少了排队。

公平锁逻辑
线程请求锁时,检查队列头后面是否有节点;如果有,乖乖排队入队等待。唤醒时按照顺序依次获得锁。

4.5 可重入性的实现原理

ReentrantLock通过AQS的state字段记录持有锁的次数来实现可重入:

  • 第一次加锁state从0设为1
  • 重入加锁state递增(例如从1变成2)
  • 解锁state递减,仅当state归零时才真正释放锁

五、锁的演进:ReentrantReadWriteLock与StampedLock

5.1 ReentrantReadWriteLock:读锁共享,写锁独占

在“读多写少”的场景下(如商品目录API,每分钟万名用户查价格,管理员每天更新一次),如果用ReentrantLock,每个读操作都会互相阻塞,完全是资源浪费。

ReentrantReadWriteLock把锁拆成两种:

  • 读锁(共享锁):多个线程可以同时持有
  • 写锁(独占锁):只有一个人能写,且写的时候不能有人读
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteCache<K, V> {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
    private final java.util.Map<K, V> cache = new java.util.HashMap<>();
    
    public V get(K key) {
        readLock.lock();  // 读锁,多个线程可同时进入
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }
    
    public void put(K key, V value) {
        writeLock.lock();  // 写锁,独占
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

⚠️ 注意ReentrantReadWriteLock存在一个潜在问题——写锁饥饿。如果读线程持续不断地获取读锁,写线程可能会一直等待,无法获得写锁。

5.2 StampedLock:乐观读与性能之王

StampedLock是JDK 8引入的读写锁优化版本,它提供了三种锁模式:

模式特点适用场景
写锁(Write Lock)独占,任何时刻只允许一个线程持有数据修改操作
悲观读锁(Read Lock)共享,会阻塞写锁需要严格一致性的读操作
乐观读(Optimistic Read)无锁读取,不阻塞写锁读多写少、对数据一致性要求不苛刻的场景
乐观读的核心思想

乐观读允许线程在不获取任何锁的情况下读取共享资源。在乐观读期间,如果有写线程获取了写锁,乐观读就会失效。因此,乐观读需要在读取完成后进行验证,以确保数据的一致性。

import java.util.concurrent.locks.StampedLock;

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    
    // 写锁:独占写入
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    
    // 乐观读(推荐用于读多写少场景)
    public double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead();  // 获取乐观读锁(不实际阻塞)
        double currentX = x, currentY = y;
        
        if (!sl.validate(stamp)) {  // 验证:乐观读期间是否有写锁发生?
            // 写锁发生了,需要升级为悲观读锁
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    
    // 锁升级:从读锁升级为写锁
    public void moveIfAtOrigin(double newX, double newY) {
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

StampedLock的乐观读机制可以显著提高读多写少场景下的吞吐量,尤其适合缓存、配置数据等场景。

六、总结与选型建议

6.1 各锁类型对比

特性synchronizedReentrantLockReentrantReadWriteLockStampedLock
锁类型互斥锁互斥锁读锁共享/写锁独占写锁/读锁/乐观读
公平锁✅ 可选✅ 可选
可中断
超时获取
Condition❌ (用wait/notify)✅ 多Condition✅ 与ReentrantLock相同
乐观读
JDK版本1.0558

6.2 选型决策树

是否需要复杂功能(公平/中断/超时/多条件)?
├─ 不需要 → 使用 synchronized(简单,安全,自动释放)
└─ 需要 → 使用 ReentrantLock
    
是否是读多写少场景?
├─ 是 → 是否需要极致性能(乐观读)?
│   ├─ 是,且JDK 8+ → 使用 StampedLock(乐观读+读锁+写锁)
│   └─ 否 → 使用 ReentrantReadWriteLock
└─ 否 → 使用 ReentrantLock

6.3 最佳实践提醒

  1. unlock()必须在finally中调用,这是不可妥协的铁律
  2. await()调用必须在循环中进行条件判断,防止虚假唤醒
  3. 不要将lock对象声明为public,避免外部恶意调用
  4. ReentrantLock默认是非公平锁,没有特殊需求不要擅自改为公平锁,因为公平锁会降低吞吐量
  5. StampedLock不可重入,且不支持Condition,使用时需注意
  6. StampedLock的乐观读使用tryOptimisticRead(),不要与readLock()混淆
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐