1- 高性能并发框架disruptor介绍

该要

前面介绍了服务端性能优化的常见思路,
在应用层,选用合适的并发编程框架,能很好的优化服务性能
disruptor是一种高性能的并发框架,提供生产者 - 消费者模式编程模式。

disruptor性能强的原因

  • 数据结构层面:环形结构,数组(数组性能比链表更好),内存预加载
  • 单线程写的方式,内存屏障
  • 消除伪共享(填充缓存行)
  • 序号栅栏和序号配合使用来消除锁和CAS

数据结构

内存预加载机制

RingBuffer的继承结构

RingBuffer的继承结构

Ringbuffer内部结构
使用数组
RingBufferFields中entries存储元素

private final Object[] entries;

内存预加载,初始化的时候,
先new 出具体的空对象放到ringbuffer中,只是不实际赋值
后续只对对象做update

在RingBufferFields的构造方法中直接调用了下图的fill方法,这个就是缓存的预加载

private void fill(EventFactory 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的结构是


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

image.png

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一定独占一个缓存行。


image.png

算法优化 - 序号栅栏机制

long sequence = ringBuffer.next();

1 消费者序号数值必须小于生产者序号数值
2 消费者序号数值,必须小于前置(依赖关系)消费者的序号数值
3 生产者序号数值不大于消费者中最小的序号数值(避免出现消息的覆盖)

其他

Unsafe类一般不要去获取,但是一定要获取的时候,需要通过反射,
可以参考如下代码

    private static final Unsafe THE_UNSAFE;
    static
    {
        try
        {
            final PrivilegedExceptionAction action = new PrivilegedExceptionAction()
            {
                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"));

经验数值


image.png

(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指针;

你可能感兴趣的:(1- 高性能并发框架disruptor介绍)