美文网首页
AQS同步原理(更新)

AQS同步原理(更新)

作者: 火兰人一个 | 来源:发表于2017-08-20 21:49 被阅读0次

    AQS维护了俩个节点的引用,一个指向头节点,一个指向尾节点。当有一个线程成功获取到同步状态(或者锁)时,其他的线程就无法获取到同步状态,因此会被构造成节点放入同步队列,但是入队的过程必须保证是线程安全的。因此同步器提供了一个基于cas的设置尾节点的方法:compareAndSetTail(Node expect, Node update),设置成功后,当前节点正式与之前的尾节点建立关联。

    同步队列遵循FIFO原则,首节点是获取同步状态的节点,当首节点的线程释放同步状态时,将会唤醒后继节点,而后继节点在获取同步状态成功的时候将自己设置为头节点。

    设置首节点是通过成功获取到同步状态的线程来完成的,因为同时只有一个线程能够获取到同步状态,因此设置头节点并不需要用CAS来保证。它只需要将首节点设置为原首节点的后继节点,并且断开原首节点的next引用。

    独占式同步状态的获取:

    首先当调用lock方法时,会调用acquire(int arg),acquire方法首先会执行一段判断:if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))。判断结果如果返回true,则当前线程执行中断selfInterrupt();。(这里需要注意,如果当前的线程获取同步状态失败后进入了同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,这里中断的意义何在???)

    首先分析tryAcquire(1),这个方法会尝试更新当前的线程获取同步状态,如果当前预期同步状态为0,并且成功更新了当前线程的同步状态为1,并且将当前线程设为独占线程,返回true;如果更新不成功,则返回false。

    再分析acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法。首先addWaiter方法,首先会以当前线程创建一个node节点a,在获取当前同步队列的tail节点,如果tail节点不为空,则利用cas更新当前a为tail节点,更新成功则返回a节点,更新失败则执行enq(node)方法(node传节点a);如果tail节点为空,也执行enq方法。

    private Node enq(finalNode node) {

    for(;;) {

    Node t =tail;

    if(t ==null) {// Must initialize

    if(compareAndSetHead(newNode()))

    tail=head;

    }else{

    node.prev= t;//将tail节点指向node的pre节点

    if(compareAndSetTail(t,node)) {//如果t节点是尾节点,则将node节点更新为尾节点,cas是为了保证在更新是t是尾节点,因为在这个方法的执行过程中,可能有其他线程被更新为尾节点。如果更新失败,则继续更新,将tail置为t,循环前面的过程。

    t.next= node;//将node置为t的next节点,此时node是尾节点。(即将t.next节点指向了node所指向的内存。node是当前线程构造的节点)

    returnt;

    }

    }

    }

    }

    分析enq方法,进入循环:第一次循环时,获取tail节点(t = tail;)。1、如果t节点为空,则初始化一个head空节点,让t = head,接着进入第二次循环;2、如果tail不等于空,则利用cas将a节点置为tail节点,如果失败,则接着执行循环,直到将a节点成功置为tail节点后,并且将a节点设置为t.next,返回t节点(1、addWaiter(Node.EXCLUSIVE)执行成功后返回的始终是以当前线程构造的node节点,而在构造过程中,当执行到enq(node)方法,只是为了保证将构造的node节点确保能插入到tail节点。2、enq(node)方法这里为什么不返回a节点,而是返回t节点,其他地方可能会用到,在此不做分析)。。(分界线)。。如果第一次循环时,t为空,则进入第二次循环,这个时候,t在第一次循环时已经被设置为空节点,并且是头节点,接着将a节点置为tail节点,利用循环cas直到成功的将a节点置为tail节点,并且将a节点设置为t.next,然后返回t节点。执行完addWaiter(Node.EXCLUSIVE)后,

    程序进入到acquireQueued(node, arg),直接进入for循环开始分析。第一次循环时,理想状态下,首先获取node节点的pre节点p(如果node节点没有pre节点,直接抛空指针,因为之前的addWaiter的循环cas保证了node节点一定是tail节点,并且有pre节点),

    如果p节点是头节点并且node节点的tryAcquire(1)方法返回true,代表node节点获取到了同步状态,那么将node节点设置为头节点,将p节点的next设为null(帮助gc),失败状态(failed)为false,中断状态返回false(保证了当前线程不会执行selfInterrupt方法)。如果p节点不是头节点或者node获取同步状态返回false,则接着往下执,if(shouldParkAfterFailedAcquire(p,node) &&parkAndCheckInterrupt())。

    首先分析shouldParkAfterFailedAcquire方法,如果p节点的状态是-1,则直接返回true(This node has already set status asking a release to signal it, so it can safely park.)。如果不为-1,往下执行,1、如果状态大于0,则将p节点的pre节点置为p节点,并且将p节点置为node的pre节点,直到p节点的status不符合大于0的条件,终止while循环,然后将node节点置为p节点的next节点,返回false。2、如果状态小于0,则将p节点的状态设置为SIGNAL(-1)。返回false。这里返回false表示虽然acquire获取失败也不需要中断线程。返回true代表要中断线程。注意:这里shouldParkAfterFailedAcquire如果返回了false,代表当前的p已经变成了p节点的pre节点(这个pre节点也有可能是pre的pre节点,直到满足while结束。)在acquire第二次循环的时候,这个时候shouldParkAfterFailedAcquire可能会返回false,或true,因此如果第一次返回的是false,那么p的状态已经是一个小于0的数,当是-1,第二次直接返回true,如果不是-1,第三次循环返回true,因为在第二次的时候如果不是-1,则会置为-1。(这个方法的最终目的是使node的pre节点的状态为signal,这样node节点就有机会获取同步状态了)

    但是acquireQueued是一个循环cas方法,直到p节点是头节点并且当前node获取到同步状态才会返回,如果在循环过程中出现异常,则会直接进入finally方法,就算正常结束,也会进入finally,判断failed后,如果为true,执行cancelAcquire(node);方法取消获取同步状态。将线程的状态置为1。

    当有节点释放同步状态后,将会唤醒其后继节点线程,使用unparkSuccessor(Node node)来唤醒等待状态的线程。然后被唤醒的线程会获取同步状态,并将自己置为head节点。

    相关文章

      网友评论

          本文标题:AQS同步原理(更新)

          本文链接:https://www.haomeiwen.com/subject/mjztdxtx.html