Java数组元素内存一致性



一个非常朴素的基于数组的循环缓冲区实现,

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实现同步,而通过volatiler/w确保内存可见性。

从并发性的角度来看有什么问题吗?当我们写入数组元素时,线程之间的内存可见性是否得到保证?

在我看来有以下问题:

  1. add()的更新不是原子
    这已经在@akarnokd和你的评论中讨论过了:1,2
    但我想再展示一个例子:

    • 给定:
      • 缓冲区大小为10
      • 50个线程执行add()
      • 1线程执行getLastN()
    • 所有50个add()线程同时完成了cursor的增量,现在正在写入holder[idx]

    因此,对于holder的每个元素,不同的线程在没有同步(即数据竞争)的情况下有5个并行写操作。
    getLastN()返回的元素可以是旧值和新写入值的任意随机组合。

  2. getLastN()不是原子的
    当你在一个线程中迭代holder的元素时,其他线程可能同时更新缓冲区。
    因此,getLastN()返回的元素可能来自"不同的代"。
    这在单线程代码中是不可能发生的——所以这也可能被认为是非线程安全的。
    (有时这样的"不一致"&;在并发算法中被认为是可接受的,但随后应该明确记录)