AQS简单源码说明

AbstractQueuedSynchronizer

所谓AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的,具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。

变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

AQS内部使用CLH算法维护等待队列,CLH锁即Craig, Landin, and Hagersten (CLH) locks。CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。

CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程须要获取锁,且不释放锁。为false表示线程释放了锁。

结点之间是通过隐形的链表相连,之所以叫隐形的链表是由于这些结点之间没有明显的next指针,而是通过myPred所指向的结点的变化情况来影响myNode的行为。

CLHLock上另一个尾指针,始终指向队列的最后一个结点。

CLHLock的类图例如以下所看到的:

在这里插入图片描述

当一个线程须要获取锁时,会创建一个新的QNode。将当中的locked设置为true表示须要获取锁。然后线程对tail域调用getAndSet方法,使自己成为队列的尾部。同一时候获取一个指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转。直到前趋结点释放锁。

当一个线程须要释放锁时,将当前结点的locked域设置为false。同一时候回收前趋结点。例如以下图所看到的,线程A须要获取锁。其myNode域为true。些时tail指向线程A的结点,然后线程B也增加到线程A后面。tail指向线程B的结点。然后线程A和B都在它的myPred域上旋转,一量它的myPred结点的locked字段变为false,它就能够获取锁扫行。明显线程A的myPred locked域为false,此时线程A获取到了锁。

在这里插入图片描述

获取独占锁:

AQS实现类中类似于ReentrantLock类,在获取锁的操作中最后调用的是如下方法。

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

tryAcquire方法由具体实现类实现,顾名思义用来获取资源,在第一次获取资源失败后进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。

首先看下addWaiter(Node.EXCLUSIVE):

private Node addWaiter(Node mode) {
    //创建独占模式的节点
    Node node = new Node(mode);
	//自旋插入节点
    for (;;) {
        //获取尾部节点
        Node oldTail = tail;
        //如果尾部节点不为空
        if (oldTail != null) {
            //新节点的前置节点设置为原先的尾节点
            node.setPrevRelaxed(oldTail);
            //将新节点设置为尾节点
            if (compareAndSetTail(oldTail, node)) {
                //原先尾节点next节点设置为新节点
                oldTail.next = node;
                return node;
            }
        } else {
            //如果没有尾节点则说明队列为空需要初始化
            initializeSyncQueue();
        }
    }
}

如上为在等待队列中插入当前获取资源的节点。

