之前我们一起分析了Disruptor的初始化和启动代码,接下来我们来分析下生产者的发布代码。还不太了解的同学建议看看我之前发的Disruptor原理翻译和导读文章,尤其是一些名词概念最好要清楚是做什么用的。
1 生产者线程
生产者一般就是我们的应用线程,在发布通常使用一个EventTranslator将数据转移到RingBuffer上,因为不涉及共享数据和实例变量,通常使用同一个EventTranslator实例进行操作(注:translate经常是“翻译”的意思,但其实还有“ move from one place or condition to another.”的转移、转换的意思)。
根据同一事件传入参数的多少,可以选择不同接口接收参数。
|
|
在demo中,我们实例化并启动了两个线程,用来生产事件放置到Disruptor中。
接下来我们跟随源码一点点深入。
2 生产事件的整体逻辑
|
|
其中第二步中,在填坑完毕还要调用Sequencer接口的publish方法对外发布事件。为啥呢?先留个疑问。
在第一步占坑中,首先通过调用Sequencer.next()获取RingBuffer实例下一个能用的序号。
AbstractSequencer作为一个抽象类,实现了Sequencer接口,是单生产者Sequencer和多生产者Sequencer的父类。
3 Disruptor的核心–Sequencer接口
为什么说Sequencer是Disruptor的核心呢?其实这也不是我说的,是Disruptor官方Wiki Introduction上说的:
Sequencer是用来保证生产者和消费者之间正确、高速传递数据的。我们先来看看以生产者的角度看Sequencer有什么作用。
先来张类图。
下边是Sequencer接口及其父接口Cursored、Sequenced 定义。
3.1 单生产者发布事件
下边先看使用单生产者SingleProducerSequencer具体是怎么占坑的。
next()占坑成功将会返回坑位号,回到RingBuffer的publishEvent方法,执行translateAndPublish方法,进行填坑和发布操作。
translator参数用户定义的对EventTranslator接口的实现对象。
上文已经介绍过EventTranslator接口,除EventTranslator外,还有EventTranslatorOneArg,EventTranslatorTwoArg,EventTranslatorThreeArg,EventTranslatorVararg。功能是将给定的数据填充到指定坑位的对象(因为RingBuffer上已经预先分配了对象)上,只不过分别对应不同参数。简单看下EventTranslatorOneArg接口定义。
在放好数据后,就可以调用sequencer的publish方法发布对象了。首先是更新当前游标,更新完毕再通知等待中的消费者,消费者将继续消费。关于消费者的等待策略,后续还会讲到。
3.2 插播Disruptor中的高效AtomicLong–Sequence
注意那个cursor,这个cursor可不是简单的long类型,而是Disruptor内部实现的Sequence类。
这个Sequence其实相当于AtomicLong,最大的区别在于Sequence解决了伪共享问题。另外Sequence#set相当于AtomicLong#lazySet。
致此,使用单生产者发布事件的流程就完成了。
3.3 多生产者发布事件
如果使用的是多生产者,占坑则调用MultiProducerSequencer.next()。
可以发现,多生产者模式占坑和放置数据的逻辑和单生产者模式区别不大。区别主要是最后调用publish发布坑位的逻辑。
对比SingleProducerSequencer的publish,MultiProducerSequencer的publish没有设置cursor,而是将内部使用的availableBuffer数组对应位置进行设置。availableBuffer是一个记录RingBuffer槽位状态的数组,通过对序列值sequence取ringBuffer大小的模,获得槽位号,再通过与ringBuffer大小相除,获取序列值所在的圈数,进行设置。这里没有直接使用模运算和触发运算,而使用更高效的位与和右移操作。
其他的操作,MultiProducerSequencer和SingleProducerSequencer类似,就不再赘述了。
4 剖析SingleProducerSequencer设计
上面已经把Disruptor的主要发布事件流程过了一遍,好奇如你,必然觉得意犹未尽。如果你没有,那肯定还是我讲的有问题,不代表Disruptor本身的精彩。
接下来说一说SingleProducerSequencer的设计。从中我们可以看到Disruptor解决伪共享问题的实际代码。
SingleProducerSequencer继承了抽象类SingleProducerSequencerFields,SingleProducerSequencerFields又继承了抽象类SingleProducerSequencerPad。其中SingleProducerSequencerFields是实际放置有效实例变量的位置。
可以发现,在两个实例变量前后各有7个long型变量。为什么这样做呢?对CPU缓存有了解的同学一定知道的……对,就是为了解决伪共享问题。
CPU在加载内存到缓存行时,一个缓存行中最多只有这两个有效变量,最大限度地避免了因伪共享问题,导致缓存失效,而造成性能损失。
为了更清晰地阐述这个道理,我们尝试看一下SingleProducerSequencer实例的内存布局。
使用HSDB(HotSpot Debugger,可通过 java -cp .;”%JAVA_HOME%/lib/sa-jdi.jar” sun.jvm.hotspot.HSDB 启动)跟踪demo对应的已断点的HotSpot进程,从Object Histogram对象图中筛选出SingleProducerSequencer实例,并通过Inspector工具对SingleProducerSequencer实例进行查看。
本例中,0x00000000828026f8为com.lmax.disruptor.SingleProducerSequencer实例在JVM中的内存起始位置。以此内存地址通过mem命令查看后续的30个内存地址内容。为啥要30个呢?其实20个就够了,可以看到”Object Histogram”中SingleProducerSequencer实例的size是160字节,mem打印一行表示一字长,对应到我本机的64位机器即8字节,所以长度选择大于等于160/8=20就可以看到SingleProducerSequencer实例的内存布局全貌。
左侧红框中的地址0x0000000082802750和0x0000000082802758分别对应右侧红框中的nextValue和cachedValue两个实例变量。而在它们前后,各有7个连续的long型整数0。CPU在加载连续内存到缓存时,以缓存行为单位。缓存行通常为64B,通过占位,可以让实际变量独享一个缓存行。从而解决了伪共享问题。
缓存行查看:linux可使用以下命令查看。cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
windows可使用CPU-Z查看。
附录:JAVA对象的内存布局相关知识
最后再说点Java对象的内存布局,和本文主题关系不大,可以略过。
HotSpot对象内存布局:
HotSpot中一个对象(非数组)的内存布局大概是这样的:对象头(Mark Word + klass pointer) + 实际数据 + 为了保持8字节对齐的填充。其中对象头的Mark Word和klass pointer长度各为一机器字(machine-word),即32位机器对应32bit(4字节),64位机器对应64bit(8字节)。如64位JVM开启了指针压缩,klass pointer将压缩到4字节。
查看是否开启了指针压缩:
jinfo -flag UseCompressedOops pid 返回-XX:+UseCompressedOops即为开启,或jinfo -flags pid 查看全部选项。
此例中返回了-XX:+UseCompressedOops,表示开启了指针压缩(jdk1.8默认开启)。此时普通类型指针将被压缩为4字节。
下面通过SingleProducerSequencer举一个实际的例子。
SingleProducerSequencer属性
使用HSDB Inspector查看实例。
查看对象内存内容:
hsdb> mem 0x00000000828026f8 20
0x00000000828026f8: 0x0000000000000009 // mark word 存储对象运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳
0x0000000082802700: 0x000000082000de38 // 高4位(82802704~82802707):int bufferSize 8 ,低4位(82802700~8280273):2000de38。由于开启了指针压缩,低4位表示klass pointer,由于使用的JDK1.8,klass metadata保存在Metadataspace中。
0x0000000082802708: 0x828028e082809e98 // 高4位:ref cursor,低4位: ref waitStrategy
0x0000000082802710: 0x000000008284b390 // ref gatingSequences ObjArray
0x0000000082802718: 0x0000000000000000 // 包括当前行的以下7行 SingleProducerSequencerPad中定义的p1~p7
0x0000000082802720: 0x0000000000000000
0x0000000082802728: 0x0000000000000000
0x0000000082802730: 0x0000000000000000
0x0000000082802738: 0x0000000000000000
0x0000000082802740: 0x0000000000000000
0x0000000082802748: 0x0000000000000000
0x0000000082802750: 0x0000000000000001 // nextValue 1
0x0000000082802758: 0xffffffffffffffff // cachedValue -1
0x0000000082802760: 0x0000000000000000 // SingleProducerSequencer定义的p1~p7
0x0000000082802768: 0x0000000000000000
0x0000000082802770: 0x0000000000000000
0x0000000082802778: 0x0000000000000000
0x0000000082802780: 0x0000000000000000
0x0000000082802788: 0x0000000000000000
0x0000000082802790: 0x0000000000000000
计算此对象的Shallow Heap size 和 Retained Heap size:
可以发现此对象一共占用208=160B内存,此值即Shallow Heap size。也可以手工计算:mark_word[8] + klass_pointer[4] + 2 ref[4] + ObjArray_ref[8] + 16 * long[8] + int[4] = 160B
而保留内存大小Retained Heap size = Shallow Heap size + (当前对象的引用对象排除GC Root引用对象)的Shallow Heap size。
这里涉及到的引用为:cursor 0x00000000828028e0 ,waitStrategy 0x0000000082809e98 ,gatingSequences 0x000000008284b390。
分别使用revptrs命令查找反向引用,发现只有gatingSequences为此对象唯一引用,故计算gatingSequences(com.lmax.disruptor.Sequence[1] ) Shallow Heap size = 12 + 4 + 1 * 4 + 4 = 24B。这里由于开启了压缩指针,引用指针占用4B,此时占用20B,需要填充4B补满24B。故对象的Retained Heap size为160+24=184。
hsdb> mem 0x000000008284b390 3
0x000000008284b390: 0x0000000000000009
0x000000008284b398: 0x000000012000e08d
0x000000008284b3a0: 0x000000008284abc0
数组对象的Shallow Heap size=引用对象头大小12字节+存储数组长度的空间大小4字节+数组的长度*数组中对象的Shallow Heap size+padding大小
最后还有个问题,我们知道从Java8开始,Metaspace替代之前的PermGen存储元信息。使用Java7的HSDB是可以通过universe命令查看到PermGen信息的,而Java8就查不到Metaspace信息。
Heap Parameters:
ParallelScavengeHeap [ PSYoungGen [
eden = [0x00000000d6300000,0x00000000d66755d0,0x00000000d8300000] ,
from = [0x00000000d8300000,0x00000000d8300000,0x00000000d8800000] ,
to = [0x00000000d8800000,0x00000000d8800000,0x00000000d8d00000] ]
PSOldGen [ [0x0000000082800000,0x00000000829d79c0,0x0000000084a00000] ] ]
Disruptor生产者相关源码就分享到这,后续将对消费者一探究竟。
参考资料:
- Java对象内存布局(推荐,写的很棒) http://www.jianshu.com/p/91e398d5d17c
- JVM——深入分析对象的内存布局 http://www.cnblogs.com/zhengbin/p/6490953.html
- 借HSDB来探索HotSpot VM的运行时数据 http://rednaxelafx.iteye.com/blog/1847971
- markOop.hpp https://github.com/dmlloyd/openjdk/blob/jdk8u/jdk8u/hotspot/src/share/vm/oops/markOop.hpp
- Shallow and retained sizes http://toolkit.globus.org/toolkit/testing/tools/docs/help/sizes.html
- AtomicLong.lazySet是如何工作的? http://ifeve.com/how-does-atomiclong-lazyset-work/
- 《深入理解Java虚拟机》2.3.2 对象的内存布局