- add: 添加元素到队列中,允许一次添加n个(原子性,要么成果,要么全部不成功),成果返回true,不成功返回false
- poll: 弹出队头,为空返回null
- peek: 返回队头元素,如果为空返回null
- isEmpty: 当前队列是否为空
- 使用CAS减少锁的开销
查阅资料
种类 | 量级 |
---|---|
Lock | 260 |
CAS | 48 |
无锁 | 2 |
因为采用重量级锁进行管理,每次都需要进入内核态,上下文切换开销交大。虽然在高度竞争的环境下,cas因为存在大量的无效操作 会性能下降,但在更多时候乐观锁的机制反而更加高校,高度竞争情况比较少见。在本场景中多线程之间只在抢夺rear指针时存在竞争 后续写入元素并无竞争,因此采用cas的方式。
-
使用自旋操作提高CAS效率 我在看java synchronzied的这个关键字时了解到了一个叫自适应自旋锁的东西,就是它自选的次数和之前成果的有关系,但是没有找到 具体的实现,我自己设计的是,一个最大尝试次数max和每次尝试次数tryNum以及修正量offset,每次尝试的次数不超过tryNum+offset 如果cas成果就将这次的自选次数更新到tryNum,这样最后tryNum就会稳定成需要的自选次数,offset是为了避免tryNum只能单调递减。 同时有一定的概率触发进行max的尝试,防止tryNum+offset仍然小于趋近值的情况。 但是在最后的实际测试中发现,一个是概率触每次生成随机数浪费了性能,其次影响自选次数的因素太多,简单的自适应自旋效果不如固定自旋 次数的方案。
-
提前分配减少竞争时间 CAS中提过,多个进程同时写时,参考了jvm内存分配方面的思路,给每个生产者分配一段内存,彼此之间写的时候互不影响,也不需要 考虑进程安全方面的问题。但是这样队列会存在间歇,因此通过增加一个与队列同样大小的available数组来标示哪些写入成功。
-
避免伪共享 在高性能这个对象中,rear和front这个指针是一直在变化的,但是诸如队列大小max_size这些是不变的。但是在64位操作系统中每一行大小为 64字节,他们可能会存储在缓存的同一行这样rear指针的失效会让max_size也需要从队列中读出来。(但在实际测试中,该优化,影响不大)
-
环形数组与预分配 提前分配好元素空间,通过环形数组进行管理,元素弹出后,后续元素直接覆盖就好,无需申请新的空间。同时如果队列较长采用链表GC时负担会特别重 环形数组通过对rear和front取模来判断在数组中位置。但是操作系统位操作更快,因此要求队列长度max_size必须为2的倍数, 然后通过(rear)&(max_size-1)来计算在数组中的位置。
- 针对大对象 如果传入队列的对象特别大,这样弹出的时候拷贝就会影响性能,可以在队列之外维护一个渐进式的hash表,以rear为key,这样队列可以只需要弹出 一个long rear号就可以继续传入新的东西了,不需要等对象完全拷贝完,同时因为hash表是渐进式的所以不会阻塞,延迟删除即可。
- 极端环境正确性测试不够
- 缺少多核环境测试(自己电脑是核数不够,开多个线程反而因为上线文切换导致性能下降)
- 缺少对比实验(网上的abs性能约为600万/秒和disruptor是2600万/秒,我是1200万/每秒左右)
刚开始拿到这个题目有点蒙蔽,因为高性能这个词让这个问题没有了具体的要求和边界,之前对提高性能也只是知道一些诸如cas,伪共享等思路或问题。 但是没有具体写过。然后开始查阅业内比较知名的高性能队列,诸如disruptor等,看懂了大部分的优化策略后,原本计划着把disruptor下下来然后 对其进行魔改。删除我不需要东西。但是发现其代码太多了,同时引入了诸如消费组(来重复消费等)等概念,是一个消息队列而不是一个纯粹的队列。 自己就尝试开始完全自己写,刚开始一上来就打算按照最终形态去写,结果因为之前同步都是用锁,对cas不熟悉,调试起来也比较麻烦,写的很痛苦,也无法跑起来。后来改变 了思路先实现了一个线程不安全,只允许每次添加单个元素的队列,然后开始一步步的往里面加我之前设计的优化策略,然后测试,调整参数。 最后就有了现在的这个版本。