JUC-AQS&ReentrantLock

22 阅读7分钟

一、AQS 原理

全称是 AbstractQueuedSynchronizer,它是一个抽象的父类,是阻塞式锁和相关的同步器工具的框架

1.1 特点

  • state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁:
    • getState() - 获取 state 状态。
    • setState() - 设置 state 状态。
    • compareAndSetState() - cas 机制设置 state 状态。
    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源。
  • 提供了基于 FIFO 的等待队列,类似于 MonitorEntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 MonitorWaitSet

1.2 使用

  • 子类主要实现这样一些方法(默认抛出 UnsupportedOperationException

    • tryAcquire()
    • tryRelease()
    • tryAcquireShared()
    • tryReleaseShared()
    • isHeldExclusively()
  • 获取锁的姿势

// 如果获取锁失败
if (!tryAcquire(arg)) {
    // 入队, 可以选择阻塞当前线程 park unpark
}
  • 释放锁的姿势
// 如果释放锁成功
if (tryRelease(arg)) {
    // 让阻塞线程恢复运行
}

1.3 自定义不可重入锁

  • 代码示例
@Slf4j
public class SyncTests {

    /**
     * 不可重入测试(与其他线程)。
     */
    @Test
    public void test01() throws InterruptedException {

        MyLock lock = new MyLock();

        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                log.debug("t1 locking...");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            } finally {
                log.debug("t1 unlock");
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                log.debug("t2 locking...");
            } finally {
                log.debug("t2 unlock");
                lock.unlock();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // t1 locking...
        // t1 unlock
        // t2 locking...
        // t2 unlock
    }

    /**
     * 不可重入测试(自己也会被挡住)。
     * 表现:只会打印一次 locking。
     */
    @Test
    public void test02() {

        MyLock lock = new MyLock();

        new Thread(() -> {
            lock.lock();
            log.debug("[1] locking...");
            lock.lock();
            log.debug("[2] locking...");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            } finally {
                log.debug("unlock");
                lock.unlock();
            }
        }).start();
        // [1] locking...
    }
}

/**
 * 一、自定义『不可重入锁』。
 */
class MyLock implements Lock {

    /**
     * 二、自定义『同步器』。
     */
    private static class MySync extends AbstractQueuedSynchronizer {

        /**
         * 尝试获取锁。
         *
         * @param arg 参数
         * @return boolean
         */
        @Override
        protected boolean tryAcquire(int arg) {
            // 0:表示未上锁;1:表示上锁。
            if (compareAndSetState(0, 1)) {
                // 设置当前线程为 owner。
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 尝试释放锁,只更改状态,但不会唤醒等待队列的其他线程。
         *
         * @param arg 参数
         * @return boolean
         */
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 1) {
                setExclusiveOwnerThread(null);
                // state 为 volatile 修饰。
                setState(0);
                return true;
            }
            return false;
        }

        /**
         * 是否持有独占锁。
         *
         * @return boolean
         */
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        /**
         * 变量条件。
         *
         * @return {@link Condition}
         */
        public Condition newCondition() {
            return new ConditionObject();
        }
    }

    /**
     * 持有同步器实例。
     */
    private final MySync sync = new MySync();

    /**
     * 尝试,不成功则进入等待队列。
     */
    @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();
    }
}

1.4 背景

  • 早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是在 JSR166java 规范提案)中创建了 AQS,提供了这种通用的同步器机制

1.5 功能目标

  • 阻塞版本获取锁 acquire() 和非阻塞的版本尝试获取锁 tryAcquire()
  • 获取锁超时机制。
  • 通过打断取消机制。
  • 独占机制及共享机制。
  • 条件不满足时的等待机制。

1.6 设计思想

AQS 的基本思想其实很简单。

  • 获取锁的逻辑
while(// state 状态不允许获取) {
    if(// 队列中还没有此线程) {
        // 入队并阻塞
    }
}
// 当前线程出队
  • 释放锁的逻辑
if(// state 状态允许了) {
    // 恢复阻塞的线程(s) 
}
  • 设计要点一:原子维护 state 状态

    • state 使用 volatile 配合 cas 保证其修改时的原子性。
    • state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想。
  • 设计要点二:阻塞及恢复线程

    • 早期的控制线程暂停和恢复的 apisuspend()resume(),但它们是不可用的,因为如果先调用的 resume() 那么 suspend() 将感知不到。
    • 解决方法是使用 park() & unpark() 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark()park() 也没问题。
    • park() & unpark() 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
    • park 线程还可以通过 interrupt() 打断。
  • 设计要点三:维护队列

    • 使用了 FIFO 先入先出队列,并不支持优先级队列。
    • 设计时借鉴了 CLH 队列,它是一种单向无锁队列。
    • CLH 好处:无锁,使用自旋;快速,无阻塞。
  • 示意图

