JUC源码系列之ReentrantLock源码解析

目录

  • ReentrantLock 简介
  • ReentrantLock 使用示例
  • ReentrantLock 与 synchronized 的区别
  • ReentrantLock 实现原理
  • ReentrantLock 源码解析

ReentrantLock 简介

ReentrantLock 是 JDK 提供的一个可重入的独占锁,

  • 独占锁:同一时间只有一个线程可以持有锁
  • 可重入:持有锁的线程可重复加锁,意味着第一次成功加锁时,需要记录锁的持有者为当前线程,后续再次加锁时,可以识别当前线程。

ReentrantLock 提供了公平锁以及非公平锁两种模式,要解释这两种模式不异同,得先了解一下 ReentrantLock 的加锁流程,ReentrantLock 基于 AQS 同步器实现加解锁,基本的实现流程为:

线程 A、B、C 同时执行加锁,加锁是通过CAS操作完成,CAS 是原子操作,可以保证同一时间只有一个线程加锁成功,假设线程 A 加锁成功,则线程 B、C 进入 AQS 等待队列并被挂起,假设 B 在前,C 在后,当线程 A 释放锁时,会唤醒排在等待队列队首的线程 B,该线程会尝试通过 CAS 进行获取锁。如果线程 B 尝试加锁的同时,有线程 D 也同时进行加锁,如果线程 D 与 线程 B 竞争加锁,则为非公平锁,线程 D 加入等待队列排在线程 C 之后,则为公平锁。

  • 非公平锁:加锁时会与等待队列中的头节点进行竞争。
  • 公平锁:加锁时首先判断等待队列中是否有线程在排队,如果没有则参与竞争锁,如果有则排队等待。

所谓公平就是,大家一起到,就竞争上岗,如果已经有人在排队了,那就先来后到。

使用示例

使用伪代码表示

class X {

    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();  // block until condition holds
        try {
            // ... method body
        } finally {
            lock.unlock();
        }
    }
}

默认实现的是非公平锁,如果要使用公平锁,只需要创建 ReentrantLock 对象时传递入参 true 即可,使用方法与非公平锁一样。

private final ReentrantLock lock = new ReentrantLock(true);

condition 使用示例:

class X {

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void poll() {
        lock.lock();  // block until condition holds
        try {
            while(条件判断表达式) {
                condition.wait();
            }
        } finally {
            lock.unlock();
        }
    }
    
    public void push() {
        condition.signal();
    }
}

ReentrantLock 与 synchronized 的区别

ReentrantLock 提供了 synchronized 类似的功能和内存语义。

相同点

  • ReentrantLock 与 synchronized 都是独占锁,可以让程序正确同步。
  • ReentrantLock 与 synchronized 都可重入锁,可以在循环中使用 synchronized 进行加锁并不用担心解锁问题,但 ReentrantLock 则必须要进行与加锁相同次数的解锁操作,不然可能导致没有真正解锁成功。

不同点

  • synchronized 是JDK提供的语法,加锁解锁的过程是隐式的,用户不用手动操作,操作简单,但不够灵活。
  • ReentrantLock 需要手动加锁解锁,且解锁次数必须与加锁次数一样,才能保证正确释放锁,操作较为复杂,但是因为是手动操作,所以可以应付复杂的并发场景。
  • ReentrantLock 可以实现公平锁
  • ReentrantLock 可以响应中断,使用 lockInterruptibly 方法进行加锁,可以在加锁过程中响应中断,synchronized 不能响应中断
  • ReentrantLock 可以实现快速失败,使用 tryLock 方法进行加锁,如果不能加锁成功,会立即返回 false,而 synchronized 是阻塞式。
  • ReentrantLock 可以结合 Condition 实现条件机制。

可以看到,ReentrantLock 与 synchronized 都是实现线程同步加锁,但 ReentrantLock 比起 synchronized 要灵活很多。


实现原理

ReentrantLock 使用组合的方式,通过继承 AQS 同步器实现线程同步。通过控制 AQS 同步器的同步状态 state 达到加锁解锁的效果,该状态默认为 0,代表锁未被占用,加锁则是通过 cas 操作将其设置为 1,cas 是原子性操作,可以保证同一时间只有一个线程可以加锁成功,同一个线程可以重复加锁,每次加锁同步状态自增 1,释放锁的过程就是将同步状态自减,减到 0 时才算完全释放,这也解释了为什么释放锁的次数必须与加锁次数一样的问题,因为只有次数一样才能将同步状态减至 0,这样其它线程才能进行加锁。

