目录

一、与synchronized区别

二、ReentrantLock 的核心特性

三、ReentrantLock 整体架构

四、可重入锁实现原理

五、公平锁与非公平锁实现原理

六、加锁与解锁流程

七、ReentrantLock 与 synchronized 对比

一、与synchronized区别

在 Java 并发编程中,最常见的加锁方式莫过于 synchronized

public synchronized void add() {
    count++;
}

synchronized 使用简单,并且由 JVM 保证锁的获取与释放,是 Java 中最基础的线程同步机制

不过随着并发场景越来越复杂,synchronized 的局限性也逐渐显现出来:

  • 无法尝试获取锁

  • 无法设置获取锁超时时间

  • 无法中断等待锁的线程

  • 不支持公平锁

  • 只能使用单一条件队列

为了解决这些问题,JDK 在 java.util.concurrent.locks 包中提供了 Lock 接口,并实现了最常用的显式锁 —— ReentrantLock

Lock lock = new ReentrantLock();

从功能上来说,ReentrantLock 与 synchronized 都属于独占锁,但它提供了更加灵活的锁控制能力

二、ReentrantLock 的核心特性

1、可重入

同一个线程获取锁后,可以再次获取同一把锁而不会发生死锁

例如:

public void methodA() {
    lock.lock();
    try {
        methodB();
    } finally {
        lock.unlock();
    }
}

public void methodB() {
    lock.lock();
    try {

    } finally {
        lock.unlock();
    }
}

methodA 获取锁后调用 methodB

如果锁不支持重入,那么 methodB 会再次申请同一把锁,从而陷入永久等待

而 ReentrantLock 允许当前持有锁的线程重复获取锁,因此不会发生死锁

2、公平锁与非公平锁

ReentrantLock 提供两种模式:

默认使用非公平锁

new ReentrantLock();

使用公平锁

new ReentrantLock(true);

公平锁要求线程按照等待顺序获取锁:

Thread-A
    ↓
Thread-B
    ↓
Thread-C

A 释放锁后,必须由 B 获取

而非公平锁允许线程插队:

A释放锁

新线程D直接获得锁

B继续等待

虽然非公平锁可能导致部分线程等待时间较长,但由于减少了线程切换,因此具有更高的吞吐量

这也是 ReentrantLock 默认采用非公平策略的原因

3、尝试获取锁

synchronized 获取不到锁时只能阻塞等待

而 ReentrantLock 提供了 tryLock:

if (lock.tryLock()) {
    try {

    } finally {
        lock.unlock();
    }
}

获取成功返回 true,获取失败立即返回 false,不会进入阻塞状态

4、可中断获取锁

普通 lock() 在等待期间无法响应中断

lock.lock();

而 lockInterruptibly() 可以:

lock.lockInterruptibly();

如果线程在等待锁期间收到中断信号:

thread.interrupt();

会立即抛出 InterruptedException,这在死锁检测、超时控制等场景中非常有用

5、Condition 条件队列

synchronized 配套的是:

wait()
notify()
notifyAll()

而 ReentrantLock 提供:

Condition condition = lock.newCondition();

配合:

condition.await();
condition.signal();
condition.signalAll();

实现线程通信

并且一个 ReentrantLock 可以创建多个 Condition,实现更精细的线程协作控制

三、ReentrantLock 整体架构

ReentrantLock 的实现并不复杂

核心结构如下:

      ReentrantLock
             │
            ▼ 
          Sync
             │
┌────┴────┐
▼                     ▼
FairSync NonfairSync 
            │
           ▼
         AQS

源码:

public class ReentrantLock implements Lock {

    private final Sync sync;

}

其中:

abstract static class Sync
        extends AbstractQueuedSynchronizer {
}

可以看到:

ReentrantLock 本身并不负责具体的同步逻辑

真正的加锁、解锁、线程排队等操作,全部由 AQS 完成

ReentrantLock 只是对 AQS 的一次具体实现

四、可重入锁实现原理

可重入的核心在于两个变量:

private volatile int state;

以及:

private transient Thread exclusiveOwnerThread;

其中:

state 表示锁的重入次数

exclusiveOwnerThread 表示当前持有锁的线程

第一次获取锁

lock.lock();

执行后:

state = 1

owner = Thread-A

第二次获取锁

lock.lock();

发现:owner == 当前线程,因此无需竞争锁

直接:state++

变成:state = 2

第三次获取锁

state = 3

依此类推

释放锁

lock.unlock();

本质上执行:

state--

例如:

state = 3

第一次:state = 2

第二次:state = 1

第三次:state = 0

只有 state 变成 0 时,锁才真正释放

因此:

lock几次

必须unlock几次

否则会导致其他线程永远无法获得锁

五、公平锁与非公平锁实现原理

ReentrantLock 内部存在两个实现:

FairSync

NonfairSync

非公平锁

默认使用:

new ReentrantLock();

其加锁逻辑非常直接:

if (compareAndSetState(0, 1)) {
    setExclusiveOwnerThread(current);
}

只要发现锁空闲,立即 CAS 抢占

即使同步队列中已经有等待线程,也允许直接插队

因此性能较高

公平锁

new ReentrantLock(true);

获取锁前会先判断:

hasQueuedPredecessors()

检查自己前面是否存在等待线程

如果存在:必须排队

不能直接获取锁

因此实现了严格的 FIFO 获取顺序

六、加锁与解锁流程

加锁流程

lock()
   │
  ▼
tryAcquire()
   │
  ▼
CAS(state)
   │
   ├──成功
   │
   └──失败
         │
        ▼
      进入AQS队列

如果锁空闲:

state=0 → 1
直接获取成功

否则进入同步队列等待

解锁流程

unlock()
    │
   ▼
tryRelease()
    │
   ▼
state--
    │
   ▼
state==0
    │
   ▼
唤醒后继节点

当重入次数归零时:

exclusiveOwnerThread = null;

锁被真正释放

随后唤醒同步队列中的下一个线程继续竞争锁

七、ReentrantLock 与 synchronized 对比

对比项 synchronized ReentrantLock
实现方式 Monitor AQS
可重入
公平锁 ×
tryLock ×
超时获取锁 ×
可中断获取锁 ×
多条件队列 ×
自动释放锁 ×

从功能角度来看:

ReentrantLock 提供了比 synchronized 更丰富的并发控制能力

从性能角度来看:

JDK 1.6 之后 synchronized 引入偏向锁、轻量级锁等优化,两者性能差距已经非常小

因此:

  • 简单同步场景优先使用 synchronized

  • 复杂并发控制场景优先使用 ReentrantLock

更多推荐