disruptor 笔记 - 高效率原因,为什么快?
Disruptor 为什么快?
原文学习地址:http://ifeve.com/locks-are-bad/
情景前提:
并发 -- 两个线程同时尝试修改同一个变量。
无锁情况,变量值为后修改值。且可能造成线程数据错误。
加锁:悲观锁 - 某一线线程获取变量值,直到它释放锁,另一线程才能获取变量值。 == 锁阻塞,线程越多,系统响应越慢。
乐观锁 - 某一线程获取变量值,不锁定,当修改值时才锁定。如果修改时变量值已经被修改,则重新获取变量值,再做修改。
死锁: 两个线程需要访问两个相同变量,都在锁定不同的一个变量后去获取另一个变量。
Disruptor 为什么高效率?为什么快?实现方式:
一 : Disruptor 不用锁。
在线程安全地方,用CAS(Compare And Swap/Set 比较并交换)操作,CPU 级指令【类似于乐观锁,cpu更新一个值,如果要更改的值被改变,操作失败。】,CAS操作比锁消耗资源小很多【因为不牵扯操作系统】。
Disruptor 的 ClaimStrategy(索赔策略)中,有另个策略:SingleThreadedStrategy 单线程策略,MultiThreadedStrategy 多线程策略。
单线程策略用 long,多线程策略用 AtomicLong (原子 long,是加了synchronized 的 long)
重点:在整个复杂框架中,只有一个地方出现多线程竞争修改同一个变量值。
所有访问对象都拥有序号,多线程唯一会被线程竞争写入的序号就是ClaimStrategy对象中的那个。
Disruptor 确保了Entry中每个变量都只被一个消费者写,没有写竞争,就不需要锁 或 CAS。
总结:Disruptor 优点
1、没有竞争 - 没有锁 - 非常快
2、访问者记录自己的序号实现方式,允许多个生产者与多个消费者共享相同数据结构
3、每个对象都能跟踪序列号,没有伪共享和非预期竞争。
二 : Disruptor的目标是尽可能多的在内存中运行 ,并对缓存行进行填充。
原文学习地址:http://ifeve.com/disruptor-cacheline-padding/
从CPU 到内存之间有好几层缓存。L1 一级缓存,L2 二级缓存, L3 三级缓存。
时间消耗对比:
从CPU到 |
CPU周期 |
时间 |
主存 |
|
约 60 - 80 纳秒 |
L3 cache |
约 40 - 45 cycles 周期 |
约 15 ns |
L2 cache |
约 10 cycles周期 |
约 3 ns |
L1 cache |
约3 - 4 cycle 周期 |
约 1 ns |
寄存器 |
1 cycle 周期 |
|
缓存是由缓存行组成,通常是 64 字节( 2 的整数幂个连续字节 )。(例:Java 中 long 类型是 8字节,一个缓存行可以存 8 个long 类型的变量。),内存读取每次会读取一个缓存行,然后对行内数据进行循环读取,期间会无效掉非本线程的数据并重新读取。
伪共享:两个线程下两个不同的变量被存储在同一个缓存行。单个线程写变量时会把整个缓存行都读取出来,然后无效掉非变量的数据,再重新读取。
Disruptor 数据存储会对缓存行进行填充。通过增加补全来确保 ring buffer 的序列号不会和其他东西同时存在于一个缓存中。没有伪共享,没有和其他变量的意外冲突。( 如果访问一个 long 数组,当数组中一个值被加载到缓存中,它会额外加载另外 7个,这样可以非常快速便利在连续内存块中分配的任意数据结构。)
三 :降低 内存屏障 使用
原文学习地址:http://ifeve.com/disruptor-memory-barrier/
内存屏障 是一个 CPU 指令,作用:
确保一些特定操作执行的顺序
影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器 和 CPU 可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障后,先于这个命令的必须先执行,后于这个命令的必须后执行。即 强制更新一次不同 CPU 的缓存。
Java 中的 Volatile 关键字 (内存屏障)。
使用 volatile 修饰的变量会强制将修改的值 立即写入主存。主存中值更新会使缓存中值失效。
Volatile 具有 可见性、有序性。 没有原子性【 Volatile 没有原子性,是于 synchronized 、Lock 最大的功能差异。】。
JDK 中 volatile 应用: ConcurrentHashMap 的 Entry 中的 value 和 next 被声明为 volatile;
AtomicLong 中的 value 被声明为 volatile。
内存屏障有开销(编译器/CPU 不能重拍指令,导致不能最搞笑利用 CPU,另外刷新缓存也有开销),Disruptor 的实现对序列号(多线程中序列号是 AtomicLong类型的)的读写频率尽量降到最低。