Lock 中的 AQS、独占锁、重入锁、读锁、写锁、Condition 源码原理分析

2,983 阅读5分钟

前言

除去使用 synchronized 隐式加锁的方式外,我们可以使用 Lock 更加灵活的控制加锁、解锁、等待和唤醒等操作

Java 中的 Lock 有如下几种实现

  • 重入锁(ReentrantLock)
  • 读写锁(ReentrantReadWriteLock)
  • 等等

不论是重入锁还是读写锁,他们都是通过 AQS(AbstractQueuedSynchronizer)来实现的,并发包作者(Doug Lea)期望 AQS 作为一个构建所或者实现其它自定义同步组件的基础框架

理解 AQS 对理解锁来说至关重要,我们先来利用 AQS 来实现一个自定义的同步组件独占锁

独占锁 Mutex

public class Mutex implements Lock {

    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    private static class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }
}

整体调用逻辑如下

加锁

调用 lock() 的时候,首先会去调用 acquire() 方法

如果 tryAcquire() 获取锁成功这里使用的是 CAS 获取锁并且直接返回成功或者失败,如果获取成功了方法返回,线程持有锁成功,否则就会调用 addWaiter() 将当前线程构造成功 Waiter 结点,放入同步队列中,同步队列是一个 FIFO 的队列,所以说新来的结点要放入尾部

然后调用 acquireQueued() 方法尝试将该节点挂起或者从队列中取出首结点然后再尝试调用 tryAcquire() 获取同步状态,如果获取成功了那么从同步队列中移除,如下图 2,如果不是头结点这线程挂起进入等待状态,直到线程被中断或者被唤醒为止再次进行头结点和获取锁的逻辑,需要注意的是每次判断的是当前节点的前一个节点,后续方便 setHead() 设置当前节点

解锁 unlock()

解锁过程

调用 tryRelease(),这个方法将 state 设置为 0 并且将锁所属于的线程置位 null,这里没有使用 CAS 因为释放锁的时候,线程是获取到锁的状态不会存在竞争

以上对 AQS 的部分功能接口做了讲解,后面结合其它锁会逐步展开剩下的点

重入锁 ReentrantLock

ReentrantLock 有两种实现分别是

  • 公平锁
  • 非公平锁

公平锁

在创建 ReentrantLock(true) 就可以创建公平锁的实现,在其内部指定了内部的 AQS 使用 FairSync

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
获取锁逻辑

上述逻辑都是整理源码罗列出来的逻辑和我们之前的独占锁的区别主要在于,同一个线程可以多次获取锁 state 会依次增加代表了重入的次数。

释放锁逻辑

释放锁的时候必须是当前线程,减少锁持有的次数为 0 的话就完全释放,将锁所属于的线程设置为 null

非公平锁

非公平锁比公平锁的实现简单了许多而且性能也好了很多,区别主要在于

  • 加锁的时候非公平锁直接去设置同步状态,而公平锁必须是头部结点

其它基本都是相似的

我们知道 AQS 是一个 FIFO 队列,从中唤醒的都是有先后顺序的,同时在这种情况下又多了操作就是由于线程是可以重入的,那么在这种情况下,同一个线程可能会连续的持有锁,导致处于等待中的队列等待更久,比如如下代码每个线程获取锁 2 次,启动了多个任务

读写锁

读写锁用一个值维护读和写锁,用高 16 位表示读状态,用低 16 位表示写状态

读锁

  • 第一处,获取写锁状态如果不为 0,再次判断是否是当前线程,如果不是则返回获取失败
  • 第二,三处,获取读锁的值,并且增加重入次数,并且统计每个线程锁的重入次数

写锁

  • 第一处,如果 state 不为 0 表示锁对象被线程持有
    • 第二处,只要存在读锁返回获取锁失败放入同步队列中,如果写线程数量超过了最大值抛出异常
    • 第三处,设置重入次数
  • 第四处,如果 state 为 0 表示初次获取锁,CAS 抢锁,成功则返回失败则放入同步队列中
  • 第五处,设置锁对象被当前线程持有

总结一下:

  • 读锁和写锁都是可重入锁
  • 可以支持锁降级
    • 如果一个线程持有了写锁则可以再次持有读锁然后释放写锁
  • 如果一个线程持有了读锁,则不能当前线程和其它线程都不能再持有写锁

Condition

一个锁可以持有一个同步队列,多个 Condition,每个 Condition 上存在多个等待结点

当调用 await() 的时候就会构造对应的结点,首先从同步队列头部移除结点,然后释放锁,然后放入 Condition 列表尾部

  • 第一处,将当前节点构造成 waiter 节点放入 Condition 等待队列尾部
  • 第二处,释放当前线程同步状态,然后唤醒同步队列后继头结点
  • 第三处,当前线程挂起等待被唤醒或者中断返回

当调用 singal() 的时候就会从 Condition 等待队列中移除放入同步队列的尾部

  • 第一处,判断必须释放当前线程持有的锁
  • 第二处,唤醒 Condition 第一个等待的结点,将其断开加入到同步队列的尾部,并且调用 unpark 将其唤醒

参考:

  • 并发编程的艺术