接下来看acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        //自旋
        for (;;) {
            //获取当前节点的前置节点
            final Node p = node.predecessor();
            //如果前置节点为头节点,说明马上就轮到自己了,可以先尝试获取资源
            //在头结点释放资源后并唤醒下一个节点的时候p==head成立,进入循环
            //这里保证FIFO
            if (p == head && tryAcquire(arg)) {
                //如果获取成功,将当前节点设置为头节点
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            //保证前置节点状态为SIGNAL
            if (shouldParkAfterFailedAcquire(p, node))
                //调用park方法将自己阻塞,激活后检查中断状态并与interrupted或运算
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

再看下shouldParkAfterFailedAcquire(p, node)方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前继节点的waitStatus
    int ws = pred.waitStatus;
    //如果ws为SIGNAL状态,表面前继节点释放资源或中断后会唤醒自己直接返回true
    if (ws == Node.SIGNAL)
        return true;
    //如果ws>0即为取消状态,跳过此前置节点,一直往前找,直到找到waitStatus<0的节点,并将此节点设置为	 //node的前置节点,同时设置此节点的后置节点为node
    if (ws > 0) {
        do {
            //这里往前变量才能保证if (p == head && tryAcquire(arg)) 跳出循环
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 将前继节点的ws值设置为Node.SIGNAL,以保证下次自旋时,shouldParkAfterFailedAcquire直接返回true
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

释放独占锁:

AQS实现类中类似于ReentrantLock类,在释放锁的操作中最后调用的是如下方法。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        //获取头节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease方法由具体实现类实现,顾名思义用来释放资源,释放成功进入后面代码。

接下来看unparkSuccessor方法:

private void unparkSuccessor(Node node) {
    //获取头结点waitStatus
    int ws = node.waitStatus;
    if (ws < 0)
        //如果ws小于0,将其waitStatus置为0
        node.compareAndSetWaitStatus(ws, 0);
	
    Node s = node.next;
    //如果头结点的nest节点为空或者waitStatus大于0,则从尾节点从后往前查找
    //搜索到等待队列中最靠前的ws值非正且非null的节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    //唤醒下一个节点线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

后继节点的阻塞线程被唤醒后,就进入到acquireQueued()的if (p == head && tryAcquire(arg))的判断中,此时被唤醒的线程将尝试获取资源。

当然,如果被唤醒的线程所在节点的前继节点不是头结点,经过shouldParkAfterFailedAcquire的调整,也会移动到等待队列的前面,直到其前继节点为头结点。

获取共享锁

AQS实现类中类似于ReentrantReadWriteLock类,在获取共享锁的操作中最后调用的是如下方法。

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

tryAcquireShared方法为AQS集成类实现,获取失败后进入doAcquireShared方法:

private void doAcquireShared(int arg) {
    //此addWaiter与获取独占锁中的一致,将一个共享节点放入等待队列队尾
    final Node node = addWaiter(Node.SHARED);
    boolean interrupted = false;
    try {
        //自旋操作
        for (;;) {
            //获取新节点前驱节点
            final Node p = node.predecessor();
            if (p == head) {
                //如果p就是头结点进入下面逻辑
                //尝试获取资源
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //获取成功后将当前节点设为头结点,如果r>0并唤醒后继节点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            //保证前置节点状态为SIGNAL,方法详情分析见独占锁中分析
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    } finally {
        if (interrupted)
            selfInterrupt();
    }
}

上述代码中与获取独占锁逻辑大致一样,不同的地方在于setHeadAndPropagate方法,跟踪代码如下:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    //将当前节点设为头节点
    setHead(node);
    //当前节点获取成功后返回值大于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();
    }
}

唤醒后继节点的方法为doReleaseShared()代码如下:

private void doReleaseShared() {
    //自旋进行唤醒,由于多个线程在操作故每次获取的头结点可能会变,操作完头结点没变则跳出循环
    for (;;) {
        //获取头结点
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //ws为SIGNAL则将状态置为0
            if (ws == Node.SIGNAL) {
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                //唤醒后继节点
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

释放共享锁:

AQS实现类中类似于ReentrantReadWriteLock类,在释放共享锁的操作中最后调用的是如下方法。

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

tryReleaseShared由实现类实现,doReleaseShared上面已经分析过了。

至此,共享模式下的资源获取/释放就讲解完了,下面以一个具体场景来概括一下:

整个获取/释放资源的过程是通过传播完成的,如最开始有10个资源,线程A、B、C分别需要5、4、3个资源。

  • A线程获取到5个资源,其发现资源还剩余5个,则唤醒B线程;
  • B线程获取到4个资源,其发现资源还剩余1个,唤醒C线程;
  • C线程尝试取3个资源,但发现只有1个资源,继续阻塞;
  • A线程释放1个资源,其发现资源还剩余2个,故唤醒C线程;
  • C线程尝试取3个资源,但发现只有2个资源,继续阻塞;
  • B线程释放2个资源,其发现资源还剩余4个,唤醒C线程;
  • C线程获取3个资源,其发现资源还剩1个,继续唤醒后续等待的D线程;

条件变量支持

类似于配合synchronized关键字进行线程之间调度的notify和wait方法,条件变量的signal和await方法是配合AQS实现的锁来进行线程之间的调度

不同于synchronized同时只能与一个共享变量的notify或wait方法配合使用,AQS一个锁可以对应多个条件变量

public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            System.out.println("t1 start");
            System.out.println("condition1 await");
            try {
                condition1.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end");
            lock.unlock();
        }
    });
    t1.start();

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            System.out.println("t2 start");
            System.out.println("condition2 await");
            try {
                condition2.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 end");
            lock.unlock();
        }
    });
    t2.start();

    Thread t3 = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            System.out.println("t3 start");
            System.out.println("condition1 singal");
            condition1.signal();
            System.out.println("t3 end");
            lock.unlock();
        }
    });
    t3.start();


}

如上代码定义了两个condition即条件变量,三个线程,t1,t2线程分别等待与condition1,condition2并释放锁,t3只唤醒了condition1,t1被唤醒,t2由于没人唤醒所以一直阻塞着。

上面这个例子说明了一个锁可以对应多个条件变量,条件变量由锁的newCondition方法产生,每个条件变量里面的await和signal对应wait跟notify方法,每个条件变量里面的调度各自独立互不影响。

代码lock.newCondition()其实就是新建了一个AQS的内部类ConditionObject对象,可以访问AQS内部的变量和方法。每个条件变量的内部都维护着一个条件对垒,用来存放调用条件变量的await方法时被阻塞的线程。注意这个条件队列跟AQS队列不是一回事!!!

参考文章:https://www.jianshu.com/p/0f876ead2846
https://blog.csdn.net/varyall/article/details/80317488

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xhjwyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值