並行化資源池隊列 2 —— 無鎖化的無界隊列

Java™ 5.0 第一次讓使用 Java 語言開發非阻塞算法成爲可能,java.util.concurrent 包充分地利用了這個功能。非阻塞算法屬於併發算法,它們可以安全地派生它們的線程,不通過鎖定派生,而是通過低級的原子性的硬件原生形式 —— 例如比較和交換。非阻塞算法的設計與實現極爲困難,但是它們能夠提供更好的吞吐率,對生存問題(例如死鎖和優先級反轉)也能提供更好的防禦。

在不只一個線程訪問一個互斥的變量時,所有線程都必須使用同步,否則就可能會發生一些非常糟糕的事情。Java 語言中主要的同步手段就是 synchronized 關鍵字(也稱爲內置鎖),它強制實行互斥,確保執行synchronized 塊的線程的動作,能夠被後來執行受相同鎖保護的 synchronized 塊的其他線程看到。在使用得當的時候,內置鎖可以讓程序做到線程安全,但是在使用鎖定保護短的代碼路徑,而且線程頻繁地爭用鎖的時候,鎖定可能成爲相當繁重的操作。

原子變量提供了原子性的讀-寫-修改操作,可以在不使用鎖的情況下安全地更新共享變量。原子變量的內存語義與volatile 變量類似,但是因爲它們也可以被原子性地修改,所以可以把它們用作不使用鎖的併發算法的基礎。基於這些原子化操作構建起來的併發控制算法稱爲無鎖化非阻塞算法,所謂無鎖化其實並不是不加鎖,只是所加的鎖粒度極小(指令級別或微指令級別),因此從程序本身的宏觀角度來看,就好象不使用鎖進行併發控制一樣,正因爲如此無鎖化操作,需要在CPU指令級別提供基礎支持,當前較新的CPU芯片都提供了原子化的CMPXHG指令,來實現原子化操作的支持。在Java平臺上所提供的原子化操作API,如果宿主系統提供原子化指令,那麼Java的原子化操作就會使用原子化指令實現原子化操作,反之Java的原子化操作會採用細粒度自旋鎖來實現,當然所有這些對於使用者都是透明化的。如果深入 JVM 和操作系統,會發現非阻塞算法無處不在。如垃圾收集器使用非阻塞算法加快併發和平行的垃圾蒐集;調度器使用非阻塞算法有效地調度線程和進程等。非阻塞算法要比基於鎖的算法複雜得多。開發非阻塞算法是相當專業的訓練,而且要證明算法的正確也極爲困難。但是在 Java 版本之間併發性能上的衆多改進來自對非阻塞算法的採用,而且隨着併發性能變得越來越重要,可以預見在 Java 平臺的未來發行版中,會使用更多的非阻塞算法。

採用非阻塞思想來實現無鎖化並行隊列,其最難理解的核心思想部分是“通過讓較快的線程幫助較慢的線程來防止方法調用陷入飢餓”,其次是要一直意識到非阻塞算法並不是不使用鎖,而是使用粒度極小的原子鎖。因此與前面的算法實現一樣,首先要構建隊列節點元素,但節點的next域採用原子化引用AtomicReference來直向下一個元素節點,因此隊列本身是一個包含哨兵頭結點和尾節點,以及由AtomicReference串接起來的隊列,同時每個元素節點還包含各自的節點值,這裏每個節點值採用Java泛型化類型表示。隊列節點定義代碼如下:

import java.util.concurrent.atomic.AtomicReference;  
   
public class NoLockNode<E> {  
   
   final E value;  
   final AtomicReference<NoLockNode<E>> next;  
   public NoLockNode(E item,NoLockNode<E> next){  
	   this.value=item;  
	   this.next=new AtomicReference<NoLockNode<E>>(next);  
   }  
   
}  

然後定義隊列的主體實現,其中主要包括:入隊和出對操作。

import java.util.concurrent.atomic.AtomicReference;
public class NoLockQueue<E> {
 
