需求: 我们经常使用缓存, ringBuffer 缓存 便是常见的一种数据结构。 支持高并发。 那问题来了,我们如何写个不错的ringBuffer 出来呢?
写ringbuffer 之前需要理解下下面几个点:
前提: 1. 原子变量如何保证线程安全? 比如 AtomicLong, AtomicInteger,其原子性操作,such as: updateAndGet(LongUnaryOperator updateFunction), 虽然updateFunction 是一个表达式,但是整个操作都是原子性质的。 具体如何保证,我也没详细研究。
2. 尽量使用位移来操作, 加快速度, 比如 &, >>, << 等
2. 伪共享,为什么会用到这个呢? 其实它跟CPU 缓存有关系,叫做 缓存行(cache line)。一般是64字节,需要8Long 整形的数据来填充。
缓存行 状态图 如下:
好了,那么我们看看 ringBuffer 是什么样子的:
tail 指针:
表示Producer生产的最大序号(此序号从0开始,持续递增)。Tail不能超过Cursor,即生产者不能覆盖未消费的slot。当Tail已赶上cursor.
cursor指针:
表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail。
那我们怎么实现这样的数据结构呢?
我们发现会有两个不同的线程(至少), 一个是不断的put 缓存内容,一个是不断take 缓存内容。 如果我们缓存的内容是一个Long 整型的数。
其实这个ringBuffer它就是个数组,由于数组元素在内存中是连续分配的,可以利用CPU cache来提升性能, 但是就会带来上面所说的伪共享的问题。解决的方式就是cache line 补齐的方式。
第一:
定义二个long数组: slot[],存数据,flag[] 数组
定义两个指针: tail 指针, cursor 指针, 都是原子long整型的。初始化都是-1L
put 操作:使用synchronized
1. 获取 tail 指针现在的value
2. 计算下一个 tail 执政的value 是多少
3. 将slots[] 赋上缓存的值,flags 做个 “can take flag”
4. tail 指针 加上一
第二:
take 缓存数据的 操作:
1. 获取cursor 指针现在的value
2. 和tail 只能value 比较,如果等于 tail 的value,表示 cursor 指针追赶到tail 指针,此时ringbuffer 是empty的。
3. 计算下一个slot[] 的index 是多少,获取值就可以了。
请记住 tail , cursor是long 原子整型, 他们是无限增加的,但是
第三:
cache line 补齐的方式:
,我们知道一条缓存行有 64 字节,而 Java 程序的对象头固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 VolatileLong 对象处于不同的缓存行,就避免了伪共享( 64 位系统超过缓存行的 64 字节也无所谓,只要保证不同线程不操作同一缓存行就可以)。
可以继承 AtomicLong, 子类里面定义6个Long 整型的,就可以避免伪共享的问题。
RingBuffer 确实不好写,我觉得,而且里面的操作都是以位移为多数,不好弄。但是搞好,速度会很快的。自己水平不行,记录ringbuffer 大概操作过程,以后用的着。
网友评论