Java基础-AQS之Node、Condition和初始化

87 阅读5分钟

前言

AQS全名:AbstractQueuedSynchronizer,是在jdk5以后添加的一个类,主要用于实现java的各种锁和同步器,功能强大,后面会分开几章来详细介绍。

AOS

AbstractQueuedSynchronizer有一个父类:AbstractOwnableSynchronizer,这里简称AOS,AOS本身实现非常简单,只有一个临时变量和相应的get/set方法。让线程以独占的方式来持有资源,为创建锁和相关同步器提供了实现基础。

/**
 * 独占模式下,持有资源的线程
 */
private transient Thread exclusiveOwnerThread;
/**
 * 设置独占线程
 */
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}
/**
 * 返回独占线程
 */
protected final Thread getExclusiveOwnerThread() {
    return exclusiveOwnerThread;
}

transient修饰的变量,不用参与到序列化逻辑,至于为什么要用transient,这里我也不清楚。
the transient keyword is used during serialization. Fields that are marked as transient can not be part of the serialization and deserialization.

Condition

condition接口只在AQS里有实现类:ConditionObject,是专门为锁设定的一个辅助类。接口里的方法就不一一列举了,只有三类,await,signal,signalAll。看着有点面熟吧,其实是模拟Object里的await,notify,notifyAll功能来实现的一套方法。

  • await: 释放当前线程持有的锁,并进入等待;
  • signal:唤醒一个线程;
  • signalAll:唤醒所有的线程;

Node

这里的Node特指AQS里的Node内部类,是实现双向同步队列的基本元素,其中有一个关键变量【waitStatus】,是用volatile修饰的,有以下几个状态:

  • 1:CANCELLED,表示线程已经因为超时或者中断而取消等待;
  • -1:SIGNAL,表示线程等待被唤醒;
  • -2:CONDITION,表示线程处于等待状态;
  • -3:PROPAGATE(扩散),在释放共享锁时,应该扩散给其他节点,只需要给头结点设置此状态,以确保传播可以持续。下一个线程来申请共享锁时,可以直接成功获取;

在AQS类中,只保留了head和tail两个node变量,其内部是通过隐形的链表相连,下面补一个示意图:
image.png

因为Node类比较简单,只有不到60行,这里贴一下源码,并加上我自己的注解

    static final class Node {
        // 标记一个节点以共享的模式进入等待
        static final Node SHARED = new Node();
        // 标记一个节点以独占的模式进入等待
        static final Node EXCLUSIVE = null;

        // 等待状态,CANCELLED表示线程已经因为超时或者中断而取消等待
        static final int CANCELLED =  1;
        // 等待状态,SIGNAL表示线程等待被唤醒
        static final int SIGNAL    = -1;
        // 等待状态,CONDITION表示线程处于等待状态
        static final int CONDITION = -2;
        // 等待状态,PROPAGATE表示下一个请求锁的线程应该直接成功持有
        static final int PROPAGATE = -3;

        // 初始化为0,表示一个普通节点;如果小于零,则表示有状况,需要特殊处理;如果大于零,则应该忽略;
        volatile int waitStatus;

        // 当前节点的前一个节点,出队时置空,方便GC
        volatile Node prev;

        // 当前节点的后一个节点,出队时置空,方便GC;
        // 如果next字段为空,并不代表当前节点位于末尾,因为入队操作【enq】要到CAS操作成功之后,才会分配prev
        // 如果碰到next为空,可以用tail节点往上检查,做一下双重校验
        volatile Node next;

        // 将该节点加入队列的线程,在构造时初始化并在使用后置空
        volatile Thread thread;

        // 获取下一个需要唤醒的节点,wait节点单独用一个链表保存,被唤醒之后,再放到上面的双向链表
        Node nextWaiter;

        // 判断是否共享锁
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        // 返回当前节点的前驱节点,如果没有,则返回空异常
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        // 用于建立初始头部或共享标记
        Node() {    // Used to establish initial head or SHARED marker
        }
        // 新增普通节点,waitStatus = 0
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        // 新增条件等待节点,waitStatus = -2【CONDITION】
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

初始化和CAS

初始化

在AQS类的最下方,有一个静态代码块,其中设置了几个重要属性的初始偏移量,state,head,tail,waitStatus,next。

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long stateOffset;
    private static final long headOffset;
    private static final long tailOffset;
    private static final long waitStatusOffset;
    private static final long nextOffset;

    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }

偏移量的用途

这里我们挑选一个变量的偏移量来举例说明,毕竟用法都是类似的。看看这个源码的注释,和compareAndSetHead一样,仅仅在入队方法中使用,只能在此修改head和tail变量的地址。

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

在尾结点的更新代码中,compareAndSwapObject方法引入了此偏移量,这是一个原生方法,底层用C++来实现,大概原理如下:

  1. 通过传入的变量在对象中的偏移量来获取到变量的地址(对象首地址 + 变量在对象中的偏移量)
  2. 调用 CompareAndSwap 方法比较变量是否与我们期望的相等,如果相等则更新内存地址的值;

注意:在这里做一个简单的说明,比较和替换的其实是变量的内存地址,而不是变量本身。

其他设置

在AQS中,还有一些其他变量设定。

spinForTimeoutThreshold

尝试获取锁时,先以轮询的方式来重试,如果获取锁则返回,失败则进入等待队列;这样做的好处是可以提高响应能力。默认是1000纳秒,也就是百万分之一秒。