AQS(AbstractQueuedSynchronizer)原理解析

1.什么事AQS?

在AQS中存在一个FIFO双向队列,队列中的节点表示被阻塞的线程,队列节点元素有4种类型, 每种类型表示线程被阻塞的原因,这四种类型分别是:

  • CANCELLED : 表示该线程是因为超时或者中断原因而被放到队列中
  • CONDITION : 表示该线程是因为某个条件不满足而被放到队列中,需要等待一个条件,直到条件成立后才会出队
  • SIGNAL : 表示该线程需要被唤醒(在队列中的除了尾节点以外的其他节点(排除超时的中断的应该都是这个状态))
  • PROPAGATE : 表示在共享模式下,当前节点执行释放release操作后,当前结点需要传播通知给后面所有节点

由于一个共享资源同一时间只能由一条线程持有,也可以被多个线程持有,因此AQS中存在两种模式,如下:

  • 1、独占模式

    独占模式表示共享状态值state每次只能由一条线程持有,其他线程如果需要获取,则需要阻塞,如JUC中的ReentrantLock

  • 2、共享模式

    共享模式表示共享状态值state每次可以由多个线程持有,如JUC中的CountDownLatch

        AQS最大的特点是当有多个线程去竞争同一个锁的时候,假设锁被某个线程占用,那么如果有成千上万个线程在等待锁,有一种做法是同时唤醒这成千上万个线程去去竞争锁,这个时候就发生了羊群效应,海量的竞争必然造成资源的剧增和浪费,因此终究只能有一个线程竞争成功,其他线程还是要老老实实的回去等待。AQS的FIFO的等待队列给解决在锁竞争方面的羊群效应问题提供了一个思路:保持一个FIFO队列,队列每个节点只关心其前一个节点的状态,线程唤醒也只唤醒head后面第一个在等待的线程。

2.AQS中的设计模式?

模板方法模式:

https://www.cnblogs.com/smallJunJun/p/10892545.html

除了通用的节点入队出队是不需要子类重写的,包括获取同步状态和释放同步状态的以整套逻辑是不需要重写的。

在AQS中,模板方法设计模式体现在其acquire()、release()方法上,其中调用tryAcquire()方法的默认实现是抛出一个异常,也就是说tryAcquire()方法留给子类去实现,acquire()方法定义了一个模板,一套处理逻辑,相关具体执行方法留给子类去实现

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //成功拿到锁的时候,acquireQueued会返回false,就不会往下执行了
        selfInterrupt();
}
protected boolean tryAcquire(int arg) {
    //这就是留给子类方法去实现的
    throw new UnsupportedOperationException();
}

3.AQS中的核心数据结构和入队出队,节点的一些变化

队列中一个节点的结构:

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; 
    // waitStatus只取上面CANCELLED、SIGNAL、CONDITION、PROPAGATE四种取值之一
    volatile int waitStatus;
    // 表示前驱节点
    volatile Node prev;
    // 表示后继节点
    volatile Node next;
    // 队列元素需要关联一个线程对象
    volatile Thread thread;
    // 表示下一个waitStatus值为CONDITION的节点
    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() {}
    /**
     *  当有线程需要入队时,那么就创建一个新节点,然后关联该线程对象,由addWaiter()方法调用
     */
    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;
    }
}

入队列:

如果来了一个线程可以直接获取同步状态,就不用进入队列,否则进入addWaiter判断,如果队列里面存在节点并且成功插入,就直接进入if循环返回插入的这个节点。队列为空,或者CAS操作失败没插入,进入enq死循环加CAS

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;
}

head节点是不持有线程对象的

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
            //通过这里我们发现头结点是不持有线程对象的,就是可以空节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

 addWaiter插入到队列之后就会进入acquireQueued方法去不断的轮询当前线程是否可以获得同步状态了

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里面设置了同步状态
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //当进入下面这个循环的话可能是因为当前节点的前一个节点不是头结点,也可能因为trAcquire没有成功获取,也就是现在同步资源正在被占用
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private final boolean parkAndCheckInterrupt() {
    //shouldParkAfterFailedAcquire返回true就表示当前节点的线程可以安心挂起,等待唤醒或中断
    LockSupport.park(this);
    return Thread.interrupted();
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        //你的前一个节点处于需要被唤醒状态(等同于头结点正在占用同步资源),你就可以安心的阻塞吧,还轮不到你呢
        return true;
    if (ws > 0) {
        //前一个状态大于0,表示这个节点被取消或超时,应该从这个队列中剔除掉了
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //由你负责把你前面的节点置为需要被唤醒的状态(也就是阻塞状态),每一个节点都需要负责它的前一个节点
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
//在acquire里面执行的,从if里面执行,有中断标志就会执行。
static void selfInterrupt() { Thread.currentThread().interrupt(); }

对于出队列,就是尝试唤醒head节点后面第一个在等待被唤醒的节点,可能是0也可能是signal

//尝试去释放,
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //这里主要是假如waitStatus为-1,就表示有后面节点在等你,你需要去唤醒它,为0的话应该就是这一个节点
            unparkSuccessor(h);
        //不经过上面的if说明这队列里面可能就你一个,就直接返回true了
        return true;
    }
    return false;
}
//去唤醒距离你最近的那一个状态为signal的状态
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        //头结点要从-1变为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)
        //唤醒后面的节点,被唤醒的这个节点又会for循环
        LockSupport.unpark(s.thread);
}

4.通过对上面的理解,我们来自定义一个同步器(这其实就是一个简便的互斥锁)

class Mutex implements Lock, java.io.Serializable {
    // 自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判断是否锁定状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // 尝试获取资源,立即返回。成功则返回true,否则false。
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 这里限定只能为1个量
            if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
                return true;
            }
            return false;
        }
        // 尝试释放资源,立即返回。成功则为true,否则false。
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定为1个量
            if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//释放资源,放弃占有状态
            return true;
        }
    }
    // 真正同步类的实现都依赖继承于AQS的自定义同步器!
    private final Sync sync = new Sync();
    //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
    public void lock() {
        sync.acquire(1);
    }
    //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    //unlock<-->release。两者语文一样:释放资源。
    public void unlock() {
        sync.release(1);
    }
    //锁是否占有状态
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
    }
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;    
    }
    @Override
    public Condition newCondition() {
        return null;    
    }
}

像ReentrantLock,CountDownLatch里面的同步器都是继承AQS这个抽象类

posted @ 2019-05-21 11:34  LeeJuly  阅读(381)  评论(0)    收藏  举报