一个非常朴素的基于数组的循环缓冲区实现,
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class SomeRingBuffer {
private volatile String[] holder;
private AtomicInteger cursor = new AtomicInteger(-1);
public SomeRingBuffer(int capacity) {
holder = new String[capacity];
}
public void add(String data) {
if (data == null) {
return; // do nothing
}
int updatedCursor = cursor.updateAndGet(i -> (i + 1) % holder.length);
holder[updatedCursor] = data;
holder = holder; // holder is marked volatile, intentional write barrier
}
public List<String> getLastN(int n) {
if (n < 1 || n > holder.length) {
n = holder.length;
}
List<String> result = new ArrayList<>(n);
int current = cursor.get();
while (n > 0) {
result.add(holder[current--]);
if (current < 0) {
current = holder.length - 1;
}
n--;
}
return result;
}
}
它应该是非阻塞和线程安全的。使用AtomicInteger
实现同步,而通过volatile
r/w确保内存可见性。
从并发性的角度来看有什么问题吗?当我们写入数组元素时,线程之间的内存可见性是否得到保证?
在我看来有以下问题:
-
add()
的更新不是原子
这已经在@akarnokd和你的评论中讨论过了:1,2
但我想再展示一个例子:- 给定:
- 缓冲区大小为10
- 50个线程执行
add()
- 1线程执行
getLastN()
- 所有50个
add()
线程同时完成了cursor
的增量,现在正在写入holder[idx]
因此,对于
holder
的每个元素,不同的线程在没有同步(即数据竞争)的情况下有5个并行写操作。getLastN()
返回的元素可以是旧值和新写入值的任意随机组合。 - 给定:
-
getLastN()
不是原子的
当你在一个线程中迭代holder
的元素时,其他线程可能同时更新缓冲区。
因此,getLastN()
返回的元素可能来自"不同的代"。
这在单线程代码中是不可能发生的——所以这也可能被认为是非线程安全的。
(有时这样的"不一致"&;在并发算法中被认为是可接受的,但随后应该明确记录)