队列中有 headtail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 state 维护节点状态入队伪代码,因此只需要考虑 tail 赋值的原子性。

  • 入队伪代码
do {
    // 原来的 tail
    Node prev = tail;
    // 用 cas 在原来 tail 的基础上改为 node
} while(tail.compareAndSet(prev, node))
  • 出队伪代码
// prev 是上一个节点
while((Node prev=node.prev).state != 唤醒状态) {
}
// 设置头节点
head = node;

二、ReentrantLock 原理

ReentrantLock可重入的互斥锁(悲观思想),虽然具有与 synchronized 相同功能,但是会比 synchronized 更加灵活,ReentrantLock底层基于AbstractQueuedSynchronizer 实现。

2.1 类继承关系

备注:参考源码为 JDK 1.8 版本。

2.2 非公平锁实现原理

大多数情况下会用到非公平锁,因此也是重点部分。

  • 构造器(默认为非公平锁实现)
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
    sync = new NonfairSync();
}

2.2.1 加锁解锁流程

  • 源码入口

  • 开始出现竞争

  • Thread-1 执行了进入时:

    1. CAS 尝试将 state 由 0 改为 1,结果失败;

    2. 进入 tryAcquire() 逻辑,这时 state 已经是1,结果仍然失败;

    3. 接下来进入 addWaiter() 逻辑,构造 Node 队列。

  • 进入 addWaiter() 逻辑

  • NodewaitStatus 状态,其中 0 为默认正常状态;

  • Node 的创建是懒惰的;

  • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程。

  • 当前线程进入 acquireQueued() 逻辑

  • acquireQueued() 会在一个死循环中不断尝试获得锁,失败后进入 park() 阻塞;

  • 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire() 尝试获取锁,当然这时 state 仍为 1,失败;

  • 进入 shouldParkAfterFailedAcquire() 逻辑,将前驱 node,即 headwaitStatus 改为 -1,这次返回 false

  • shouldParkAfterFailedAcquire() 执行完毕回到 acquireQueued() ,再次 tryAcquire() 尝试获取锁,当然这时 state 仍为 1,失败;

  • 当再次进入 shouldParkAfterFailedAcquire() 时,这时因为其前驱 nodewaitStatus 已经是 -1,这次返回 true

  • 进入 parkAndCheckInterrupt(), Thread-1 park()(灰色表示)。

  • 多线程竞争失败

  • 尝试释放锁

  • Thread-0 释放锁,进入 tryRelease() 流程,如果成功(设置 exclusiveOwnerThread()null,state = 0);

  • 当前队列不为 null,并且 headwaitStatus = -1,进入 unparkSuccessor() 流程;

  • 找到队列中离 head 最近的一个 Node(没取消的),unpark() 恢复其运行,本例中即为 Thread-1;

  • 回到 Thread-1 的 acquireQueued() 流程。

  • 发生非公平的竞争

  • 如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了;
  • 如果不巧又被 Thread-4 占了先:
    1. Thread-4 被设置为 exclusiveOwnerThreadstate = 1;
    2. Thread-1 再次进入 acquireQueued() 流程,获取锁失败,重新进入 park() 阻塞。

2.2.2 加锁源码

备注:参考源码为 JDK 1.8 版本。

  • 加锁源码入口

  • 尝试失败

  • 阻塞线程

  • 注意:是否需要 unpark() 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,而不是本节点的 waitStatus 决定。

2.2.3 解锁源码

备注:参考源码为 JDK 1.8 版本。

  • 源码关键部分解读

2.3 可重入原理

  • 源码关键部分解读

2.4 可打断原理

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。

  • 『不可打断模式』源码关键部分解读

  • 『可打断模式』源码关键部分解读

2.5 公平锁实现原理

  • 源码关键部分解读

2.6 条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

2.6.1 await 流程

  • 开始 Thread-0 持有锁,调用 await,进入 ConditionObjectaddConditionWaiter() 流程;
  • 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部;
  • 接下来进入 AQSfullyRelease() 流程,释放同步器上的锁。

  • unpark() AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功;
  • park() 阻塞 Thread-0。

2.6.2 signal 流程

  • 假设 Thread-1 要来唤醒 Thread-0:
  • 进入 ConditionObjectdoSignal() 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
  • 执行 transferForSignal() 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1;
  • Thread-1 释放锁,接下来进入 unlock() 流程。

2.6.3 源码

  • 关键方法及说明