AQS抽象同步器的核心原理

126 阅读11分钟

AQS抽象同步器的核心原理

参考:Java高并发核心编程(卷2)多线程、锁、JMM、JUC、高并发设计模式 第六章

背景:使用基于CAS自旋实现的轻量级锁会有问题,CAS恶性空自旋会浪费大量的CPU资源。

常见的解决方案有2种,分散热点操作和使用队列削峰。JUC并发包使用的是队列削峰的方案解决CAS性能问题,并提供了一个基于双向队列的削峰基类-抽象基础类 AbstractQueuedSynchronizer(抽象同步器,简称AQS)。

锁与队列的关系

无论是单体服务应用内部的锁,还是分布式环境下多体服务应用所使用的分布式锁,为了减少由于无效争夺导致的资源浪费和性能恶化,一般都基于队列进行排队与削峰。

CLH锁的内部队列

CLH自旋锁使用的是一个单向队列,也是一个FIFO队列。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问,队列头部的节点表示占有锁的节点,新加入的抢锁线程需要等待,会插入队列尾部。

1.png

AQS的内部队列

AQS是JUC提供的一个用于构建锁和同步容器的基础类。JUC包内许多类都是基于AQS构建的,例如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的前驱节点和直接的后继节点。所以双向链表可以从任意一个节点开始很方便地访问前驱节点和后继节点。每个节点其实是由线程封装的,当线程争抢锁失败后会封装成节点加入AQS队列中;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

2.png

AQS的核心成员

AQS出于“分离变与不变”的原则,基于模板模式实现。AQS为锁获取、锁释放的排队和出队过程提供了一系列的模板方法。由于JUC的显式锁种类丰富,因此AQS将不同锁的具体操作抽取为钩子方法,供各种锁的子类(或者其内部类)去实现。

状态标志位

锁的同步状态。volatile保证了操作的可见性,所以任何线程通过getState() 都可以得到最新的状态,通过setState设置线程的状态。

volatile无法保证原子性,可以使用compareAndSetState保证原子性,底层使用的是Unsafe类。

private volatile int state;

  1. 0:当state为0时,表示该同步器处于空闲状态,即没有被任何线程占用。
  2. 非0:当state为非0时,表示该同步器被占用或被等待获取。具体而言,state的值可以是正数、负数或零。
  • 正数:当state为正数时,表示该同步器被独占模式下的线程占用,且占用的线程数量为state的绝对值。
  • 负数:当state为负数时,表示该同步器被共享模式下的线程占用,且占用的线程数量为state的绝对值。

需要注意的是,state的值并不直接代表线程的数量,而是代表了线程数量的绝对值。因此,当state为-2时,表示共享模式下有两个线程正在等待获取同步器。当state为1时,表示独占模式下有一个线程正在占用同步器。

队列节点类

AQS是一个虚拟队列,不存在队列实例,仅存在节点之前的前后关系,节点类型通过内部类Node定义,部分属性:

static final class Node {
    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;
}

waitStatus

每个节点与等待线程关联,每个节点维护一个状态waitStatus

  1. static final int CANCELLED = 1

    该线程节点已释放(超时,中断)已取消的节点不会再阻塞,表示线程因为中断或等待超时,需要从队列中取消等待。该类型节点不会参与竞争,且会一直处于取消状态。

  2. static final int SIGNAL = -1

    表示其后继的节点处与等待状态,当前的节点对应的线程如果释放了同步状态或被取消,就会通知后继节点,使后继节点线程得以运行。

  3. static final int CONDITION = -2

    表示该线程在条件队列中阻塞(Condition有使用),表示节点在等待队列中(这里指的是等待在某个锁的CONDITION上,关于CONDITION的原理后面会讲到),当持有锁的线程调用了CONDITION的signal()方法之后,节点会从该CONDITION的等待队列转移到该锁的同步队列上,去竞争锁(注意:这里的同步队列就是我们讲的AQS维护的FIFO队列,等待队列则是每个CONDITION关联的队列)。

    节点处于等待队列中,节点线程等待在CONDITION上,当其他线程对CONDITION调用了signal()方法后,该节点从等待队列中转移到同步队列中,加入对同步状态的获取中。

  4. static final int PROPAGATE = -3

    表示下一个线程获取共享锁后,自己的共享状态会被无条件地传播下去,因为共享锁可能出现同时有N个锁可以用,这时直接让后面的N个节点都来工作。这种状态在CountDownLatch中使用到了。

  5. 0

表示初始状态。

thread成员

Node的thread成员用来存放进入AQS队列中的线程引用;Node的nextWaiter成员用来指向自己的后继等待节点,此成员只有线程处于条件等待队列中的时候使用。

抢占类型常亮标识

SHARED表示线程是因为获取共享资源时阻塞而被添加到队列中的;EXCLUSIVE表示线程是因为获取独占资源时阻塞而被添加到队列中的。

FIFO双向同步队列

AQS的内部队列是CLH队列的变种,每当线程通过AQS获取锁失败时,线程将被封装成一个Node节点,通过CAS原子操作插入队列尾部。当有线程释放锁时,AQS会尝试让队头的后继节点占用锁。

AQS通过内置的FIFO双向队列来完成线程的排队工作,内部通过节点head和tail记录队首和队尾元素,元素的节点类型为Node类型。

