【實戰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高併發程序設計》