这是我参与 8 月更文挑战的第 13天,活动详情查看: 8月更文挑战
在开篇前我们抛出两个问题,多线程环境下是否需要尽可能扩充队列长度,多线程情况下是否一定要加锁呢,答案是否定的;
带着这两个问题今天我们了解一下强大的Disruptor框架,基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。2011年,企业应用软件专家Martin Fowler专门撰写长文介绍。同年它还获得了Oracle官方的Duke大奖,最近的更新日期就是八月份,可以说还是很活跃的,LMAX 的成立初衷是为了创建一个非常高性能的金融交易所。在它的简文中有这么一句:非常接近现代处理器在内核之间交换数据的理论极限;
好了。我们先来把上面两个问题解决掉,可能大家都知道这问题,但不知道问题的细节;先来了解一下并发流程:并发不仅仅意味着多个任务的并发发生,还意味着他们会对资源进行竞争访问,资源可能是文件,库表数据、内存空间/值等;在竞争中资源修改的可见性,有兴趣可以看下ArrayList线程不安全,JUC是如何处理的,了解一下JUC如何通过transient和volatile处理的;
对于上面的问题,我们完全可以简单暴躁的通过加锁来处理,但加锁后竞争获取需要通过内核的上下文切换来实现,而上下文切换是非常耗时的,这也是现在云服务器厂商多核处理器也是个卖点的原因,某些项目的并发线程数计算也会根据你的内核数动态生成;
说到这也许大家会立马想到CAS(compare and swap)比较交换,CAS虽然很高效的解决原子操作,可是CAS仍然存在三大问题。ABA问题,循环CAS开销过大、只能保证一个共享变量的原子操作
- ABA问题得通过版本号解决,jdk的atomic包里提供了一个类AtomicStampedReference来解决ABA问题
- 自旋长时间不成功会带来巨大资源开销如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率
- 只能保证一个变量的原子操作,但Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作;
数据任何值的最新版本在写入后的任何阶段都可以在寄存器、存储缓冲区、多层缓存之一或主内存中,如果线程要共享此值,则需要以有序的方式使其可见,这是通过缓存一致性消息的协调交换来实现的,这些消息的及时生成可以通过内存屏障来控制;
读内存屏障通过在无效队列中标记一个点来指示 CPU 上的加载指令来执行它,以便更改进入其缓存,这使它对在读取屏障之前排序的写入操作具有一致的全局性。
写屏障通过在存储缓冲区中标记一个点来命令 CPU 上的存储指令执行它,从而通过其缓存刷新写出,这个屏障提供了一个有序的视图,了解在写入屏障之前发生了什么存储操作。
完整的内存屏障对加载和存储进行排序,但仅在执行它的 CPU 上排序。volatile便是如此;
队列通常使用链表或数组作为元素的底层存储。如果允许内存中队列是无界的,它可以不受检查地增长,直到耗尽内存而导致系统崩了。生产者超过消费者时就会发生这种情况。无界队列在保证生产者不会超过消费者并且内存是宝贵资源的系统中是很有用的,但是如果这个假设不成立并且队列无限增长,则总是存在风险。所以我们通常创建线程池的时候都会定义线程大小,队列大小,java中使用ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
复制代码
队列实现往往在头、尾和大小变量上有写争用。在使用时,由于消费者和生产者之间的速度差异,队列通常总是接近满或接近空。这种总是满或总是空的倾向会导致严重的争用,即使使用不同的并发对象(例如锁或 CAS 变量)将头和尾机制分开,它们通常也占用相同的缓存行(硬件通常以固定大小字节组成缓存行。这是缓存一致性协议运行的粒度级别,也就是说如果两个变量在同一个缓存行中,且由不同的线程写入,那么它们会出现与单个变量相同的写入争用问题。这是一个被称为“false sharing”的概念。为了获得高性能,如果要最小化争用,那么确保独立但同时写入的变量不共享相同的缓存行是很重要的(小tips:对于树和链表结构由于分布广泛,CPU的内存数据预测会受到限制))。
管理声明队列头部的生产者、声明队列尾部的消费者以及中间节点的存储的问题使得并发实现的设计变得非常复杂,无法在队列上使用单个大粒度锁进行管理(如果使用大粒度锁,虽然实现容易了,但性能直线下降,如果细粒度,实现又很麻烦)。
在 Java 中,队列的使用也是垃圾的重要来源
首先,必须分配对象并将其放入队列中。
其次,如果支持链表,则必须分配代表链表节点的对象。
当不再被引用时,所有这些分配给支持队列实现的对象都需要重新声明。
上面两个问题差不多了吧,下章说说Disruptor怎么做的