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方法嘗試日後喚醒節點,實現了共享狀態的向後傳播。
相關文章
相關標籤/搜索