【实战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】让普通变量也享受原子操做

相关文章
相关标签/搜索