我们已经比较完整得介绍了有关无锁的概念和使用方法。相对于有锁的方法,使用无锁的方式编程更加考验一个程序员的耐心和智力。但是,无锁带来的好处也是显而易见的,第一,在高并发的情况下,它比有锁的程序拥有更好的性能;第二,它天生就是死锁免疫的。就凭借这2个优势,就值得我们冒险尝试使用无锁的并发。

这里,我想向大家介绍一种使用无锁方式实现的Vector。通过这个案例,我们可以更加深刻地认识无锁的算法,同时也可以学习一下有关Vector实现的细节和算法技巧。(在本例中,讲述的无锁Vector来自于amino并发包)

我们将这个无锁的Vector称为LockFreeVector。它的特点是可以根据需求动态扩展其内部空间。在这里,我们使用二维数组来表示LockFreeVector的内部存储,如下:

private final AtomicReferenceArray<AtomicReferenceArray<E>> buckets;

变量buckets存放所有的内部元素。从定义上看,它是一个保存着数组的数组,也就是通常的二维数组。特别之处在于这些数组都是使用CAS的原子数组。为什么使用二维数组去实现一个一维的Vector呢?这是为了将来Vector进行动态扩展时可以更加方便。我们知道,AtomicReferenceArray内部使用Object[]来进行实际数据的存储,这使得动态空间增加特别的麻烦,因此使用二维数组的好处就是为将来增加新的元素。

此外,为了更有序的读写数组,定义一个称为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】让普通变量也享受原子操作

【实战Java高并发程序设计6】挑战无锁算法相关推荐

  1. 《实战Java高并发程序设计》github笔记和源码

    笔记 <实战Java高并发程序设计>中有很多代码范例,适合初学者通过实践入门并发编程,这本书有个问题就是前面的代码都用JDK7,第六章开始又用JDK8了 笔者做了相关笔记并整理源代码,欢迎 ...

  2. 《实战Java高并发程序设计》.pdf

    关注"Java后端技术全栈" 回复"面试"获取全套面试资料 如今,秒杀已经变得十分常见,我们也都习以为常. 然而,从技术的角度来说,秒杀对于Web系统是一个巨大 ...

  3. 实战java高并发程序设计-笔记进行中

    <JAVA并发编程实践>:出书时间太早,内容比较散,专业术语翻译较早和现在有差异 <Java并发编程的艺术>:手绘图较多文字内容较少,主要讲解并发实现的底层原理和面临的问题,底 ...

  4. 今天开始拜读一本大神写的书《实战Java高并发程序设计 》

    基本信息 书名:实战Java高并发程序设计 定价:69.00元 作者:葛一鸣著 出版社:电子工业出版社 出版日期:2015-10-01 ISBN:9787121273049 字数:493000 页码: ...

  5. java unsafe获取指针_【实战Java高并发程序设计 1】Java中的指针:Unsafe类

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

  6. 《实战 Java 高并发程序设计》笔记——第3章 JDK 并发包(二)

    文章目录 3.2 线程复用:线程池 3.2.1 什么是线程池 3.2.2 不要重复发明轮子:JDK 对线程池的支持 1. 固定大小的线程池 2. 计划任务 3.2.3 刨根究底:核心线程池的内部实现 ...

  7. 【读书笔记】实战Java高并发程序设计(第2版)读书笔记

    文章目录 1.概述 1.1. 走进并行世界 1.2 java并行程序基础 1.3 jdk并发包 1.4 锁的优化以及注意事项 1.5 并行模式与算法 1.6 java 8 9 10与并发 1.7 使用 ...

  8. 《实战Java高并发程序设计》读后感

    写在前面无关的内容 白驹过隙,看下日历已经毕业4年多,加上在大学里的4年,算算在计算机界也躺了八年,按照格拉德韦尔的1万小时定律差不多我也该成为行业的专家了,然后并没有.当看着"什么是Jav ...

  9. 最新炼数成金实战Java高并发程序设计+完整课件

    课程目录: 1.前言 2.多线程基础[vxia.net] 3.Java内存模型和线程安全 4.无锁 5.JDK并发包1[vxia.net] 6.JDK并发包2 7.并行设计模式 8.NIO和AIO[v ...

最新文章

  1. 使用MSBuild实现完整daily build流程
  2. Android Studio导入项目非常慢的解决办法
  3. java获取当前电脑的ip_使用Java获取当前计算机的IP地址
  4. java序列化流_java 序列化流与反序列化流
  5. ADB投屏_最强开源投屏神器,跨平台电脑控制+文件传输——scrcpy
  6. 我是一个*** (十四) 完
  7. Android 系统(36)---Android O、N版本修改dex2oat编译选项
  8. lazy load 图片延迟加载 跟随滚动条
  9. java通过jdbc访问数据库
  10. 印尼Widya Robotics携手华为云,让建筑工地安全看得见
  11. Python图片转base64
  12. 应聘华为的朋友小心了,应聘华为的悲惨遭遇!
  13. Linux系统和Windows系统的区别
  14. GPS的一些浅显知识兼介绍一下GPS测试仪
  15. 计算机收藏夹位于哪个磁盘,win10收藏夹在电脑什么位置_win10系统收藏夹在哪里...
  16. word文档转化html 工具mammoth
  17. Habit-基于JFinal+vue+element的后台通用模板项目
  18. 解决mac系统向日葵远控无法被远程控制问题(白屏)
  19. 反向代理原理反向代理服务器配置解决访问加速
  20. Hack ID Pro 破解门卡钥匙(你无法将我拒之门外)

热门文章

  1. nginx与php处理用户请求,配置 NGINX 处理 PHP 的请求《 LEMP 网站应用运行环境 》
  2. vue3中v-model的重大更新
  3. c 提示错误expected) before ; token_实践总结——Git 常见错误及解决方法
  4. php中reset()函数,php reset()函数 语法
  5. php soap function,从PHP SoapServer返回PHP数组
  6. DFS(深度优先遍历)走迷宫算法
  7. iframe调用父页面js方法_JS高级技巧
  8. lookup函数和vlookup_VLOOKUP和LOOKUP两个函数PK,哪个才是你心目中的查找之王?
  9. DIY高清云台(吊舱)方案
  10. [vb]全面控制 Excel