Lock的独占锁和共享锁的比较分析

Lock锁底层依赖于AQS实现,AQS提供了多种锁的实现模式,其中独占锁和共享锁是主要的两种模式。AQS自己是一种模板方法设计模式,即AQS对外部提供了一些模板方法,而这些模板方法又会调用由子类实现的抽象方法。今天咱们主要是比较AQS中共享锁和独占锁的底层实现方面的不一样。
public final void acquire(int arg){/*对外提供的独占锁的模板方法*/             public final void acquireShared(int arg){ //对外提供的共享锁的模板方式
    if(!tryAcquire(arg)                                                      if(tryAcquireShared(arg)<0)
          &&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))                         doAcquireShared(arg);
          selfInterrupt()/*中断当前调用线程*/                                }
}
先来分析acuqire(arg)方法,首先咱们要理解java中的短路运算符&&,也就是说当tryAcquire(arg)方法返回false时,即获取锁失败时,才会执行acquireQueued(addWaiter(Node.EXCLUSIVE),arg),剖开语句acquireQueued(**),先执行addWaiter(Node.EXCLUSIVE),而后执行acquireQueued(),因此一句if基本上就调用了全部的后续处理,这种编码方式,在java源码实现中很是常见。相比之下,acquireShared(arg)方法更加符合咱们平时的编码习惯。
addWaiter方法的目的是将未成功获取到锁的线程中加入到同步队列中去,先看源码:
private Node addWaiter(Node mode){                                      private Node enq(final Node node){
        Node node=new Node(Thread.currentThread(),mode);                             for(;;){
        Node pred=tail;                                                                  Node t=tail;
        if(pred!=null){                                                                  if(t==null){
            node.prev=pred;                                                                    if(compareAndSetHead(new Node()))
            if(compareAndSetTail(pred,node)){/*注意该方式是原子方式*/                                 tail=head;
               pred.next=node;                                                            }else{
               return node;                                                                    node.prev=t;
             }                                                                                 if(compareAndSetTail(t,node)){   
         }                                                                                            t.next=node;
         enq(node);                                                                                   return t;
         return node;                                                                            }
     }                                                                                     }
                                                                                       }
                                                                             }
上述的addWaiter方法首先构造一个新的节点,并先尝试插入同步队列,若是成功后,直接返回,若是不成功,则调用enq方法进行循环插入。节点既然已经被加入到同步队列中去了,那么接下来就须要将线程阻塞,阻塞以前须要再次尝试获取锁,若是仍然失败则阻塞,具体的处理方法在acquireQueued(node,arg);
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;
                    failed=false;
                    return interrupted;
                 }
                 if(shouldParkAfterFailedAcquire(p,node)&&parkAndCheckInterrupt()) 
                         interrupted=true;
             }
         }finally{
             if(failed)
                cancelAcquire(node);
         }
   }
在上述代码中,关键的一点是shouParkAfterFailedAcquire方法和parkAndCheckInterrupt方法,接下来咱们看下这两个函数的源码实现:
private static boolean shouldParkAfterFailedAcquire(Node pred,Node node){
           int ws=pred.waitStatus;
           if(ws==Node.SIGNAL) return true;// SIGNAL表示该节点的后继节点正在阻塞中,当该节点释放时,将唤醒后继节点。此时node能够安全地进行阻塞,由于能够保证会被唤醒
           if(ws>0){//表示前置节点已经被取消
               do{//循环找到一个未被取消的节点
                   node.prev=pred=pred.prev;
               }while(pred.waitStatus>0);
               pred.next=node;  //执行到这一句时,acquireQueued方法会循环一次,再次尝试获取锁
           }else{
               compareAndSetWaitStatus(pred,ws,Node.SIGNAL);
           }
           return false;
    }
规则1:若是前继的节点状态为SIGNAL,代表当前节点能够安全地进行阻塞,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将致使线程阻塞
规则2:若是前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,致使线程阻塞
规则3:若是前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同


下面咱们再来分析一下,共享锁acquireShared()方法中的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.predecessor();
                if(p==head){
                    int r=tryAcquireShared(arg);//再次尝试获取共享锁
                    if(r>=0){
                        setHeadAndPropagate(node,r);//这一句很关键
                        p.next=null;
                        if(interrupted) selfInterrupt();
                        failed=false;
                        return;
                    }
                    if(shouldParkAfterFailedAcquire(p,node)&&parkAndCheckInterrupt())//同独占锁的规则同样
                        interrupted=true;
                }
            }
        }finally{
            if(failed)
                cancelAcquire(node);
        }
    }
上面的代码中主要的一句关键代码是setHeadAndPropagate方法,主要可以调用setHeadAndPropagate方法,说明当前线程已经活到了锁,下面咱们来看看这句代码的实现:
private void setHeadAndPropagate(Node node,int propagate){
        Node h=head;
        setHead(node);//由于有多个线程可能同时获取了共享锁,setHead方法可能会设置不成功,不过已经获取了锁,也不用关心是否设置成功
        if(propagate>0||h==null||h.waitStatus<0){
            Node s=node.next;
            if(s==null||s.isShared())
             doReleaseShared();
        }
    }
独占锁某个节点被唤醒以后,它只须要将这个节点设置成head就完事了,而共享锁不同,某个节点被设置为head以后,若是它的后继节点是SHARED状态的,那么将继续经过doReleaseShared方法尝试日后唤醒节点,实现了共享状态的向后传播。
相关文章
相关标签/搜索