20.AQS原理

重入锁的实现一个比较复杂的过程,涉及多个类和方法。而这还只是AQS的一小部分,要真正理解JUC体系 ,我们必须先梳理清楚AQS的问题。 AQS, Abstract Queued Synchronizer ,即抽象队列同步器。JUC里提供的线程工具中,很大一部分都基于AQS来实现的,甚至可以说AQS是整个JUC体系的根基。

1 如何设计一把锁

如果我们要自己设计一个锁,应该考虑哪些问题呢?

一个完整的锁,要保证多个线程访问时能正常访问和处理临界资源,如下图所示,此时至少应该满足如下条件:

1.满足互斥功能,也即竞争同一个共享变量时能保证安全的访问。

2.竞争失败的线程应该阻塞,并且能在资源被释放之后能被唤醒。这也要求,如果有N个线程被阻塞,则应该有一个容器来管理这些线程。

3.不同线程之间能够调整优先级(也称为公平非公平锁问题)。

4.要能满足重入的要求。

20.AQS原理_第1张图片

如果只有两三个线程 ,上面大部分操作还是比较好实现的,关键是多个线程同时竞争时该如何实现上述功能呢?我们该用哪种数据结构才能方便的满足该要求呢?答案是带头结点的双向链表:

20.AQS原理_第2张图片

 

在双链表中,我们定义了头结点head和tail用来快速访问首尾元素。获得资源的就是head指向的结点,执行完成后就可以被释放,对应的链表操作就是删除thread1对应的结点。如果有线程被阻塞,就将其打包成一个node连接到tail指向的位置上去。这样如果竞争线程比较多,就可以都连到队列上进行等待,这就实现了阻塞并等待的功能。

那如何实现优先级策略呢?假如某个新来的线程优先级高,要马上执行,此时我们只要将其连接到head位置就可以了,在上图对应的就是thread1执行完成之后马上执行这个优先级高的线程。(注意,这里是已经抢占资源的要先执行完,而不是直接中断正在执行的线程。)

那如何实现重入呢?这个更简单,我们只要在临界资源上增加一个state字段即可。如果当前资源空闲,那么state=0,否则就代表资源被某个线程抢占了还没释放。如果是重入,则继续将state的值增加即可,当然临界资源还会记录自己被哪个线程占用了。

这样,我们就大致设计了一个锁的基本结构,里面还有很多问题要进一步探讨,例如每个结点是如何等待的,如何唤醒的等等。接下来我们就详细看AQS是如何做的。

2 AQS的基本工作过程

2.1 AQS 基本结构

AQS是JUC的核心,无论是信号量还是可重入锁,背后都有AQS的影子。这些类的同步过程一般如下:

20.AQS原理_第3张图片

 

tryAcquire和tryRelease过程很好理解,就是CAS地修改AQS的state值,关键是doAcquire和doRelease如何管理众多线程的状态,又如何决定哪个线程可以获得锁。答案就是,AQS在其内部管理了一个链表,所有的线程都会被添加到这个链表中进行管理,这个链表在公平实现中又具有先进先出特性(非公平实现当然就是优先队列),这种思想也叫做CLH(三个人的姓名简称),而AQS在实现过程中体现了该思想,因此该队列也被称为CLH队列。

首先回顾一下doAcquireInterruptibly方法的代码:

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    ...
}

可以看到,在方法最开始,首先调用了addWaiter方法,这个方法就是将当前线程添加到等待队列的过程,对链表都所了解的都能轻易看懂:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

可见AQS是将线程包装成Node再入队的。Node是AQS里的一个内部类。从addWaiter可以看到,CLH队列的入队实际上就是把当前节点设置为队尾,那么相应的,出队就是处理队首节点。

在这里还有对前驱节点、后继节点的引用。前驱结点主要用在取消等待的场景:当前节点出队后,需要把后继结点连接到前驱结点后面;后继结点的作用是避免竞争,在doRelease方法中,当前节点出队后,会让自己的后继结点接替自己获取锁,有了明确的继承关系,就不会出现竞争了。