源码分析

Lock 接口

ReentrantLock 实现了 Lock 接口,这是 JDK 提供的所有 JVM 锁的基类。

public interface Lock {
    // 阻塞式加锁
    void lock();
    // 阻塞式加锁,但可以响应中断,加锁过程中线程中断,抛出 InterruptedException 异常
    void lockInterruptibly() throws InterruptedException;
    // 快速失败加锁,只尝试一次,
    boolean tryLock();
    // 阻塞式加锁,可以响应中断并且实现超时失败
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 释放锁
    void unlock();
    // 实现条件
    Condition newCondition();
}

通过代码可以看到,ReentrantLock 的内部实现都是通过 Sync 这个类实现,可以认为遵守组合设计原则,Sync 是 ReentrantLock 的内部类。这里的方法调用,并没有区分是公平锁还是非公平锁,而是无差别地调用,所以区别一定在 Sync 这个类的实现中。

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    public void lock() {
        sync.lock();
    }
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    public void unlock() {
        sync.release(1);
    }
    public Condition newCondition() {
        return sync.newCondition();
    }
}

Sync 类继承了 AQS 同步器,通过同步器实现线程同步,因为是独占锁,所以最重要的就是实现 tryAcquiretryRelease 两个方法,Sync 类是一个 abstract 类,它拥有两个实现类 FairSyncNonfairSync,通过名字应该就可分辨他们就是公平锁与非公平锁。

abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();
    // 非公平加锁
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) { // 如果没有线程执行锁
            if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁
                setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁
            int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    // 释放锁
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases; // 将同步状态进行自减,acquires 的传值为 1
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) { // 当同步状态减成 0 时,代表完全释放锁,将锁的拥有者置空
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
    // ...省略其它...
}

所以公平锁与非公平锁的玄机就在 ReentrantLock 的构造方法中,默认的无参构造方法创建非公平锁,如果传参 true,则创建公平锁。而这两个锁都是 Sync 的子类,使用了不同的实现策略,可以认为使用了策略模式。

public class ReentrantLock implements Lock, java.io.Serializable {
    // 默认创建非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    // 如果为 true,则创建公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

接下来分别看一下 FairSyncNonfairSync 是如何实现公平锁与非公平锁的,首先分析非公平锁

static final class NonfairSync extends Sync {
    // 阻塞式加锁
    final void lock() {
        // 首先尝试竞争加锁,如果成功则设置当前线程为锁的拥有者
        if (compareAndSetState(0, 1)) 
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1); // 使用 AQS 排队
    }
    // 尝试加锁
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

阻塞式加锁调用 ReentrantLock 的 lock() 方法,该方法调用 sync.lock() 执行加锁,非公平锁也就是调用 NonfairSync 类的 lock 方法,该方法首先尝试竞争加锁,此时有三种情况:

  • 此时锁没有人持有,竞争成功,直接设置当前线程为锁的拥有者并返回
  • 此时锁没有人持有,竞争失败,走 AQS 加锁流程
  • 此时锁被其它线程拥有,走 AQS 加锁流程
  • 此时锁被自己拥有,竞争失败,走 AQS 加锁流程

AQS 加锁流程就是调用 tryAcquire 方法尝试加锁,如果成功则返回加锁成功,如果失败则进入等待队列并挂起,等待锁的持有者释放锁时唤醒等待队列中的线程,并再次尝试加锁,如此反复,直到加锁成功。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

AQS 加锁流程在 AQS 中已经提供了完整实现模板,不需要去了解底层就可以使用,需要做的就是自行实现 tryAcquire 方法,NonfairSync 的 tryAcquire 方法这里再贴一次实现代码。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { // 如果没有线程执行锁
        if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁
            setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁
        int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

接下来看一下 FairSync 的实现

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) { // 如果锁没有人持有
            // 首先判断队列是否为空,如果为空则竞争锁,如果不为空则返回尝试失败,线程会被加入等待队列
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程持有,与非公平锁同样处理
            int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

释放锁的过程,公平锁与非公平锁是一样的,前面的代码中已经解释过了,这里就不再多说了。

举报
评论 0