【實戰Java高併發程序設計6】挑戰無鎖算法:無鎖的Vector實現

【實戰Java高併發程序設計 1】Java中的指針:Unsafe類html

【實戰Java高併發程序設計 2】無鎖的對象引用:AtomicReferencejava

【實戰Java高併發程序設計 3】帶有時間戳的對象引用:AtomicStampedReference程序員

【實戰Java高併發程序設計 4】數組也能無鎖:AtomicIntegerArray算法

 【實戰Java高併發程序設計 5】讓普通變量也享受原子操做編程

 

     咱們已經比較完整得介紹了有關無鎖的概念和使用方法。相對於有鎖的方法,使用無鎖的方式編程更加考驗一個程序員的耐心和智力。可是,無鎖帶來的好處也是顯而易見的,第一,在高併發的狀況下,它比有鎖的程序擁有更好的性能;第二,它天生就是死鎖免疫的。就憑藉這2個優點,就值得咱們冒險嘗試使用無鎖的併發。數組

    這裏,我想向你們介紹一種使用無鎖方式實現的Vector。經過這個案例,咱們能夠更加深入地認識無鎖的算法,同時也能夠學習一下有關Vector實現的細節和算法技巧。(在本例中,講述的無鎖Vector來自於amino併發包)安全

    咱們將這個無鎖的Vector稱爲LockFreeVector。它的特色是能夠根據需求動態擴展其內部空間。在這裏,咱們使用二維數組來表示LockFreeVector的內部存儲,以下:併發

private final AtomicReferenceArray<AtomicReferenceArray<E>> buckets;

 變量buckets存放全部的內部元素。從定義上看,它是一個保存着數組的數組,也就是一般的二維數組。特別之處在於這些數組都是使用CAS的原子數組。爲何使用二維數組去實現一個一維的Vector呢?這是爲了未來Vector進行動態擴展時能夠更加方便。咱們知道,AtomicReferenceArray內部使用Object[]來進行實際數據的存儲,這使得動態空間增長特別的麻煩,所以使用二維數組的好處就是爲未來增長新的元素。函數

   此外,爲了更有序的讀寫數組,定義一個稱爲Descriptor的元素。它的做用是使用CAS操做寫入新數據。高併發

01 static class Descriptor<E> {

02    public int size;

03    volatile WriteDescriptor<E> writeop;

04    public Descriptor(int size, WriteDescriptor<E> writeop) {

05             this.size = size;

06             this.writeop = writeop;

07    }

08    public void completeWrite() {

09             WriteDescriptor<E> tmpOp = writeop;

10             if (tmpOp != null) {

11                      tmpOp.doIt();

12                      writeop = null; // this is safe since all write to writeop use

13                      // null as r_value.

14             }

15    }

16 }

17

18 static class WriteDescriptor<E> {

19    public E oldV;

20    public E newV;

21    public AtomicReferenceArray<E> addr;

22    public int addr_ind;

23

24    public WriteDescriptor(AtomicReferenceArray<E> addr, int addr_ind,

25                      E oldV, E newV) {

26             this.addr = addr;

27             this.addr_ind = addr_ind;

28             this.oldV = oldV;

29             this.newV = newV;

30    }

31

32    public void doIt() {

33             addr.compareAndSet(addr_ind, oldV, newV);

34    }

35 }

  

上述代碼第4行定義的Descriptor構造函數接收2個參數,第一個爲整個Vector的長度,第2個爲一個writer。最終,寫入數據是經過writer進行的(經過completeWrite()方法)。

第24行,WriteDescriptor的構造函數接收4個參數。第一個參數addr表示要修改的原子數組,第二個參數爲要寫入的數組索引位置,第三個oldV爲指望值,第4個newV爲須要寫入的值。

在構造LockFreeVector時,顯然須要將buckets和descriptor進行初始化。

public LockFreeVector() {

         buckets = new AtomicReferenceArray<AtomicReferenceArray<E>>(N_BUCKET);

         buckets.set(0, new AtomicReferenceArray<E>(FIRST_BUCKET_SIZE));

         descriptor = new AtomicReference<Descriptor<E>>(new Descriptor<E>(0,

                            null));

}

  在這裏N_BUCKET爲30,也就是說這個buckets裏面能夠存放一共30個數組(因爲數組沒法動態增加,所以數組總數也就不能超過30個)。而且將第一個數組的大小爲FIRST_BUCKET_SIZE爲8。到這裏,你們可能會有一個疑問,若是每一個數組8個元素,一共30個數組,那豈不是一共只能存放240個元素嗎?

    若是你們瞭解JDK內的Vector實現,應該知道,Vector在進行空間增加時,默認狀況下,每次都會將總容量翻倍。所以,這裏也借鑑相似的思想,每次空間擴張,新的數組的大小爲原來的2倍(即每次空間擴展都啓用一個新的數組),所以,第一個數組爲8,第2個就是16,第3個就是32。以此類推,所以30個數組能夠支持的總元素達到。

   這數值已經超過了2^33,即在80億以上。所以,能夠知足通常的應用。

    當有元素須要加入LockFreeVector時,使用一個名爲push_back()的方法,將元素壓入Vector最後一個位置。這個操做顯然就是LockFreeVector的最爲核心的方法,也是最能體現CAS使用特色的方法,它的實現以下:

01 public void push_back(E e) {

02    Descriptor<E> desc;

03    Descriptor<E> newd;

04    do {

05             desc = descriptor.get();

06             desc.completeWrite();

07

08             int pos = desc.size + FIRST_BUCKET_SIZE;

09             int zeroNumPos = Integer.numberOfLeadingZeros(pos);

10             int bucketInd = zeroNumFirst - zeroNumPos;

11             if (buckets.get(bucketInd) == null) {

12                      int newLen = 2 * buckets.get(bucketInd - 1).length();

13                      if (debug)

14                                System.out.println("New Length is:" + newLen);

15                      buckets.compareAndSet(bucketInd, null,

16                                         new AtomicReferenceArray<E>(newLen));

17             }

18

19             int idx = (0x80000000>>>zeroNumPos) ^ pos;

20             newd = new Descriptor<E>(desc.size + 1, new WriteDescriptor<E>(

21                                buckets.get(bucketInd), idx, null, e));

22    } while (!descriptor.compareAndSet(desc, newd));

23    descriptor.get().completeWrite();

24 }

  

能夠看到,這個方法主體部分是一個do-while循環,用來不斷嘗試對descriptor的設置。也就是經過CAS保證了descriptor的一致性和安全性。在第23行,使用descriptor將數據真正地寫入數組中。這個descriptor寫入的數據由20~21行構造的WriteDescriptor決定。

摘自《實戰Java高併發程序設計》

相關文章
相關標籤/搜索