在前一篇文章Java中的阻塞队列(BlockingQueue)中介绍了Java中的阻塞队列。从性能上我们能得出一个结论:数组优于链表,CAS优于锁。那么有没有一种队列,通过数组的方式实现,而且采用无锁的结构?嗯,那就是Disruptor,而且比想象中更为强大。

1. 无处不在的锁

Java中的阻塞队列采用锁来实现对临界区资源的同步访问,保证操作的线程安全。

在上一篇文章中我们知道ArrayBlockingQueue通过ReentrantLock以及它的两个condition来控制并发:

final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull; lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();

比如在压入元素的时候:

public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}

如果数组已满,则等待notfull,在enqueue中如果消费者去除元素,则会调用notFull.signal(),put方法将会被唤醒。

这种wait-notify模式很好的实现了阻塞队列。

但是在性能上因为锁的缘故,会有额外的性能消耗。

2. 无声的伪共享

从计算机的存储结构说起,在CPU和主存之间插入一个更小、更快的存储设备(例如,高速缓存存储器)已成为存储设备的一个设计主流,在计算机系统中,存储设备被组织成一个存储器层次模型,如下图(来自《深入理解计算机系统》),在此层次中,从上至下,容量越来越大,访问速度越来越慢,但是造价也更便宜。

![存储器层次模型(来自《深入理解计算机系统》)](http://images2015.cnblogs.com/blog/658141/201706/658141-20170602163117805-429263978.png)

下图是简化版的多核计算机基本结构,当CPU执行运算的时候,先去L1查找所需要的数据源,再去L2,如果这些缓存中都没有,所需的数据就得去主存中拿。

![](http://images2015.cnblogs.com/blog/658141/201706/658141-20170609003509778-1191749694.png)

下面是Martin 和 Mike的 QCon presentation 演讲中给出了一些缓存未命中的消耗数据:

今天的CPU不再是按字节访问内存,而是以64字节(64位系统)为单位的块(chunk)拿取,称为一个缓存行(cache line)。当你读一个特定的内存地址,整个缓存行将从主存换入缓存,并且访问同一个缓存行内的其它值的开销是很小的。

比如,Java中的long类型是8个字节,因此在一个缓冲行中可以存8个long类型的变量,也就是说如果访问一个long类型的数组,访问第一个元素的时候,会把另外7个也加载到缓存中,可以非常快速的遍历数组,这也是数组比链表快的原因。

美团点评技术团队写了测试程序,利用一个long型的二维数组,测试cache line的特性的效果:

public class CacheLineEffect {

    //考虑一般缓存行大小是64字节,一个 long 类型占8字节
static long[][] arr; public static void main(String[] args) {
arr = new long[1024 * 1024][];
for (int i = 0; i < 1024 * 1024; i++) {
arr[i] = new long[8];
for (int j = 0; j < 8; j++) {
arr[i][j] = 0L;
}
}
long sum = 0L;
long marked = System.currentTimeMillis();
for (int i = 0; i < 1024 * 1024; i+=1) {
for(int j =0; j< 8;j++){
sum = arr[i][j];
}
}
System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms"); marked = System.currentTimeMillis();
for (int i = 0; i < 8; i+=1) {
for(int j =0; j< 1024 * 1024;j++){
sum = arr[j][i];
}
}
System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms");
} }

运行结果:

Loop times:24ms
Loop times:97ms

缓存行利用局部性的确能提高效率,但是有一个弊端,当我们的数据不相关,只是一个单独的变量,这两个数据在一个缓存行中,而且他们的访问频率都很高,这时候反而会影响效率。如下图:

![](http://images2015.cnblogs.com/blog/658141/201706/658141-20170609000114090-1014829094.png)

比如我们有一个类存放了两个变量的值data1,data2。当加载data1的时候,data2也被加载到缓存中,也就是存在于同一个缓存行。当core1改变data1的值的时候,core1缓存中的值和内存中的值都被改变了,这时候core2也会重新加载这个缓存行,因为data1变了,而core2只是想读取自己缓存中的data2,却任然要等从内存中重新加载这个缓存行。

这种无法充分使用缓存行特性的现象,称为伪共享

3. 总结

无论是锁还是伪共享,都对我们程序的性能产生了或多或少的影响,而Disruptor很好的解决了这些问题,采用了无锁的数据结构,而且利用 cache line padding(缓存行填充)很好的解决了伪共享问题。

引用范仲淹在infoq接受采访时的语录:

我个人的观点,就看你对性能的要求有多高。如果你要达到极致的性能,对延迟要求非常低,而且对高并发要求性能非常高的时候,你肯定要选择Disruptor。但是从易用性上来讲,Disruptor使用起来并没有传统的queue使用上更方便。你在百万级别并发的时候,我推荐大家使用Java的ConcurrentQueue跟BlockingQueue。但是如果你需要更低的延迟的话,我推荐用Disruptor。

参考资料:

高性能队列——Disruptor

Disruptor入门

高性能队列Disruptor系列1--传统队列的不足的更多相关文章

  1. 高性能队列Disruptor系列2--浅析Disruptor

    1. Disruptor简单介绍 Disruptor是一个由LMAX开源的Java并发框架.LMAX是一种新型零售金融交易平台,这个系统是建立在 JVM 平台上,核心是一个业务逻辑处理器,它能够在一个 ...

  2. 高性能队列Disruptor系列3--Disruptor的简单使用(译)

    简单用法 下面以一个简单的例子来看看Disruptor的用法:生产者发送一个long型的消息,消费者接收消息并打印出来. 首先,我们定义一个Event: public class LongEvent ...

  3. 高性能队列Disruptor的使用

    一.什么是 Disruptor 从功能上来看,Disruptor 是实现了"队列"的功能,而且是一个有界队列.那么它的应用场景自然就是"生产者-消费者"模型的应 ...

  4. 高性能无锁队列 Disruptor 初体验

    原文地址: haifeiWu和他朋友们的博客 博客地址:www.hchstudio.cn 欢迎转载,转载请注明作者及出处,谢谢! 最近一直在研究队列的一些问题,今天楼主要分享一个高性能的队列 Disr ...

  5. JUC并发编程与高性能内存队列disruptor实战-上

    JUC并发实战 Synchonized与Lock 区别 Synchronized是Java的关键字,由JVM层面实现的,Lock是一个接口,有实现类,由JDK实现. Synchronized无法获取锁 ...

  6. Java队列——Disruptor 的使用

    .什么是 Disruptor  从功能上来看,Disruptor 是实现了“队列”的功能,而且是一个有界队列.那么它的应用场景自然就是“生产者-消费者”模型的应用场合了. 可以拿 JDK 的 Bloc ...

  7. Azure Messaging-ServiceBus Messaging消息队列技术系列3-消息顺序保证

    上一篇:Window Azure ServiceBus Messaging消息队列技术系列2-编程SDK入门  http://www.cnblogs.com/tianqing/p/5944573.ht ...

  8. Azure Messaging-ServiceBus Messaging消息队列技术系列4-复杂对象消息是否需要支持序列化和消息持久化

    在上一篇中,我们介绍了消息的顺序收发保证: Azure Messaging-ServiceBus Messaging消息队列技术系列3-消息顺序保证 在本文中我们主要介绍下复杂对象消息是否需要支持序列 ...

  9. Azure Messaging-ServiceBus Messaging消息队列技术系列5-重复消息:at-least-once at-most-once

    上篇博客中,我们用实际的业务场景和代码示例了Azure Messaging-ServiceBus Messaging对复杂对象消息的支持和消息的持久化: Azure Messaging-Service ...

随机推荐

  1. 浅析c++/java/c#三大热门编程语言的运行效率

    从安全角度考虑,C#是这几中语言中最为安全的,它其中定义的相关安全机制很好的确保了系统的安全... 今天和同学们一起探讨下c++/java/c# 三大热门语言的运行效率情况,以及各自的用途. 估计有很 ...

  2. Lucene工作原理

    Lucene是一个高性能的java全文检索工具包,它使用的是倒排文件索引结构.该结构及相应的生成算法如下: 0)设有两篇文章1和2 文章1的内容为:Tom lives in Guangzhou,I l ...

  3. Dora.Interception: 一个为.NET Core度身定制的AOP框架

    多年从事框架设计开发使我有了一种强迫症,那就是见不得一个应用里频繁地出现重复的代码.之前经常Review别人的代码,一看到这样的程序,我就会想如何将这些重复的代码写在一个地方,然后采用“注入”的方式将 ...

  4. ASP.NET Core实现强类型Configuration读取配置数据

    前言 实现读取JSON文件几种方式,在项目中采取老办法简单粗暴,结果老大过来一看,恩,这样不太可取,行吧那我就用.NET Core中最新的方式诺,切记,适合的才是最好的,切勿懒. .NET Core读 ...

  5. 深入浅出Node.js(一):什么是Node.js(转贴)

    以下内容转自:http://www.infoq.com/cn/articles/what-is-nodejs/ 作者:崔康 [编者按]:Node.js从2009年诞生至今,已经发展了两年有余,其成长的 ...

  6. Ionic2 + Angular4 + JSSDK开发中的若干问题汇总

    前景 目前微信公众号程序开发已经相当火热,客户要求自己的系统有一个公众号,已经是一个很常见的需要. 使用公众号可以很方便的便于项目干系人查看信息和进行互动,还可以很方便录入一些电脑端不便于录入的数据, ...

  7. O(mn)实现LCIS

    序: LCIS即求两序列的最长公共不下降子序列.思路于LCS基本一致. 用dp[i][j]记录当前最大值. 代码实现: /* About: LCIS O(mn) Auther: kongse_qi D ...

  8. IOS开发常用的基础方法

    .//退出键盘 [self.view endEditing:YES]; 隐藏手机上方的状态栏 -(BOOL)prefersStatusBarHidden{ return YES; } //获取当前控制 ...

  9. MM们,你们为什么要找一个程序猿男票?

    前言 免责声明:这篇文章关于什么?六一儿童节马上就要到了,作为一个前端攻城师,自我感觉效率还可以,老早已把任务搞完,页面布局和前端编码高效按时交付,呵呵.趁有时间,写写文章娱乐一下.MM们,请不要拿起 ...

  10. css中最基本几个选择器

    css中有四种不同的选择器 ①类选择器,又叫class选择器.类选择器{属性名:属性值:...}/*类选择器*/.s1{ font-weight:bold;font-size:16px;}②id选择器 ...