SPSC场景下自定义SpscArrayQueue的线程安全问题咨询
SPSC队列线程安全问题解答
你当前实现的SpscArrayQueue不是线程安全的,两个疑问点都需要做可见性、有序性保障,具体说明如下:
1. takeIndex与putIndex是否需要定义为AtomicLong类型?
不需要用全功能的AtomicLong,但不能用普通long类型,原因如下:
- 可见性问题:普通long的写入没有内存屏障保障,生产者更新
putIndex后消费者可能永远看不到新值,反过来消费者更新takeIndex后生产者也可能看不到,最终会出现生产者永远认为队列满、消费者永远认为队列空的假死问题。 - 原子性问题:32位JVM下long类型的读写不具备原子性,可能出现「字撕裂」,读取到半个更新的索引值,直接导致数组越界等异常。
- 有序性问题:普通变量的读写可能被JVM指令重排,比如
putIndex更新的操作可能被排到数组元素写入操作之前,导致消费者读到新索引后拿到空数组元素,造成数据丢失。
正确的做法有两种:
- 简单实现:给两个索引加
volatile修饰即可,因为SPSC场景下两个索引分别只有一个线程写,不需要CAS原子操作,只要保障可见性和有序性就足够。 - 高性能实现:用
AtomicLong存储索引,写入时调用lazySet方法,比volatile写入少一次内存屏障,性能更高。
2. Object[] arr是否需要替换为AtomicReferenceArray?
需要替换,普通对象数组的元素读写不具备volatile语义:
- 生产者往数组写入元素后,消费者可能读到旧值(null),哪怕生产者已经完成写入操作,会导致队列有数据但消费者读不到。
- 消费者读取元素后把对应位置设为null的操作,生产者也可能看不到,导致队列有空位但生产者认为队列满无法写入。
- 同样存在指令重排风险,数组元素的写入和索引更新的顺序可能被打乱,引发数据异常。
如果追求极致性能,也可以不用AtomicReferenceArray,直接用Unsafe的putObjectVolatile、getObjectVolatile方法操作数组元素,和原子数组效果一致,但可以省去包装类的额外开销,JCTools、Disruptor等高性能开源框架的SPSC队列都是这么实现的。
额外说明
你之前认为普通数组只会导致消费延迟、不会有异常行为的认知是错误的:没有可见性和有序性保障的情况下,极端场景下会出现队列完全不可用、数据永久丢失、运行时异常等严重问题,远不止延迟这么简单。
内容的提问来源于stack exchange,提问作者zysaaa
相关产品推荐
相关产品推荐