由此可见AQS是通过队列操作实现任务的管理,而且更方便:

  • 同步操作只涉及头节点、尾节点、当前节点、前驱结点、后继结点,对整个队列影响很小,明显优于整体加锁或者分段加锁。

  • 通过后继结点机制,明确了获得锁的顺序(除非出现信号量申请许可过多无法获取锁这种情况,此时只需要传播状态,继续向下寻找合适节点继承锁即可),避免了竞争。

2.2 核心结点Node

既然任务是通过队列来管理的,那每个任务自然就是队列中的一个个结点,那这些结点又是如何工作的呢?

AbstractQueuedSynchronizer类中的内容非常多,我们首先注意到内部有两个静态内部类Node和ConditionObject,这两个分别构造出了同步队列和条件队列。条件队列的问题我们后面再看,这里先看同步相关问题。

同步队列的基本结构如下,每个结点对应的就是一个被阻塞的线程,每个结点能够实现自旋等待的功能。

20.AQS原理_第4张图片

 上一节说,在同步队列中,如果当前线程获取资源失败,就会通过addWaiter()方法将当前线程放入队列的尾部,并且保持自选等待的状态,不断判断自己所在的结点是否是队列的头结点。如果自己所在的节点是头结点,那么就会不断尝试获取资源,如果成功,则通过acquire()方法退出同步队列,并在CPU里执行。以doAcquireInterruptibly为例,parkAndCheckInterrupt方法使用LockSupport令当前线程阻塞,直到收到信号被唤醒后,进入下一轮自旋:

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {//自旋
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
 
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

那每个任务被封装成的结点,也就 静态内部类Node又是什么呢?Node的定义如下:

static final class Node {
        // 共享模式
        static final Node SHARED = new Node();
        // 独占(排它)模式
        static final Node EXCLUSIVE = null;

        // 线程已被取消,当首节点释放锁后,
        // 开始查找下一个 waitStatus < 0 的节点,
        // 如果遇到已取消的线程,则移除
        static final int CANCELLED =  1;
        // 当前线程的后继线程需要被unpark(唤醒)
        // 后继节点处于等待状态,当前节点(为-1)被取消或者中断时会通知后继节点,
        // 使后继节点的线程得以运行
        static final int SIGNAL    = -1;
        // 当前节点处于等待队列,节点线程等待在Condition上,
        // 当其他线程对condition执行signall方法时,
        // 等待队列转移到同步队列,加入到对同步状态的获取
        static final int CONDITION = -2;
        // 与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态
        static final int PROPAGATE = -3;

        // 当前节点的状态,默认是 0 
        volatile int waitStatus;
//双向链表
        volatile Node prev;
        volatile Node next;

        // 等待获取锁而自旋的线程
        volatile Thread thread;
        
        // Node既可以作为同步队列节点使用,也可以作为Condition的等待队列节点使用(将会在后面讲Condition时讲到)。
        // 在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式;
        // 在作为等待队列节点使用时,nextWaiter保存后继节点。
        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
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
}

 

从上面可以看到Node类的几个关键定义:

第一个是:Node类型的 prev和next说明该结构是一个双向链表,prev和next分别是前驱和后继结点。

第二个是:Node 类型的SHARED和EXCLUSIVE表示共享还是独占类型的结点。

第三个是:定义了四个常量CANCELLED、SIGNAL、CONDITION和PROPAGATE四个表示状态的常量。

  • CANCELLED:表示当前节点中的线程已经被取消。

  • SIGNAL:表示后继结点中的线程处于等待状态。

  • CONDITION:表示当前结点的线程在等待某个条件,也就是当前结点处于Condition队列中。

  • PROPAGATE:表示当前场景下能够执行后续的acquireShared操作。

  • 在默认情况下,waitStatus的取值为0,表示当前节点在sync队列中,等待获取锁。

第四个是:Node类中存在一个volatile类型的成员变量waitStatus,其取值就是上面的几个常量值。

AQS的实现类有好几种 ,这些类与AQS搭配能够实现很多强大的功能,例如:

20.AQS原理_第5张图片

 后面我们就分专题详细分析。

从本小节的分析可以看到,AQS本身是通过队列来管理线程,而每个结点在等待机制上是“自旋+阻塞+唤醒”的机制,相比于纯自旋,这种方式能够很好地兼顾性能与效率。

你可能感兴趣的:(多线程与高并发,java,开发语言)