AQS的首节点和尾节点都是懒加载的。懒加载的意思是在需要的时候才真正创建。只有在线程竞争失败的情况下,有新线程加入同步队列时,AQS才创建一个head节点。head节点只能被setHead()方法修改,并且节点的waitStatus不能为CANCELLED。尾节点只在有新线程阻塞时才被创建。

AQS锁原理

自定义SimpleMockLock,详细说明AQS锁抢占原理。核心是Sync类,lock和unlock分别调用Sync(AbstractQueuedSynchorinzer)的acquire和release

package org.gjy.m8.thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class SimpleMockLock implements Lock {

    private final Sync sync = new Sync();

    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 (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            }
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }

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

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

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

    @Override
    public Condition newCondition() {
        return null;
    }
}

显示锁抢占总体流程

  1. 开始加锁,调用SimpleMockLock对象的 lock()

    public void lock() {
        sync.acquire(1);
    }
    
  2. 该 lock() 调用的是Sync父类AbstractQueuedSynchronizer的acquire模板方法

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
  3. acquire先调用子类的tryAcquire方法,判断是否成功,成功则直接返回,失败进行addWaiter操作。(第二步)

    protected boolean tryAcquire(int arg) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
    
  4. 通过addWaiter操作将该节点加入到同步队列尾部。(第二步)

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
  5. 自旋入队 enq(第四步)

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }
    
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
    
  6. 自旋抢占 acquireQueued(第二步)

    当前Node节点线程在死循环中不断获取同步状态,并且不断在前驱节点上自旋,只有当前驱节点是头节点时才能尝试获取锁。具体可看下面的代码注释。

    为了不浪费资源,acquireQueued()自旋过程中会阻塞线程,等待被前驱节点唤醒后才启动循环。如果成功就返回,否则执行shouldParkAfterFailedAcquire()、parkAndCheckInterrupt()来达到阻塞的效果。

    调用acquireQueued()方法的线程一定是node所绑定的线程(由它的thread属性所引用),该线程也是最开始调用lock()方法抢锁的那个线程,在acquireQueued()的死循环中,该线程可能重复进行阻塞和被唤醒。

    AQS队列上每一个节点所绑定的线程在抢锁的过程中都会自旋执行acquireQueued()方法的死循环,也就是说,AQS队列上每个节点的线程都不断自旋。

    如果头节点获取了锁,那么该节点绑定的线程会终止acquireQueued()自旋,线程会去执行临界区代码。此时,其余的节点处于自旋状态,处于自旋状态的线程当然也不会执行无效的空循环而导致CPU资源浪费,而是被挂起(Park)进入阻塞状态。AQS队列的节点自旋不像CLH节点那样在空自旋而耗费资源。

    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)) { // 当前驱节点为头结点,并且tryAcquire(子类实现,可参考第3步)为true,
                    setHead(node); // 将当前节点设置为头结点
                    p.next = null; // help GC
                    failed = false; // 是否失败设置为false
                    return interrupted; // 是否被打断 设置为 false
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed) // 失败的话会调用cancelAcquire,将当前节点的状态设置为Node.CANCELLED,含义可参考AQS的核心成员-队列节点类有讲解
                cancelAcquire(node);
        }
    }
    
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    
  7. 挂起预判 shouldParkAfterFailedAcquire

    将当前节点的有效前驱节点(是指有效节点不是CANCELLED类型的节点)找到,并且将有效前驱节点的状态设置为SIGNAL,之后返回true代表当前线程可以马上被阻塞了。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
    }
    
  8. 挂起线程parkAndCheckInterrupt

    AbstractQueuedSynchronizer会把所有的等待线程构成一个阻塞等待队列,当一个线程执行完lock.unlock()时,会激活其后继节点,通过调用LockSupport.unpark(postThread)完成后继线程的唤醒。

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
    

3.png

锁释放总体流程

  1. 开始释放锁,调用SimpleMockLock的unlock

    public void unlock() {
        sync.release(1);
    }
    
  2. 该unlock调用的是Sync父类AbstractQueuedSynchronizer的release方法

    public final boolean release(int arg) {
        if (tryRelease(arg)) { // 调用的是子类的
            Node h = head;
            if (h != null && h.waitStatus != 0) // 当head指向的头节点不为null,并且状态值不为0时才会执行
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
  3. tryRelease

    protected boolean tryRelease(int arg) {
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new IllegalMonitorStateException();
        }
        if (getState() == 0) {
            throw new IllegalMonitorStateException();
        }
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }
    
  4. tryRelease为true,且满足条件时,调用unparkSuccessor

    没有立即从队列中删除该无效节点,仅仅唤醒了后继节点的线程,重启了后继节点的自旋抢锁。

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        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);
    }
    

4.png

ReentrantLock

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。

非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。

ReentrantLock在同一个时间点只能被一个线程获取,ReentrantLock是通过一个FIFO的等待队列(AQS队列)来管理获取该锁所有线程的。ReentrantLock是继承自Lock接口实现的独占式可重入锁,并且ReentrantLock组合一个AQS内部实例完成同步操作。

非公平锁

5.png

非公平锁类

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

公平锁

6.png

公平锁类

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}