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

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

public final void acquire (int arg){
    //主要逻辑:
    //首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,若同步            
    //状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)
    //并通过addWaiter(Node node)方法将节点加入到同步队列的尾部。最后调用acquireQueued(Node         
    //node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。若获取不到则阻塞节点中的线程,而
    //被阻塞线程的唤醒主要依靠前驱节点的出对或阻塞线程被中断来实现
    if(!tryAcquire (arg) && acquireQueued (addWaiter (Node.EXCLUSIVE),arg))
        selfInterrupt();

}

分析节点的构造以及加入到同步队列,代码如下

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;
}
public 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)){
                    t.next=node;
                    return t;
                }
            }
        }
}   

上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。试想下:若使用一个普通的LinkedList来维护节点之间的关系,那么当一个线程获取了同步状态,而其他线程由于调用tryAcquire(int arg)方法获取同步状态失败而并发的添加到LinkedList时,而LinkedList将难以保证Node的正确添加,最终的结果可能是节点的数量有偏差,而且顺序也是混乱的。

在enq(final Node node)方法中,同步器通过死循环来保证节点的正确添加,在"死循环"中只有通过CAS将节点设置成尾节点后,当前线程才能从该方法返回。否则,当前线程不断的尝试设置。如此看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。


当节点进入同步队列后,进入了一个自旋的过程,每个节点(或者说每个线程)都在自省的观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个过程中(并会阻塞节点的线程)。代码如下;

final boolean acquireQueued(final Node node,int arg){
        boolean failed =true;
        try {
            boolean interrupted =false;
            for (;;){
                final  Node p=node.predecessor(arg);
                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);
        }
}

在acquireQueued(final Node node ,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够获取同步状态。

原因之一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其后继承点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。

原因之二,维护同步队列的FIFO原则,该方法中,节点自旋获取同步状态的行为如下图所示。

独占式同步状态获取与释放_第1张图片

在上图中,由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,若是则获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单的判断自己的前驱是否为头节点,这样使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。


独占式同步状态获取流程,也是acquire(int arg)方法调用流程如下

独占式同步状态获取与释放_第2张图片

在上图中,前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。当同步状态获取成功后,当前线程从acquire(int arg)方法返回。若对于锁这种锁并发组件而言,代表着当前线程获取了锁。


当前线程获取同步状态并执行了相应逻辑后,就需要释放同步状态,使得后继节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法释放后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。其代码如下:

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

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LookSupport来唤醒处于等待状态的线程。

总结:

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会加入到队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态是,同步器调用tryRelease(int arg)释放同步状态,然后唤醒头节点的后继节点。

 

 

你可能感兴趣的:(juc)