摘要:在本例中,讲述的无锁来自于并发包我们将这个无锁的称为。在这里,我们使用二维数组来表示的内部存储,如下变量存放所有的内部元素。为什么使用二维数组去实现一个一维的呢这是为了将来进行动态扩展时可以更加方便。
我们已经比较完整得介绍了有关无锁的概念和使用方法。相对于有锁的方法,使用无锁的方式编程更加考验一个程序员的耐心和智力。但是,无锁带来的好处也是显而易见的,第一,在高并发的情况下,它比有锁的程序拥有更好的性能;第二,它天生就是死锁免疫的。就凭借这2个优势,就值得我们冒险尝试使用无锁的并发。
这里,我想向大家介绍一种使用无锁方式实现的Vector。通过这个案例,我们可以更加深刻地认识无锁的算法,同时也可以学习一下有关Vector实现的细节和算法技巧。(在本例中,讲述的无锁Vector来自于amino并发包)
我们将这个无锁的Vector称为LockFreeVector。它的特点是可以根据需求动态扩展其内部空间。在这里,我们使用二维数组来表示LockFreeVector的内部存储,如下:
private final AtomicReferenceArray> buckets;
变量buckets存放所有的内部元素。从定义上看,它是一个保存着数组的数组,也就是通常的二维数组。特别之处在于这些数组都是使用CAS的原子数组。为什么使用二维数组去实现一个一维的Vector呢?这是为了将来Vector进行动态扩展时可以更加方便。我们知道,AtomicReferenceArray内部使用Object[]来进行实际数据的存储,这使得动态空间增加特别的麻烦,因此使用二维数组的好处就是为将来增加新的元素。
此外,为了更有序的读写数组,定义一个称为Descriptor的元素。它的作用是使用CAS操作写入新数据。
static class Descriptor{ public int size; volatile WriteDescriptor writeop; public Descriptor(int size, WriteDescriptor writeop) { this.size = size; this.writeop = writeop; } public void completeWrite() { WriteDescriptor 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 { public E oldV; public E newV; public AtomicReferenceArray addr; public int addr_ind; public WriteDescriptor(AtomicReferenceArray 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>(N_BUCKET); buckets.set(0, new AtomicReferenceArray (FIRST_BUCKET_SIZE)); descriptor = new AtomicReference >(new Descriptor (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) { Descriptordesc; Descriptor 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 (newLen)); } int idx = (0x80000000>>>zeroNumPos) ^ pos; newd = new Descriptor (desc.size + 1, new WriteDescriptor ( 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】让普通变量也享受原子操作
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/65760.html
摘要:实战高并发程序设计连载中的指针类和非常类似,不同之处就在于是对整数的封装,而则对应普通的对象引用。这样,当前线程就无法正确判断这个对象究竟是否被修改过。摘自实战高并发程序设计一书 【实战Java高并发程序设计】连载1–Java中的指针:Unsafe类 AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而Atomi...
摘要:当前可用的原子数组有和,分别表示整数数组型数组和普通的对象数组。摘自实战高并发程序设计一书实战高并发程序设计中的指针类实战高并发程序设计无锁的对象引用实战高并发程序设计带有时间戳的对象引用 除了提供基本数据类型外,JDK还为我们准备了数组等复合结构。当前可用的原子数组有:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分别...
摘要:有时候,由于初期考虑不周,或者后期的需求变化,一些普通变量可能也会有线程安全的需求。它可以让你在不改动或者极少改动原有代码的基础上,让普通的变量也享受操作带来的线程安全性,这样你可以修改极少的代码,来获得线程安全的保证。 有时候,由于初期考虑不周,或者后期的需求变化,一些普通变量可能也会有线程安全的需求。如果改动不大,我们可以简单地修改程序中每一个使用或者读取这个变量的地方。但显然,这...
摘要:相比与其他操作系统包括其他类系统有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。因为多线程竞争锁时会引起上下文切换。减少线程的使用。很多编程语言中都有协程。所以如何避免死锁的产生,在我们使用并发编程时至关重要。 系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)syn...
摘要:通过指令重排可以减少流水线停顿提升巨大效率原则程序顺序原则一个线程内保证语义的串行性。锁规则解锁必然发生在随后的加锁前。线程的方法先于它的每一个动作。 1. 基本概念 同步(Synchronous)和异步(Asynchronous) 并发(Conncurrency)和并行(Parallelism) 临界区 阻塞(Blocking)与非阻塞(Non-Blocking) 死锁(Deadl...
阅读 3946·2021-10-09 09:43
阅读 2875·2021-10-08 10:05
阅读 2735·2021-09-08 10:44
阅读 886·2019-08-30 15:52
阅读 2812·2019-08-26 17:01
阅读 3019·2019-08-26 13:54
阅读 1654·2019-08-26 10:48
阅读 809·2019-08-23 14:41