美文网首页
并发设计模式

并发设计模式

作者: 一生逍遥一生 | 来源:发表于2020-08-16 16:18 被阅读0次

    Immutability模式: 如何利用不变形解决并发问题

    不变性:对象一旦创建之后,状态就不再发生变化。

    将一个类所有的属性都设置成final的,并且只允许存在只读方法,那个这个类基本上就具有不可变性了。

    利用享元模式避免可以减少创建对象的数量,从而减少内存占用。

    享元模式本质上就是一个对象池,在创建之前,判断对象池中是否存在,如果存在,就不创建,否则,创建。

    Long仅缓存[-128,127]之间的数字。Integer和String类型的内部使用了享元模式,导致看上去私有的锁,实际上是共有的。

    使用Immutablity模式的注意是想:

    • 1.对象的所有属性都是final的,并不能保证不可变性;
    • 2.不可变对象也需要正确发布。

    使用Immutablity模式的一定要确认保持不变性的边界在哪里,是否要求属性对象也具有不可变性。

    Copy-on-Write模式:不是延时策略的COW

    COW更多地体现的是一种延时策略,只有在真正需要复制的时候才复制,而不是提前复制。

    线程本地存储模式:没有共享,就没有伤害

    Java语言提供的显示本地存储(ThreadLocal)可以避免共享。

    ThreadLocalMap的key是Thread,在删除key的时候,要执行remove操作,防止内存泄漏。

    Guarded Suspension模式:等待唤醒机制的规范实现

    GuardedObject的内部实现非常简单,是管程的一个经典用法,get方法是通过条件变量的
    await()方法实现等待,onChanged方法通过条件变量的signalAll方法实现唤醒功能。

    Balking 模式:再谈线程安全的单例模式

    Balking模式的经典实现是使用互斥锁,也可以使用双重检查方案来优化性能。

    Thread-Per-Message模式:最简单实用的分工方法

    解决并发问题,首要问题是解决宏观的分工问题。
    可以使用Thread、Fiber来实现Thread-Per-Message模式。

    Work Thread模式:如何避免重复创建线程

    使用线程池来创建线程。

    Guava RateLimiter:高性能限流器

    Guava采用的是令牌桶算法,其核心是想要通过限流器,必须拿到令牌。
    Guava实现令牌桶算法,其关键是记录并动态计算下一个令牌方法的时间。只需要记录一下下一个令牌产生的时间,并动态更新它,就能够轻松完成限流功能。
    新产生令牌的计算公式是:(now-next)/interval。
    令牌桶算法:定时向令牌桶发送令牌,请求能够从令牌桶中拿到令牌,然后才能通过限流器;漏桶算法:请求会像谁一样注入漏桶,
    漏桶会按照一定的速率自动将水漏掉,只有漏桶里还能注入水的时候,请求只能通过限流器。

    高效性能队列Disruptor

    Disruptor的特点:

    • 内存分配更合理,使用RingBuffer数据结构,数组元再初始化时一次性全部创建,提高缓存命中率;对象循环利用,避免频繁GC。
    • 能够避免伪共享,提升缓存利用率。
    • 采用无锁算法,避免频繁加锁、解决的性能消耗。
    • 支持批量消费,消费者可以无锁方法消费多个消息。

    RingBuffer

    RingBuffer是一个环(首位相接的环),RingBuffer拥有一个序号,这个序号指向数组中下一个可用的元素。随着不停的填充这个buffer,这个序号
    会一直增长,直到绕过这个环。要找到数组中当前序号指向的元素,可以通过mod操作,槽的个数是2的N次方更有利于基于二进制的计算机进行计算。
    RingBuffer与常用队列的区别是:不删除buffer中的数据,数据一直存在buffer中,直到新的数据覆盖他们。这是不需要尾指针的原因。
    RingBuffer底层结构是数组,要比链表快,而且有一个容易预测的访问模式。(数组内元素的内存地址的连续性存储的)。
    这是对CPU缓存友好的—也就是说,在硬件级别,数组中的元素是会被预加载的,因此在ringbuffer当中,cpu无需时不时去主存加载数组中的下一个元素。
    (因为只要一个元素被加载到缓存行,其他相邻的几个元素也会被加载进同一个缓存行)。
    其次,你可以为数组预先分配内存,使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收。
    此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。

    避免伪共享

    缓存是由缓存行组成的,通常是64字节(这篇文章发表时常用处理器的缓存行是64字节的,比较旧的处理器缓存行是32字节),
    并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。

    当加载head时,也会加载tail,消费者更新了head的值。缓存中的值和内存中的值都被更新了,而其他所有存储head的缓存行都会都会失效,
    因为其它缓存中head不是最新值了。请记住我们必须以整个缓存行作为单位来处理(这是CPU的实现所规定的,详细可参见深入分析Volatile的实现原理),
    不能只把head标记为无效。如果这样的话,就是伪共享。
    Disruptor通过增加补全来确保Ring Buffer的序列号不会和其他东西同时存在于一个缓存行中。

    无锁算法

    使用CAS代替锁,效率高。所有访问者都记录自己的序号的实现方式,允许多个生产者与多个消费者共享相同的数据结构(这样就不需要使用锁)。
    在每个对象中都能跟踪序列号(Ring Buffer,Claim Strategy,生产者和消费者),加上神奇的cache line padding,
    就意味着没有为伪共享和非预期的竞争。

    Volatile 的实现原理

    Java代码 instance = new Singleton(); //instance是volatile变量
    汇编代码 0x01a3de1d: movb 0x0,0x1104800(%esi);0x01a3de24: lock addl0x0,(%esp);

    如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
    但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,
    就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,
    就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

    volatile优化:用一种追加字节的方式来优化队列出队和入队的性。
    不是所有的Volatile变量时都应该追加到64字节。
    在两种场景下不应该使用追加字节这种方式。第一:缓存行非64字节宽的处理器,如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个字节宽。
    第二:共享变量不会被频繁的写。

    伪共享现象

    当多个线程操作不同的变量,这些变量在同一个缓存行。
    在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。
    如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。
    这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

    对于HotSpot JVM,所有对象都有两个字长的对象头。第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word。
    第二个字是对象所属类的引用。如果是数组对象还需要一个额外的字来存储数组的长度。每个对象的起始地址都对齐于8字节以提高性能。
    因此当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序:
    doubles (8) 和 longs (8)
    ints (4) 和 floats (4)
    shorts (2) 和 chars (2)
    booleans (1) 和 bytes (1)
    references (4/8)

    内存屏障

    内存屏障就是一个CPU指令。这样的指令:a)确保一些特定操作执行的顺序; b)影响一些数据的可见性(可能是某些指令执行后的结果)。

    内存屏障的作用:编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化;强制更新一次不同CPU的缓存。
    内存屏障作为另一个CPU级的指令,没有锁那样大的开销。内核并没有在多个线程间干涉和调度。但凡事都是有代价的。
    内存屏障的确是有开销的——编译器/cpu不能重排序指令,导致不可以尽可能地高效利用CPU,另外刷新缓存亦会有开销。

    参考文献

    Disruptor核心(一) 快速上手
    Disruptor核心(二) RingBuffer
    并发框架DISRUPTOR译文
    Disruptor核心(三) Sequence Sequencer SequenceBarrier
    Disruptor核心(四)WaitStrategy
    Disruptor核心(五) EventProcessor & WorkProcessor
    Disruptor高级(一)互联网系统中的核心链路
    Disruptor高级(二)串行消费 & 并行消费
    Disruptor高级(三)菱形消费
    Disruptor高级(四)六边形消费
    Disruptor高级(五)多生产者多消费者
    Disruptor高级(六)高性能的核心思想
    Disruptor源码(一)RingBuffer底层结构 & 构建过程
    Disruptor源码(二)填充缓存行消除伪共享
    Disruptor源码(三)生产者怎么知道在哪里“下蛋”?
    为什么Disruptor会那么快?
    强如 Disruptor 也发生内存溢出?
    伪共享(False Sharing)

    相关文章

      网友评论

          本文标题:并发设计模式

          本文链接:https://www.haomeiwen.com/subject/feotjktx.html