Lock的獨佔鎖和共享鎖的比較分析

haofengpingjieli發表於2019-01-19
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方法嘗試往後喚醒節點,實現了共享狀態的向後傳播。



相關文章