【实战Java高并发程序设计 1】Java中的指针:Unsafe类

【实战Java高并发程序设计 2】无锁的对象引用:AtomicReference

【实战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高并发程序设计》

【实战Java高并发程序设计6】挑战无锁算法:无锁的Vector实现的更多相关文章

  1. 【实战Java高并发程序设计 4】数组也能无锁:AtomicIntegerArray

    除了提供基本数据类型外,JDK还为我们准备了数组等复合结构.当前可用的原子数组有:AtomicIntegerArray.AtomicLongArray和AtomicReferenceArray,分别表 ...

  2. 【实战Java高并发程序设计 7】让线程之间互相帮助--SynchronousQueue的实现

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  3. 【实战Java高并发程序设计 5】让普通变量也享受原子操作

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  4. 【实战Java高并发程序设计 3】带有时间戳的对象引用:AtomicStampedReference

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference AtomicReference无法解决上述问题的根 ...

  5. 《实战Java高并发程序设计》读书笔记

    文章目录 第二章 Java并行程序基础 2.1 线程的基本操作 2.1.1 线程中断 2.1.2 等待(wait)和通知(notify) 2.1.3 等待线程结束(join)和谦让(yield) 2. ...

  6. 【实战Java高并发程序设计 1】Java中的指针:Unsafe类

    是<实战Java高并发程序设计>第4章的几点. 如果你对技术有着不折不挠的追求,应该还会特别在意incrementAndGet() 方法中compareAndSet()的实现.现在,就让我 ...

  7. 《实战java高并发程序设计》源码整理及读书笔记

    日常啰嗦 不要被标题吓到,虽然书籍是<实战java高并发程序设计>,但是这篇文章不会讲高并发.线程安全.锁啊这些比较恼人的知识点,甚至都不会谈相关的技术,只是写一写本人的一点读书感受,顺便 ...

  8. 《实战Java高并发程序设计》读书笔记三

    第三章 JDK并发包 1.同步控制 重入锁:重入锁使用java.util.concurrent.locks.ReentrantLock类来实现,这种锁可以反复使用所以叫重入锁. 重入锁和synchro ...

  9. 《实战Java高并发程序设计》读书笔记四

    第四章 锁的优化及注意事项 1.锁性能的几点建议 减小锁持有时间: 系统持有锁时间越长锁竞争程度就越激烈,只对需要同步的方法加锁,可以减小锁持有时间进而提高锁性能. 减少锁的持有时间有助于降低锁冲突的 ...

随机推荐

  1. 五星评分效果 原生js

    五星评分在很多地方都可以用到,网上也有插件或者相应的代码,在这里我给大家提供一款我自己写的超级简单实用的五星评分代码,连图片都不需要 <!-- 评分start --> <ul> ...

  2. centos7.0 下安装git(ssh方式)

    最近刚弄了个阿里云,在上面弄个git服务器,这里只弄了ssh方式访问,http方式访问的可以看我另外一个随笔http://www.cnblogs.com/hz-cww/p/6077970.html. ...

  3. ***HTML +CSS 总结与归纳

    一.首先W3C标准 结构.表现.动作  与  html.css.javascript相对应,它本意是结构表现分离,而且按照html规范编写结构. 标签方面: -所有标签都要小写.关闭.并且合理嵌套,i ...

  4. 如何让include标签包裹的布局置于屏幕最下方?

    如何让一个Layout 始终在屏幕的下方 我想让<include layout="@layout/bottom" />一直在屏幕下,怎么做? 1.相对布局中用属性  a ...

  5. Zbrush 快捷键

    1.按住shift 在空白地方移动鼠标左键 就会去到正交视图 2.shift+F可以看一下布线的情况 3.按住shift 点一下画布,松开shift键,就可以旋转画布

  6. Ubuntu服务器被黑经历(ElastichSearch漏洞)

    起因 最近我们的一台Ubuntu阿里云服务器一直提示有肉鸡行为,提示了好几天,开始并没有关注,然后连续几天后发现应该是个大问题啊.很可能服务被侵入了!!! 寻找线索 一开始我是完全懵逼的状态的,Lin ...

  7. tomcat实现域名访问步骤

    1.找到tomcat的主目录,进入conf文件夹,找到server.xml文件,并打开: 2.修改tomcat的监听端口为80端口: 3.将内容中的 localhost 替换成你想修改的IP地址或者域 ...

  8. GIT版本管理工具

    原文:http://blog.csdn.net/ithomer/article/details/7527877 Git 是一个分布式版本控制工具,它的作者 Linus Torvalds 是这样给我们介 ...

  9. 测试...外部指针访问private

    #include<iostream> using namespace std; class A{ public: int* getPointer(){ return &m; } v ...

  10. 闲来无事,写个基于UDP协议的Socket通讯Demo

    项目一期已经做完,二期需求还没定稿,所以最近比较闲. 上一篇写的是TCP协议,今天写一下UDP协议.TCP是有连接协议,所以发送和接收消息前客户端和服务端需要建立连接:UDP是无连接协议,所以发送消息 ...