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

咱們已經比較完整得介紹了有關無鎖的概念和使用方法。相對於有鎖的方法,使用無鎖的方式編程更加考驗一個程序員的耐心和智力。可是,無鎖帶來的好處也是顯而易見的,第一,在高併發的狀況下,它比有鎖的程序擁有更好的性能;第二,它天生就是死鎖免疫的。就憑藉這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高併發程序設計

Center

【實戰Java高併發程序設計1】Java中的指針:Unsafe類
【實戰Java高併發程序設計2】無鎖的對象引用:AtomicReference
【實戰Java高併發程序設計 3】帶有時間戳的對象引用:AtomicStampedReference
【實戰Java高併發程序設計 4】數組也能無鎖AtomicIntegerArray【實戰Java高併發程序設計5】讓普通變量也享受原子操做

相關文章
相關標籤/搜索