  //頭尾哨兵節點
  private AtomicReference<NoLockNode<E>>head,tail=null;
  //構造函數初始化哨兵節點,讓頭尾指針相等,進而構造空隊列
  public NoLockQueue(){
 
      head=new AtomicReference<NoLockNode<E>>(new NoLockNode<E>(null,null));
      tail=head;
  }
 
//入隊實現方法
  publicvoidenq(E value){
//創建將要入隊的新節點
      NoLockNode<E> node=new NoLockNode<E>(value,null);
      while(true){
//通過尾節點獲取鏈表最後一個元素節點,以及該元素節點的後繼結點。因爲入//隊操作要從隊尾進行
         NoLockNode<E> last=tail.get();
         NoLockNode<E> next=last.next.get();
		 
//判斷最後一個節點是否爲尾節點,此判斷通過只說明最後一個節點爲“疑似尾節點”,
//並不能最後定論,因爲線程在運行過程中並沒有加鎖控制,
//是無鎖化//運行的,因此在任何時刻都可能改變上一時刻的狀態
         if(last==tail.get()){
		 
//所以還要增加判斷疑似尾節點的後繼節點,以便驗明正身其確爲尾節點。
//如果疑似尾節點的後繼節點不爲空,那說明有後繼結點,
//說明上一個判斷的狀態被打破
//(不管是如何被打破的,因爲在併發情況下原因會很多),
//所以停止本次嘗試開始新一次嘗試;如果疑似尾節點的後繼節點爲空,
//那說明沒有後繼結點,此時嘗試通過原子化的CAS操作添加新節點

            if(next==null){
			
//調用CAS操作在最後一個節點的後繼上添加新節點,如果添加失敗,
//說明被其//他線程搶先添加了新節點,所以停止本次嘗試並開始一次新的嘗試

               if(last.next.compareAndSet(next,node)){
			   
//如果成功添加了新的節點,線程要第二次調用CAS操作,
//以便使tail指向新節//點。這裏是“幫助”思想的體現,這個調用可能會失敗,不過無所謂,
//因爲即//便失敗線程也能成功返回,因爲這個調用只有在其他某個線程已經設置了
//tail節點來“幫助”了本線程時纔可能失敗

                   tail.compareAndSet(last,node);
                   return;
               }
            }
         }
 
//如果最後一個元素節點還有後繼節點,那麼要方法要在插入新節點之前,
//調整tail指向最後一個節點的後繼節點,這就是“幫助”思想的一個體現。
//這裏通過調整tail,來嘗試幫助其他線程。因爲此時其他線程可能剛剛插入新節點,
//同時還沒有來得及調整tail,因此當前這個入隊線程檢測到這種情況發生後,
//要主動嘗試協助已經成功添加入隊元素的線程調整tail。
//因爲採用原子化操作調整tail因此無需等待,所以該入隊方法在這一點上實現了線性化。
//(這是原子化操作其實是粒度極小的指令級別鎖的體現)

          else{
            tail.compareAndSet(last,next);
         }
      }
  }
 
//出隊方法
  public E deq()throws Exception{
 
      E result=null;
      while(true){
//通過頭節點獲取鏈表第一個元素節點,以及該元素節點的後繼結點。
//因爲出隊操作要從隊首進行,同時還要通過隊尾節點獲取最後一個節點,用於進行相關的判斷
         NoLockNode<E> first=head.get();
         NoLockNode<E> last=tail.get();
         NoLockNode<E> next=first.next.get();
 
//判斷首節點是否爲頭節點
         if(first==head.get()){
 
//判斷首節點是否指向最後一個節點,如果是則隊列可能爲空
            if(first==last){
 
//如果head等於tail,同時head後繼爲空,說明隊列爲空,那麼拋出相關異常,//結束操作
               if(next==null){
 
                   throw new Exception("Empty Queue!!!!");
               }
 
//如果head等於tail,同時head後繼不爲空,說明隊列爲非空,同時說明tail
//的調整滯後了,因爲此時可能由於併發的其他線程成功的入隊後,但還沒有調整tail,
//這時出對線程開始了運行,所以同樣需要出隊線程來“幫助”調整//tail節點
               tail.compareAndSet(last,next);
            }
         }
 
//如果首節點沒有指向頭節點,說明頭節點說明頭節點已經被其他線程改變,
//那麼要取出首節點的節點值,同時嘗試設置頭節點指向首節點的後繼節點,
//如果設置成功,則返回節點值,即出隊成功
          else{
//取出首節點值
            result=next.value;
//嘗試設置頭節點指向首節點的後繼節點,如果設置成功,則返回節點值,即出隊成功
            if(head.compareAndSet(first,next))
               return result;
         }
      }
  } 
}



很容易看出這種實現的隊列是無鎖的。每個方法調用首先找出一個未完成的入隊操作,然後嘗試完成它。最壞的情形下,所有的線程都試圖移動隊列的tail域,且其中之一必須成功。僅當另一個線程的方法調用在改變引用中獲得成功時,一個線程纔可能在入隊或出隊一個節點時失敗,因此總會有某個方法調用成功。這種無鎖化的實現從本質上改進了隊列的性能,無鎖算法的性能比最有效的阻塞算法還要高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章