该要
前面介绍了服务端性能优化的常见思路,
在应用层,选用合适的并发编程框架,能很好的优化服务性能
disruptor是一种高性能的并发框架,提供生产者 - 消费者模式编程模式。
disruptor性能强的原因
- 数据结构层面:环形结构,数组(数组性能比链表更好),内存预加载
- 单线程写的方式,内存屏障
- 消除伪共享(填充缓存行)
- 序号栅栏和序号配合使用来消除锁和CAS
数据结构
内存预加载机制
RingBuffer的继承结构
Ringbuffer内部结构
使用数组
RingBufferFields中entries存储元素
private final Object[] entries;
内存预加载,初始化的时候,
先new 出具体的空对象放到ringbuffer中,只是不实际赋值
后续只对对象做update
在RingBufferFields的构造方法中直接调用了下图的fill方法,这个就是缓存的预加载
private void fill(EventFactory<E> eventFactory)
{
for (int i = 0; i < bufferSize; i++)
{
entries[BUFFER_PAD + i] = eventFactory.newInstance();
}
}
一直存在的一个好处,
减少gc频率,空间换时间。
初始化大小
this.entries = new Object[sequencer.getBufferSize() + 2 * BUFFER_PAD];
再填充(预加载)
for (int i = 0; i < bufferSize; i++)
{
entries[BUFFER_PAD + i] = eventFactory.newInstance();
}
注意这个初始化的空间大小和预填充的大小不一致
其实是前后各留了一个BUFFER_PAD的空间,
相当于这个entries的结构是
TODO:但是留这个空间的用途还没有领会到,先留个TODO吧。有知道的同学可以留言给我
bufferSize的约束
if (Integer.bitCount(bufferSize) != 1)
{
throw new IllegalArgumentException("bufferSize must be a power of 2");
}
内核--使用单线程写(无锁)
可以做到无锁的原因也是因为单线程写。
这个是无锁的前提
Redis和Netty其实都是单线程写。
内存优化 - 内存屏障(无锁)
对应java语言是:volatile变量和happens before语义
在linux中的内存屏障
barrier()
smp_wmb()
smp_rmb()
# rmb()不允许读操作穿过内存屏障;wmb()不允许写操作穿过屏障;而mb()二者都不允许
缓存优化,消除伪共享
缓存系统的缓存单位是 缓存行,缓存行是 2 的幂,最常见的64个字节。
V每次加载到缓存,额外加载其他数据U,导致V缓存失效,影响V的性能,是伪共享
Sequence是一个AtomicLong,标识进度,
另外还有一个目的是,不同的Sequence之间,不会出现False Sharing
Sequence的结构继承了RhsPadding
RhsPadding就是一系列的long
// 右填充
class RhsPadding extends Value
{
protected long p9, p10, p11, p12, p13, p14, p15;
}
class Value extends LhsPadding
{
protected volatile long value;
}
// 左填充
class LhsPadding
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
java long是8个字节,value在左或者右填充7个long。
如下图所示,p是填充,V是实际数值,U是其他值,前后填充7个,可以保证V一定独占一个缓存行。
算法优化 - 序号栅栏机制
long sequence = ringBuffer.next();
1 消费者序号数值必须小于生产者序号数值
2 消费者序号数值,必须小于前置(依赖关系)消费者的序号数值
3 生产者序号数值不大于消费者中最小的序号数值(避免出现消息的覆盖)
其他
Unsafe类一般不要去获取,但是一定要获取的时候,需要通过反射,
可以参考如下代码
private static final Unsafe THE_UNSAFE;
static
{
try
{
final PrivilegedExceptionAction<Unsafe> action = new PrivilegedExceptionAction<Unsafe>()
{
public Unsafe run() throws Exception
{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe) theUnsafe.get(null);
}
};
THE_UNSAFE = AccessController.doPrivileged(action);
}
catch (Exception e)
{
throw new RuntimeException("Unable to load unsafe", e);
}
}
其他,计算内存偏移的函数
THE_UNSAFE.objectFieldOffset(Value.class.getDeclaredField("aChar"));
经验数值
(1)obj : 40 (即 shallow size:遇到引用时,只计算引用的长度,不计算所引用的对象的实际大小。)
Mark Word(8) + Klass(4) + int0(4) + long0(8) + long1(8) + short0(2) + byte0(1) + Padding(1) + str0(4) = 40
经验:
字段的存储顺序和其在对象中申明的顺序并不是完全相同的。这是因为:
HotSpot 虚拟机默认的分配策略为 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配在一起。
不开启指针压缩Klass是8
32G内存以下的,默认开启对象指针压缩,4个字节
Padding(内存对齐),按照8的倍数对齐---用于补齐8的倍数,凑整;
引用类型是:4个字节,就是Oop指针;