AQS源码讲解

目录

一、什么是AQS ?

二、AQS框架

三、Node

四、源码讲解 

1.1 acquire

1.2 Release

2.1 acquireShared

2.2 releaseShared

小结:


一、什么是AQS ?

AQS(AbstractQueuedSynchronizer),正如其名,抽象的队列同步器,它提供了一套可用于实现锁同步机制的框架,是除了java自带的synchronized关键字之外的锁机制,许多同步类的实现都依赖于它,如:CountDownLatch、Semaphore、ReentrantLock...

二、AQS框架

它维护了一个volatile int State(代表资源,可以通过getState()、setState()、CompareAndSetState()操作这个变量)和一个CLH队列(它是一个FIFO的双向队列),队列的每一个Node节点用于包装线程。线程请求共享资源时,如果被请求的资源state是空闲的,线程就获取资源,得以继续执行,否则将线程阻塞并放入CLH同步队列中,等待被唤醒继续争夺资源。

AQS根据资源互斥级别提供了独占(Exclusive)和共享(Share)两种资源访问模式。

独占:资源只能被一个线程使用。如:ReentrantLock是一个独占式的锁。

共享:资源同时能被多个线程所使用。如:Semaphore、CountDownLatch。

AQS是一个典型的模板模式,我们在自定义同步器时只需要继承它并实现其中的共享资源state的获取和释放的方法即可,AQS已经定义好主要的功能、模块及调用流程。前面提到了AQS的两种资源共享方式(独占和共享),所以需要实现它的方法也有两套(根据选择的共享方式,实现其中的一套即可;也可以同时实现独占和共享两种方式,如ReentrantReadWriteLock)。

独占方式:

boolean tryAcquire(int): 尝试获取资源。传入的参数为获取资源的个数。如果成功返回true,否则返回false。

boolean tryRelease(int): 尝试释放资源。传入的参数为释放资源的个数。如果成功返回true,否则返回false。

共享方式:

int tryAcquireShared(int): 尝试获取资源。0和正数代表获取 成功,负数则获取失败。

boolean tryReleaseShared(int): 尝试释放资源。传入的参数为释放资源的个数。成功返回true,失败返回false。

以ReentrantLock为例,它实现的是tryAcquire/tryRelease, state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的.

再以Semaphore为例,它实现了tryAcquireShared/tryReleaseShared,state>0表示有可用的资源数,state=0资源不可用,多个线程获取资源时会自旋CAS ,让 state = state - 请求的资源个数,当 state < 0时,线程资源获取失败放入CLH同步队列中,如果 state>0则该资源还能分配给其他线程。

三、Node

Node主要包含5个核心字段:

waitStatus:当前节点状态,该字段共有5种取值:

  • CANCELLED = 1。表示当前结点已取消调度。当节点引用线程由于等待超时或被打断时会进入此状态。

  • SIGNAL = -1。表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

  • CONDITION = -2。当节点线程进入condition队列时的状态。

  • PROPAGATE = -3。共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0。节点初始化时的状态。

prev:前驱节点。

next:后继节点。

thread:引用线程,头节点不包含线程。

