在 Java 中,如何确保布尔标志的安全和一致的并发使用,同时最大限度地减少时间性能影响?

tpi*_*sch 17 java multithreading atomic volatile

在我的场景中,我的DirtyArray对象基本上是原始数组包装器,它们在发生写访问时设置一个布尔“脏”标志。

public class DirtyArray {
    private byte[] data;

    public DirtyArray(byte[] data) {
        this.data = data;
    }

    private boolean dirty = false;

    public void setValue(int index, byte value) {
        dirty = true;
        data[index] = value;
    }

    public boolean isDirty() {
        return dirty;
    }
}
Run Code Online (Sandbox Code Playgroud)

脏标志只从falsetrue

我需要确保并发使用是安全的:有一个或多个线程可以修改数组 ( setValue)。有一个或多个线程DirtyArray在它被 GC 之前捕获,并且如果它已被修改,应该将它写到磁盘上 ( isDirty)。

现在,如果我理解正确的话,像上面那样做是不安全的:实际上,从isDirty线程的角度来看,data[index]=value可以在dirty=true存储之前对存储进行重新排序。因此,看到isDirty()==false并不能保证data没有被修改。

这样对吗?

假设是,那么制作dirty标志volatile应该可以解决这个问题。但是,在以下基准测试中,我看到执行此操作时速度降低了约 50 倍至 100 倍。

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void touchAll()
{
    for (int i = 0; i < numEntities; i++)
        bytes.setValue(i, ( byte ) 2);
}
Run Code Online (Sandbox Code Playgroud)

使用 AtomicBoolean 代替 Java 9 中引入的内存排序 get/set 变体,我有这个变体:

public class DirtyArray {
    private byte[] data;

    public DirtyArray(byte[] data) {
        this.data = data;
    }

    private AtomicBoolean dirty = new AtomicBoolean();

    public void setValue(int index, byte value) {
        if (!dirty.getPlain())
            dirty.setRelease(true);
        data[index] = value;
    }

    public boolean isDirty() {
        return dirty.getAcquire();
    }
}
Run Code Online (Sandbox Code Playgroud)

它具有与原始非易失性版本相同的性能(在上述基准测试中)。

这样做安全吗?也就是说,它是否保证data修改后我会看到isDirty()==true?(在我的理解中它应该是,但只是因为dirty只从falsetrue,从不返回。)

是否还有其他变化可以实现此保证,甚至可能允许重置dirtyfalse,并且理想情况下不会对性能产生负面影响?


更新

我同意到目前为止对答案的一般评估,即保证更改的data数组和dirty标志之间一致性的唯一方法是同步setValueisDirtyPak Uula指出的竞争条件才是真正的问题,而不是让脏标志可见。所以基本上我上面问的问题是错误的问题......

有关更多上下文:这是关于在https://github.com/imglib 中存储透明缓存图像的像素。它用于非常紧密的循环,并且从同步中获取命中并不是一个真正的选择。典型的使用场景是:

  • 多个线程修改图像(由 many 支持DirtyArrays)。
  • isDirty()检查发生在另一个线程捕获 DirtyArray之前,它是垃圾收集(PhantomReference上的持有人DirtyArray),如果它是脏的,它写入磁盘。

我现在的观点是,应该在比单独setValue()调用更粗略的级别上进行处理。有一些“自然”同步点会发生,因为线程DirtyArray通过从 a 获取它们ConcurrentHashMap(当然会忽略细节)在s之间切换,线程在线程池中并从共享队列中获取作业,或者线程以其他方式互相等待。在这些同步点,较早(按程序顺序)setValue()的影响必须变得可见。所以我倾向于只使用普通的非同步版本并依赖于更粗略的同步。

唯一让我有点头疼的是清理是由垃圾收集触发的,我必须确保DirtyArray在粗级同步点之前没有收集(的持有者)。但我认为我可以通过保持强引用并在必要时添加可达性围栏来确保这一点。

Sha*_*her 8

AtomicBoolean(或任何其他原子族)不保证与不同变量的同步。所以不行。代码不保证修改数据时会得到isDirty()==true。唯一的保证是所有线程总是看到相同的 isDirty() 值。事实上,列出的选项都不能保证。

使保障的唯一途径是让上排它锁的一套方法内代码块:if语句一起与分配。这可以使用 synchronized 关键字(在方法上或代码块中)或使用其中一种锁定机制来实现java.util.concurrency