JUC源码系统之AQS同步器源码解析

目录

  • 同步器简介
  • 方法简介
  • 同步器实现原理
  • 自己动手实现独占锁
  • 同步器源码分析

同步器简介

AQS 可以认为是一个模板方法,是 JDK 提供的实现 JVM 锁的模板,AQS 封装了同步机制,通过实现 AQS 可以让开发者比较简单就可以实现 JVM 锁,而不用去考虑底层的同步机制。降低了锁的实现难度及实现代价。

AQS 同步器提供了两套同步方案:独占式、共享式。也就是说要实现独占锁或者共享锁都可以通过继承 AQS 实现。

方法简介

独占式共享式方法介绍
acquire(int arg)acquireShared(int arg)阻塞式加锁
acquireInterruptibly(int arg)acquireSharedInterruptibly(int arg)在 acquire 的基础上响应中断
tryAcquireNanos(int arg, long nanosTimeout)tryAcquireSharedNanos(int arg, long nanosTimeout)在 acquireInterruptibly 的基础上增加了超时时间
release(int arg)releaseShared(int arg)解锁

可以看出 AQS 同步器为两套方案分别提供了两套方法,不带 Shared 的方法是独占式的,相应带 Shared 的方法就是共享式的。AQS 同步器提供了三种不同的加锁方式,用于适用不同的业务场景。

实现原理

同步状态

AQS 提供了一个同步状态属性 state,通过线程同步地设置同步状态实现加锁逻辑,让一个属性实现线程同步很简单,两个步骤:

  • 使用 volatile 修饰,保证线程可见性
  • 使用 Unsafe 类的 cas 方法修改属性值

volatile + cas 可以保证线程安全,当有多个线程需要修改 state 属性值时,只会有一个线程会操作成功,这就可以在JVM层面实现加锁操作。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    // 同步状态
    private volatile int state;
    // 获取同步状态
    protected final int getState() {
        return state;
    }
    // 设置同步状态
    protected final void setState(int newState) {
        state = newState;
    }
    // cas 设置同步状态
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

同步状态 state 是个 int 类型的数值,通过对数值的控制可以实现不同类型的锁。比如 ReentrantLock 就是将同步状态从 0 修改为 1 表示加锁来实现独占锁,而 Semaphore 是为同步状态设置一个初始值,同步状态大于 0 就可以加锁成功,然后同步状态减 1,这样实现多个线程同时加锁的共享锁。

多个线程同时加锁,但是只有一个线程会成功,那么加锁失败的线程怎么处理?AQS 提供了一套完整的基于 CLH 队列的解决方案,加锁失败后线程会进入 CLH 队列进行阻塞等待,AQS 的加锁流程如下图所示

Created with Raphaël 2.2.0 开始 尝试加锁 尝试成功? 结束 进入CLH队列 yes no
CLH 锁

CLH(Craig, Landin, and Hagersten locks): 是一种基于链表的可扩展、高性能、公平的自旋锁,能确保无饥饿性,提供先来先服务的公平性。

通过 CLH 的定义可以捕捉到几个重要的点

  • 基于链表
  • 自旋
  • 先进先服务

AQS 使用的是一个 FIFO 的双向链表来实现 CLH 队列,可以看到 AQS 中定义了一个 Node 类作为 CLH 队列中的节点,Node 类中定义了 prev 指向前一个节点,next 指向后一个节点,AQS 中定义了 headtail 分别表示 CLH 的头节点和尾节点,通过这个结构就可以从头节点或者尾节点遍历整个 CLH 队列。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    // 头节点
    private transient volatile Node head;
    // 尾节点
    private transient volatile Node tail;
    // 节点定义    
    static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        // 等待状态
        volatile int waitStatus;
        // 前驱节点
        volatile Node prev;
        // 后继节点
        volatile Node next;
        // 线程
        volatile Thread thread;
        // 标记用
        Node nextWaiter;
    }
}

CLH 队列处理流程

源码中提供了一个简易的图形表示,线程加锁失败后,进入 CLH 队列,成为队列的尾节点,优先头节点获取锁,然后按链表顺序依次获取锁。

      +------+  prev +-----+       +-----+
 head |      | <---- |     | <---- |     |  tail
      +------+       +-----+       +-----+

按 CLH 定义除头节点外,其它节点应该自旋等待,头节点释放锁后,则其后续节点就可以立刻获取锁。但是自旋是有代价的,AQS 使用 阻塞-唤醒 来替代自旋,其实还是自旋,只是自旋过程中会挂起线程,等待线程被唤醒后继续自旋。