nextWaitercondition`条件队列。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

四、源码讲解 

1.1 acquire

acquire核心为tryAcquireaddWaiteracquireQueued三个函数,其中tryAcquire需具体类实现。 每当线程调用acquire时都首先会调用tryAcquire,失败后才会挂载到队列,因此acquire实现默认为非公平锁

public final void acquire(int arg) {

        //tryAcquire(arg)尝试获取资源(自定义),获取失败执行下一步addWaiter(Node.EXCLUSIVE)将线程加入同步队列中。
        // acquireQueued(node,arg)在线程节点加入队列后判断是否可再次尝试获取资源,并返回获取结果
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();   //等待过程中被中断,补上中断
 }

 addWaiter将线程包装为独占节点,尾插式加入到队列中,如队列为空,则会添加一个空的头节点。值得注意的是addWaiter中的enq方法,通过CAS+自旋的方式处理尾节点添加冲突。

private Node addWaiter(Node mode) {    //将当前线程加入同步队列
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);    //创建当前线程的Node节点

    //尝试快速方式直接放到队尾。
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;     //设置当前节点的前驱节点
        if (compareAndSetTail(pred, node)) {    //将当前节点设置为尾节点
            pred.next = node;    //前驱节点指向当前节点
            return node;       //成功返回
        }
    }

    //上一步失败则通过enq入队。
    enq(node);
    return node;
}





private Node enq(final Node node) {

    //使用自旋CAS将当前节点放入队尾
    for (;;) {
        Node t = tail;
        if (t == null) {   //队列为空,创建一个空的标志结点作为head结点,并将tail也指向它
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {          //队列不为空,又执行之前那一步
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueue在线程节点加入队列后判断是否可再次尝试获取资源,如不能获取则寻找安全点,然后park进入阻塞状态,直到线程被unpark。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;   //标记是否成功拿到资源
        try {
            boolean interrupted = false;    //标记在等待过程中是否被中断过
            for (;;) {
                final Node p = node.predecessor();   //获取当前node的前置节点
                if (p == head && tryAcquire(arg)) {   //如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
                    setHead(node);              //将头节点设置为当前节点
                    p.next = null; // help GC
                    failed = false;  //成功获取到资源
                    return interrupted;    //返回
                }

                //未拿到资源,park进入阻塞状态,等着被unpark()或interrupt()
                //shouldParkAfterFailedAcquire(p,node)  :   找到安全点,成功返回true
                //parkAndCheckInterrupt() 阻塞并检测阻塞时是否被中断过
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;    //阻塞被中断,true
            }
        } finally {
            if (failed)   // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire检查状态,看是否可以去休息了,否则将其前驱节点标志为SIGNAL状态(表示其需要被unpark唤醒)后,再通过park进入阻塞状态。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;   //拿到前驱节点的状态

        //SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
        if (ws == Node.SIGNAL)   //如果前驱节点的状态为Node.SIGNAL
            /*
             *这个节点已经设置了前置节点为SIGNAL,即前置节点在释放资源时会唤醒它,所以它是一个安全点,就可以放心休息了
             *
             */
            return true;
        if (ws > 0) {  //前置节点已经被取消
            /*
             *
             * 前置进程已经被取消,跳过这个进程,从该节点往前找一个安全点
             */
            do {
                node.prev = pred = pred.prev;    //不断往前找,直到遇到正常的节点
            } while (pred.waitStatus > 0);
            pred.next = node;   //让这个状态正常的节点指向当前节点
        } else {
            /*
             *
             * pred.waitStatus设置为SIGNAL,但是不会park,等下一轮
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//调用park()使线程进入waiting状态
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}

1.2 Release

release流程较为简单,尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued中的for(;;)循环开始新一轮的资源竞争。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;   //找到头节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);    //唤醒同步队列里的下一个线程
            return true;
        }
        return false;
 }

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) {   //为空或者已经被取消CANCELLED
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)   //从队尾往前找
            if (t.waitStatus <= 0)  //从这里可以看出,waitStatus<=0的结点,都是还有效的结点。
                s = t;    //记录有效节点
    }
    if (s != null)
        LockSupport.unpark(s.thread);  //唤醒线程
}

2.1 acquireShared

acquireSharedreleaseShared整体流程与独占锁类似,tryAcquireShared获取失败后以Node.SHARED挂载到队尾阻塞,直到队头节点将其唤醒。在doAcquireShared与独占锁不同的是,由于共享锁是可以被多个线程获取的,因此在首个阻塞节点被唤醒后,会通过setHeadAndPropagate传递唤醒后续的阻塞节点。

public final void acquireShared(int arg) {   //共享式获取资源
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);    //获取失败,将当前线程加入队列,直到获取到资源为止才返回。
}

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

doAcquireShared它与acquireQueued非常相似,是不过将补中断selfInterrupt()放到了里面,唯一不同的是setHeadAndPropagate(node, r)

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);   //入队,共享
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();   //拿到前驱节点
            if (p == head) {      //如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
                int r = tryAcquireShared(arg);
                if (r >= 0) {   //资源获取成功
                    setHeadAndPropagate(node, r);  //将head指向自己,还有剩余资源可以再唤醒之后的线程
                    p.next = null; // help GC  前驱节点被垃圾回收
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //找安全点,park,等着被unpark()或interrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate将头节点指向自己,并且检查资源是否还有剩余,如果有调用doReleaseShared继续唤醒其余阻塞的节点。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node); //将头节点指向自己
    

    //如果还有剩余量,继续唤醒下一个邻居线程
    //如果propagate > 0,表示存在多个共享锁可以获取,可直接进行doReleaseShared唤醒阻塞节点
    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;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                unparkSuccessor(h);  //唤醒后继节点
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // CAS失败继续循环
        }
        if (h == head)     //head被改变               
            break;
    }
}

2.2 releaseShared

releaseShared它会尝试释放指定的资源,如果释放成功则继续尝试调用doReleaseShared去唤醒同步队列里的线程来获取资源。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();  //唤醒后继节点
        return true;
    }
    return false;
}

小结:

到这里AQS的几个核心方法也讲完了,相信到了这里大家对AQS的框架也有了基本的认识,来对前面的知识做一下总结吧。

AQS是一个抽象的类,它已经定义好了主要的功能、模块及调用流程。AQS根据资源互斥级别提供了独占(Exclusive)和共享(Share)两种资源访问模式,其中的tryAcquire/tryRelease或tryAcquireShared/tryReleaseShared需代码要自行定义。

独占锁共享锁默认都是非公平获取策略,可能被插队。也可以自定义tryAcquire/tryRelease或tryAcquireShared/tryReleaseShared方法让它成为公平锁。独占锁只有一个线程可获取,其他线程均被阻塞在队列中;共享锁可以有多个线程获取。独占锁释放仅唤醒一个阻塞节点,共享锁可以根据可用数量,一次唤醒多个阻塞节点。希望大家一定要搞清楚独占锁和共享锁的区别!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值