咱們已經比較完整得介紹了有關無鎖的概念和使用方法。相對於有鎖的方法,使用無鎖的方式編程更加考驗一個程序員的耐心和智力。可是,無鎖帶來的好處也是顯而易見的,第一,在高併發的狀況下,它比有鎖的程序擁有更好的性能;第二,它天生就是死鎖免疫的。就憑藉這2個優點,就值得咱們冒險嘗試使用無鎖的併發。程序員
這裏,我想向你們介紹一種使用無鎖方式實現的Vector。經過這個案例,咱們能夠更加深入地認識無鎖的算法,同時也能夠學習一下有關Vector實現的細節和算法技巧。(在本例中,講述的無鎖Vector來自於amino併發包)算法
咱們將這個無鎖的Vector稱爲LockFreeVector。它的特色是能夠根據需求動態擴展其內部空間。在這裏,咱們使用二維數組來表示LockFreeVector的內部存儲,以下:編程
private final AtomicReferenceArray<AtomicReferenceArray<E>> buckets;
變量buckets存放全部的內部元素。從定義上看,它是一個保存着數組的數組,也就是一般的二維數組。特別之處在於這些數組都是使用CAS的原子數組。爲何使用二維數組去實現一個一維的Vector呢?這是爲了未來Vector進行動態擴展時能夠更加方便。咱們知道,AtomicReferenceArray內部使用Object[]來進行實際數據的存儲,這使得動態空間增長特別的麻煩,所以使用二維數組的好處就是爲未來增長新的元素。segmentfault
此外,爲了更有序的讀寫數組,定義一個稱爲Descriptor的元素。它的做用是使用CAS操做寫入新數據。數組
static class Descriptor<E> { public int size; volatile WriteDescriptor<E> writeop; public Descriptor(int size, WriteDescriptor<E> writeop) { this.size = size; this.writeop = writeop; } public void completeWrite() { WriteDescriptor<E> tmpOp = writeop; if (tmpOp != null) { tmpOp.doIt(); writeop = null; // this is safe since all write to writeop use // null as r_value. } } } static class WriteDescriptor<E> { public E oldV; public E newV; public AtomicReferenceArray<E> addr; public int addr_ind; public WriteDescriptor(AtomicReferenceArray<E> addr, int addr_ind, E oldV, E newV) { this.addr = addr; this.addr_ind = addr_ind; this.oldV = oldV; this.newV = newV; } public void doIt() { addr.compareAndSet(addr_ind, oldV, newV); } }
上述代碼第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使用特色的方法,它的實現以下:
public void push_back(E e) { Descriptor<E> desc; Descriptor<E> newd; do { desc = descriptor.get(); desc.completeWrite(); int pos = desc.size + FIRST_BUCKET_SIZE; int zeroNumPos = Integer.numberOfLeadingZeros(pos); int bucketInd = zeroNumFirst - zeroNumPos; if (buckets.get(bucketInd) == null) { int newLen = 2 * buckets.get(bucketInd - 1).length(); if (debug) System.out.println("New Length is:" + newLen); buckets.compareAndSet(bucketInd, null, new AtomicReferenceArray<E>(newLen)); } int idx = (0x80000000>>>zeroNumPos) ^ pos; newd = new Descriptor<E>(desc.size + 1, new WriteDescriptor<E>( buckets.get(bucketInd), idx, null, e)); } while (!descriptor.compareAndSet(desc, newd)); descriptor.get().completeWrite(); }
能夠看到,這個方法主體部分是一個do-while循環,用來不斷嘗試對descriptor的設置。也就是經過CAS保證了descriptor的一致性和安全性。在第23行,使用descriptor將數據真正地寫入數組中。這個descriptor寫入的數據由20~21行構造的WriteDescriptor決定。
摘自:實戰Java高併發程序設計
【實戰Java高併發程序設計1】Java中的指針:Unsafe類
【實戰Java高併發程序設計2】無鎖的對象引用:AtomicReference
【實戰Java高併發程序設計 3】帶有時間戳的對象引用:AtomicStampedReference
【實戰Java高併發程序設計 4】數組也能無鎖AtomicIntegerArray【實戰Java高併發程序設計5】讓普通變量也享受原子操做