独占锁与共享锁

AQS 提供了独占与共享两种模式,这两种模式的处理方案的主要区别为

  • 加锁,独占锁只有一个线程能加锁成功,而共享锁可以多个。
  • 释放锁,都需要唤醒 CLH 中的线程重新参与加锁,独占锁只需要唤醒一个节点即可,但共享锁需要唤醒多个。

接下来演示一下独占模式 AQS 实际的处理流程

第一步:CLH 队列原始状态,只有头节点拥有同步状态(标记绿色),其它节点都处于阻塞状态(标记红色)

在这里插入图片描述

第二步:当前线程尝试加锁失败,进入 CLH 队列尾部(标记黄色),排队等待获取同步状态

在这里插入图片描述

第三步:CLH 队列头节点(N0)释放同步状态,同时唤醒其后继节点(N1),N1 尝试加锁,如果成功,则将自己设置为头节点,N0 被踢出队列

在这里插入图片描述

重复第三步,直到当前线程节点获取到同步状态

在这里插入图片描述

公平锁与非公平锁

AQS 并没有提供公平锁与非公平锁的实现,但是基于 AQS 提供的方法可以实现公平锁与非公平锁,比如 ReentrantLock 就可以在创建的时候选择公平锁还是非公平锁。

所谓公平与非公平指的是尝试加锁的那个瞬间,当线程释放锁时,会唤醒 CLH 队列中的节点进行加锁,如果此时还有其它线程也需要加锁,那么就会存在竞争关系,公平锁的处理方案就是先来后到,非公平锁处理方案就是竞争,如果竞争不成功就老实排队。

自己动手实现独占锁

通过阅读上文的实现原理,可以知道 AQS 已经实现了 CLH 队列,实现一个锁只需要实现加锁及释放锁的逻辑即可,这个步骤可以通过对同步状态的设置进行实现。

实现思路:同步状态初始值为 0,加锁,将同步状态设置为 1,释放锁,将同步状态重置为 0,如此就可以实现一个最简单的独占锁。

// 实现 JDK 提供的锁接口
public class MutexLock implements Lock {
    // 实现 AQS,实现自定义的加锁及释放锁逻辑
    private static class Sync extends AbstractQueuedSynchronizer {
        // 加锁,cas 设置同步状态为 1
        @Override 
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, arg);
        }
        // 解锁,cas 设置同步状态为 0
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setState(0);
            return true;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }
    // 锁的实现委托给 AQS 的实现类
    private final Sync sync = new Sync();
    public void lock() { sync.acquire(1); }
    public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
    public boolean tryLock() { return sync.tryAcquire(1); }
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); }
    public void unlock() { sync.release(0); }
    public Condition newCondition() { return sync.newCondition(); }
}

源码分析

独占锁源码分析

首先看加锁方法 acquire,代码如下

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

这部分代码写的十分精简,扩展开来会比较清晰,这里涉及到几个重要的方法:

  • tryAcquire:尝试获取独占锁,需要实现类自行实现
  • addWaiter:将当前线程封装成 Node 节点,添加至 CLH 等待队列尾部
  • acquireQueued:在 CLH 队列中通过自旋的方式进行锁的获取
  • selfInterrupt:调用 Thread.currentThread().interrupt() 方法为当前线程设置个中断标记
public final void acquire(int arg) {
    // 尝试获取独占锁,如果成功立即返回
    boolean flag = tryAcquire(arg);
    // 如果加锁不成功
    if (!flag) {
        // 将当前线程加入等待队列尾部
        Node waiter = addWaiter(Node.EXCLUSIVE);
        // 通过自旋方式获取独占锁
        boolean interrupted = acquireQueued(waiter, arg);
        if (interrupted) {
            selfInterrupt();
        }
    }
}

tryAcquire 方法需要实现类自行实现加锁逻辑,通过操作同步状态就可以实现加锁

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

addWaiter 方法完成两件事件:

  • 将当前线程封装为 Node 对象
  • 将 Node 对象添加至等待队列尾部,为保证线程安全性,替换队列尾节点的操作通过 cas 完成。
private Node addWaiter(Node mode) {
    // 将当前线程封装成 Node 对象
    Node node = new Node(Thread.currentThread(), mode);
    // 快速尝试一次将 Node 对象添加至等待队列尾部,如果尝试失败则调用 enq 方法
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 将 Node 对象添加至等待队列尾部
    enq(node);
    return node;
}

