队列同步器的实现分析

91 阅读7分钟

队列同步器的实现分析

主要从同步队列、占有式同步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法.

1.同步队列

同步器依赖内部的同步队列-FIFO双向队列,来完成同步状态的管理.当前线程获取同步状态失败,同步器会将当前线程以及等待状态等信息构造成一个节点,并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态. 同步队列中的节点,用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称.

节点是构成同步队列的基础,同步器拥有首节点(head)和尾结点(tail),没有成功获取同步状态的线程将会成为节点,加入该队列的尾部

同步器中包含了两个节点的类型引用,一个指向头节点,另一个指向尾结点.当一个线程成功获取到同步状态(锁),其他线程将无法获取到同步状态,转而被狗造成节点,并加入同步队列中.这个加入的过程必须保证是线程安全的,所以同步器提供了一个基于CAS的设置尾结点的方法:compareAndSetTail(Node expect,Node update),他需要传递当前线程认为的尾结点和当前结点,只有设置成功后,当前结点才正式与尾结点建立关联.

设置首节点,是通过获取同步状态成功的来完成,由于只有一个线程能成功获得同步状态,因此设置首节点并不需要CAS.

2.独占式同步状态获取与释放

通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后,加入同步队列中,后续对线程进行中断操作,线程不会从同步队列中移除.

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

上述代码主要完成了获取同步状态、结点构造、加入同步队列中自旋等待的相关工作,主要逻辑如下: 首先调用自定义同步器实现的try Acquire方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步结点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取到同步状态),并通过addWaiter方法将该结点加入到同步队列的尾部,最后调用acquireQueued方法,使该结点以“死循环”的方式获取同步状态.如果获取不到,则阻塞线结点中的线程,而被阻塞的线程如果要被唤醒,只能依靠前驱结点的出队,或阻塞线程被中断来实现.

同步器的addWaiter和enq方法

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

private Node enq(final Node node){
    for(;;){
        Node t = tail;
        if(t == null){// Must initialize
            if(compareAndSetHead(new Node())){
                tail = head;
            }
        }else{
            node.prev = t;
            if(compareAndSetTail(t,node)){
                tail.next = node;
                return t
            }
        }

    }
}

如果用LinkedList的话,获取同步状态失败的线程,被并发的加入到LinkedList中,很难保证Node的正确添加,最终的结果,可能是结点的数量有偏差,而且顺序也可能是乱序的.

在enq(final Node node)方法中,同步器通过死循环来保证结点的正确添加,在死循环中,只有通过CAS将结点设置为尾部结点以后,才从该方法返回.否则,当前线程不断的尝试设置.

结点进入到同步队列之后,就进入了一个自旋的过程,每个节点都在自省观察,当条件满足,获取到同步状态,就可以从这个自旋的过程中退出,否则,依旧留在自旋过程中,并会阻塞节点的线程.

同步器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)){
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if(shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally{
        if(failed)
            cancelAcquire(node);
    } 
}

思考为什么自旋的条件是,只有前驱节点是头节点,才能够尝试获取同步状态: 1.头节点是成功获取到同步状态的节点,只有头节点的线程释放了同步状态,才会唤醒后继节点,后继节点被唤醒后,需要检查自己的前驱节点是否是头节点. 2.维护FIFO原则.节点间没有相互通信,只需要检查自己的前驱节点是否是头节点,就能保证FIFO原则.

当前线程获取到同步状态,并执行了相应的逻辑以后,就需要释放同步状态,使得后续节点可以继续获取同步状态.通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点

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方法使用LockSupport来唤醒处于等待状态的线程.

总结,独占式获取同步状态的获取和释放过程:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列,并在队列中自旋;移除队列或停止自旋的状态是,前驱节点是头节点,且当前结点已经成功获取了同步状态.在释放同步状态的时候,同步器调用tryRelease方法,释放同步状态,并唤醒头节点的后继节点.

3、共享式同步状态获取和释放

共享式和独占式获取的最主要区别是,是否能在同一时刻有多个线同时获取到同步状态.

通过调用同步器的acquireShared(int arg)方法可以共享地获取同步状态

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

private void doAcquireShared(int arg){
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try{
        boolean interrupted = false;
        for(;;){
            final Node p = node.processor();
            if(p == head){
                int r = tryAcquireShared(arg);
                if(r >= 0){
                    setHeadAndPropagate(node,r);
                    p.next = null;
                    if(interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if(shouldParkFailed(p,node) && 
            parkAndCheckInterrupt())
                interrupt = true;
        }
    }finally{
        if(failed)
            cancelAcquire(node);
    }
}

释放同步状态,调用releaseShared(int arg)方法.

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

4、独占式超时获取同步状态

通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定时间段内获取同步状态,如果获取到同步状态,则返回true,否则,返回false.这个是java传统同步操作所不具备的.比如synchronized关键字. 超时获取同步状态的原理是,在支持相应中断的基础上,增加了超时获取的特性.主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout = now-lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间.如果nanosTimeout>0说明超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时. 同步器的doAcquireNanos方法:

private boolean doAcquireNanos(int arg,long nanosTimeout)
throw InterruptedException{
    long lastTime = System.nanoTime();
    final Node node = addWaiter(Node.Exclusive);
    boolean failed = true;
    try{
        for(;;){
            final Node p = head.predcessor();
            if(p == head && tryAcquire(arg)){
                setHead(node);
                p.next = null;// help GC
                failed = false;
                return true;
            }
            if(nanosTimeout <=0)
            return false;
            if(shouldParkAfterFailedAcquire(p,node)
                && nanosTimeout > spinForTImeoutThreshold)
                LockSupport.parkNanos(this,nanosTimeout);
            long now = System.nanoTime();
            //计算时间,当前now减去睡眠之前的时间lastTime得到已经睡眠时间
            //的时间delta,然后被原有超时时间nanosTimeout减去,得到了
            //还应该睡眠的时间
            nanosTimeout -= now - lastTime;
            lastTime = now;
            if(Thread.interupted())
                throw new InterruptedException();
        }
    }finally{
        if(failed)
            cancelAcquire(Node);
        
    }
}