AQS原理解析(独占式)

简介

AQS的全称为(AbstractQueuedSynchronizer)。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

原理

AQS内部维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列,队列数据结构为双向链表,这是AQS实现锁的基础。我们在使用AQS是只需要实现tryAcquire(int),tryRelease(int)和tryAcquireShared(int),tryReleaseShared(int)中的一种,前一种是独占方式如ReentrantLock,后一种是共享方式CountDownLatch,也可以两种都实现如ReentrantReadWriteLock。还有一个isHeldExclusively()方法,只有用到condition才需要去实现它。

下面是源码和我自己的一些理解。这里先分析独占式,为了篇幅不是过长,共享式以后会单独写个博客或者结合CountDownLatch一起讲。

acquire

独占方式的实现主要靠acquire和release方法

在这里插入图片描述

tryAcquire(arg)这个方法上面说过是要我们自己实现的,它尝试获取资源,成功则返回true,失败则返回false。所以如果当前线程获取到了资源!tryAcquire(arg)返回false,该方法无需再继续执行直接返回。
如果当前线程没有获取到资源,说明资源已经被其他线程占用,它就需要进入线程等待队列。

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

首先执行addWaiter(Node.EXCLUSIVE)方法即添加该线程到等待队列并标记为独占式。如果对列尾节点不为空即当前队列存在,则将新的线程节点添加尾节点后面,并通过原子操作将 尾节点修改为新的节点。
如果当前队列还不存在,则执行enq(node)方法。

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)) {
                /*拿到资源后,设置该节点为头节点并断开原来头部的连接
                这样原来头节点就变成了游离无引用节点,GC就会去回收*/
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    //返会中断结果
                    return interrupted;
                }
                //如果还没有轮到自己执行则排队加入合适位置并判断中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

如果该节点的前继节点为头节点且自己已经获取到资源则设置该节点为头节点并返回中断结果,否则执行shouldParkAfterFailedAcquire方法。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)
        /*如果前驱节点状态为SIGNAL,那么它执行完后就会通知唤醒自己,所以只要
        排在该节点之后即可*/
        return true;
    if (ws > 0) {
        /*
         * 如果前驱已经成为无用节点,那就一直往前找,直到找到
         * 最近一个正常等待的状态,并排在它的后边。
         *而那些无用节点就形成形成一个无引用链,稍后就会被GC回收
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         /*如果前驱正常,那就把前驱的状态设置成SIGNAL,
         使其执行结束后通知唤醒自己。         
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

这个方法说简单点就是从后往前找到一个有效的节点并排在其后面,如果他的状态已经是SIGNAL,则无需修改,如果不是则通过CAS操作将其状态修改为SIGNAL。这里附上状态信息。

CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

0状态:值为0,代表初始化状态。

排完队后就使线程进入waiting状态并判断它在排队中是否被中断过。

在这里插入图片描述

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)) {
                /*拿到资源后,设置该节点为头节点并断开原来头部的连接
                这样原来头节点就变成了游离无引用节点,GC就会去回收*/
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    //返会中断结果
                    return interrupted;
                }
                //如果还没有轮到自己执行则排队加入合适位置并判断中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

回到acquireQueued方法,为了方便我把代码再复制一遍。在请求队列中,如果前继节点为头结点且自己已获取资源,那么当前线程就可以执行,退出自旋,否则就找到前继节点状态为signal的节点排在其后面,然后自旋,自旋过程中会不断判断当前位置是否合理(防止在自旋过程中前继节点突然失效)和判断是否被中断过。

讲到这里AQS独占式源码分析基本结束,我们总结一下。

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

1.如果线程已经拿到请求资源就直接返回执行线程,否则添加到排队队列,标记方式为独占式。
2.如果请求队列还不存在就创建一个空的节点头,将线程添加到头后并设为尾节点。如果存在则直接添加到尾部。
3.在请求队列中,如果前继节点已经是头节点就开始尝试获取资源,获取到资源后就将原头结点从链表中移除,自己成为头结点,返回中断结果,如果还没有轮到自己获取资源,就从后往前排在第一个前继节点状态为signal的节点之后,然后waiting,并判断是否中断,等待过程自旋。
4.拿到资源后返回中断结果interrupted ,如果为false,即等待过程中没有被中断过则可以执行线程,如果为true,则自我中断。(说明:线程在等待过程中被中断过,它是不响应的,只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。)

release

acquire()说完了就来讲讲它的反操作release()吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

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()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了,所以自定义同步器在设计tryRelease()的时候要明确这一点。如果资源已经释放则唤醒等待队列的下一个线程。

private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断,即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦,然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值