enq 方法完成两件事件:

  • 如果 CLH 队列为空,则初始化队列,初始化时会在队列头设置一个空节点,通过这种方式保证唤醒操作,永远都是唤醒第二个节点。
  • 如果 CLH 队列不为这,则将当前线程节点添加至等待队尾。为保证线程安全性,替换队列头节点及尾节点的操作都是通过 cas 完成。

addWaiter 方法中快速尝试替换尾节点的操作跟 enq 方法中的代码其实是一模一样的,所以本人没看出来那部分代码有什么用,并不存在效率上的提升,这只是个人愚见,有不同意见可以探讨。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            // 如果尾部不存在,初始化等待队列,将队列头及尾设置为空 Node
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // 通过 cas 线程安全地将当前 Node 替换原来的尾节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

此至,当前线程的 Node 对象已添加至等待队列尾部,acquireQueued 方法则是在队列中通过自旋的方式获取锁,AQS 中的自旋,并不是无限制地自旋,而是通过 挂起-唤醒 机制消除自旋可能引起的 CPU 空耗。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点,则尝试获取锁
            if (p == head && tryAcquire(arg)) {
                // 加锁成功,则将当前节点设置为头节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 加锁失败,根据节点状态决定是否需要挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果出现异常,则取消加锁,并进行出队操作
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire 方法根据前驱节点的状态判断是否需要挂起当前线程节点

  • 如果前驱节点的状态为 SIGNAL,表示该节点释放锁时会唤醒其后继节点,所以可以安心挂起当前线程
  • 如果前驱节点的状态大于 0,即为 CANCELLED 已取消状态,则将当前节点前驱的所有已取消状态的节点都从队列中踢出
  • 如果前驱节点的状态小于等于 0,即为 CONDITION 或者 PROPAGATE,分别表示在等待条件或者是共享状态,则将前驱节点的状态通过 cas 设置为 SIGNAL,不需要挂起

可以看出,只有前驱节点为 SIGNAL 状态时,后继节点才可以挂起,因为 SIGNAL 状态的节点在释放锁时会唤醒其后继节点,而前驱节点处于其它状态时,当前线程不会挂起,而是将前驱节点的状态修改为 SIGNAL,继续自旋,如果再次获取锁失败,此时前驱节点的状态就是 SIGNAL 了,然后就可以挂起了。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 前驱节点的状态
    int ws = pred.waitStatus;
    // 如果前驱节点的状态为 SIGNAL
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) { // 前驱节点处于取消状态,则需要将前驱节点从队列中删除
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { // 0、`CONDITION` 或者 `PROPAGATE` 状态,设置为 SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt 方法用于阻塞当前线程

private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程,进入等待状态,需要调用 LockSupport.unpark 方法或者 interrupt 中断进行唤醒
    LockSupport.park(this);
    // 被唤醒之后返回当前线程是否在中断状态,并清除中断记号
    return Thread.interrupted();
}

至此,整个加锁过程就已经完成。

最后看一下解锁方法 tryRelease,代码如下

public final boolean release(int arg) {
    // 执行释放锁,需要实现类自行实现逻辑
    if (tryRelease(arg)) {
        Node h = head;
        // 唤醒 CLH 队列的头节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

unparkSuccessor 方法用于唤醒 CLH 头节点的后继节点(前文中一直说唤醒头节点,其实真正唤醒的是头节点的后继节点),传入的 node 就是头节点。如果后继节点状态为已取消,则向后查找最近的一个有效节点进行唤醒。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 将头节点的状态重置为 0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 如果后续节点为已取消,则从队列尾部开始向前查找,找到离当前节点最近的一个有效节点    
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 唤醒该节点中的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

最后理一下独占模式下 CLH 节点的状态变化过程,如下图所示,其中后继节点加入与加锁是同步进行,只要在释放锁之前有后继节点加入,则状态会被修改为 SIGNAL,这样后继节点才可以挂起线程,等待前驱节点释放锁之后会主动唤醒后继节点。

Created with Raphaël 2.2.0 开始 进入CLH队列:状态为 0 后继节点加入 waitStatus = -1 释放锁 waitStatus != 0? 唤醒后继节点 waitStatus = 0 结束 加锁 yes no
共享锁源码分析

共享锁与独占锁的实现逻辑大体一致,也是利用 CLH 队列,CLH 队列也是通过 挂机-唤醒 机制自旋式获取锁,但是共享锁与独占锁不同的是,共享锁可以允许多个线程同时成功加锁,所以在加锁的实现逻辑上有差别,另外,线程释放锁时,其它线程就可以进行加锁,这里也不能像独占锁一样,只唤醒后继节点即可,因为可能多个节点都可以成功加锁,只唤醒一个就失去了共享锁的意义。

首先查看一下加锁方法 acquireShared,该方法首先调用 tryAcquireShared 方法尝试加锁,这里与独占锁的加锁方法 tryAcquire 有所不同,独占锁只有一个线程可以成功,所以返回布尔值,要么成功,要么失败。tryAcquireShared 方法返回一个数值,大于等于 0,表示加锁成功,小于 0,表示加锁失败,走 CLH 队列流程。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

tryAcquireShared 方法与 tryAcquire 方法一样需要实现类自行实现

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

doAcquireShared 方法相当于独占锁的 addWaiter + acquireQueued + selfInterrupt,独占锁写的比较艺术,而这里就是简单的代码陈列。

private void doAcquireShared(int arg) {
    // 设置节点为共享节点,加入 CLH 队列尾部
    final Node node = addWaiter(Node.SHARED); 
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) { // 自旋
            final Node p = node.predecessor(); // 获取前驱节点
            if (p == head) {
                int r = tryAcquireShared(arg); // 尝试加锁
                if (r >= 0) { // 加锁成功
                    setHeadAndPropagate(node, r);// 设置当前节点为头节点
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt(); // 设置中断标记
                    failed = false;
                    return;
                }
            }
            // 加锁失败,阻塞线程,这里与独占锁的实现一模一样
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

可以对比一下独占锁的代码实现,可以发现实现逻辑几乎一样。这里只说明一下不一样的地方。

  • 创建节点,调用 addWaiter(Node.SHARED) 创建共享节点,而独占锁调用 addWaiter(Node.EXCLUSIVE) 创建独占节点。
  • 加锁,调用 tryAcquireShared,而独占锁调用 tryAcquire 方法,这两个方法都需要实现类自行实现。
  • 加锁成功,调用 setHeadAndPropagate(node, r) 方法设置当前节点为头节点,而独占锁调用 setHead(node)

共享锁的加锁与独占锁不一样,虽然都是利用同步状态做文章,共享锁可以理解为资源,每一次加锁,就是向 AQS 申请资源,而释放锁就是向 AQS 归还资源,所以只要 AQS 还有资源就可以允许加锁。所以 tryAcquireShared 方法的返回值,可以理解为加锁后的剩余资源,所以只要大于等于 0,就是加锁成功。

这里重点关注一下 setHeadAndPropagate 这个方法,通过方法名可以猜测,setHeadAndPropagate 包含了 setHead 的逻辑,同时还要实现 propagate (传递),所谓传递就是指,只要 AQS 还有剩余资源,就继续唤醒后继节点,进行加锁操作。唤醒操作调用 doReleaseShared() 方法实现,这个方法也是释放同步锁时会调用的方法,在下文还会详细介绍,这里只要理解为继续唤醒后继节点即可。

// propagate 就是 tryAcquireShared 加锁后返回的剩余资源
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node); // 将当前节点设置为头节点
    // propagate > 0 表示只要资源还有,则需要继续唤醒后续节点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 后继节点应该也是共享节点
        if (s == null || s.isShared())
            doReleaseShared(); // 释放共享锁时调用的方法
    }
}

接着看一下共享锁释放方法 releaseShared,首先调用 tryReleaseShared 进行资源归还,这个方法需要实现类自行实现,然后调用 doReleaseShared 方法唤醒 CLH 队列中的节点。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared 这个方法对比独占锁释放时的操作就要复杂很多,因为独占锁只有一个线程可以获取锁,所以释放的时候不存在线程安全的问题,但是这个方法在队列中加锁成功与任意线程释放锁都会调用,就有可能出现判断过程中出现头节点被替换的问题。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果节点状态为 -1,则将状态设置为 0,唤醒后继节点
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            // 如果节点状态为 0,则将状态设置为 -3
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

最后理一下共享模式下 CLH 节点的状态变化过程,如下图所示。

Created with Raphaël 2.2.0 开始 进入CLH队列:状态为 0 后继节点加入 waitStatus = -1 释放锁 waitStatus == -1? waitStatus = 0 唤醒后继节点 结束 waitStatus == 0? waitStatus = -3 加锁 yes no yes no
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值