是否可以在Java中高效地实现seqlock?

dffbzjpn  于 2023-02-07  发布在  Java
关注(0)|答案(2)|浏览(112)

另一个question让我怀疑seqlock是否可以用Java中的易失性版本计数器有效地实现。
下面是一个原型实现,在这种情况下,将永远只有一个写入线程:

class Seqlock {
  private volatile long version = 0;
  private final byte[] data = new byte[10];

  void write(byte[] newData) {
    version++;  // 1
    System.arraycopy(newData, 0, data, 0, data.length);  // 2
    version++;  // 3
  }

  byte[] read() {
    long v1, v2;
    byte[] ret = new byte[data.length];
    do {
      v1 = version; // 4
      System.arraycopy(data, 0, ret, 0, data.length);  // 5
      v2 = version; // 6
    } while (v1 != v2 || (v1 & 1) == 1);
  }
}

其基本思想是在写入之前和之后递增版本号,读者可以通过验证版本号是否相同且为偶数来检查他们获得了“一致”的读取,因为奇数表示“正在写入”。
由于版本是易变的,因此在编写器线程和读取器线程中的关键操作之间存在各种各样的发生前关系。
然而,我看不出是什么阻止了(2)处的写入移动到(1)之上,从而导致读者看到正在进行的写入。
例如,volatile读取和写入的以下同步顺序,使用每行旁边注解中的标签(还显示了非volatile的data读取和写入,因此不属于同步顺序的一部分,缩进):

1a (version is now 1)
  2a (not part of the synchronization order)
3 (version is now 2)
4 (read version == 2, happens before 3)
  5 (not part of the synchronization order)
6 (read version == 2, happens before 4 and hence 3)
1b (second write, version is now 3)
  2b (not part of the synchronization order)

ISTM表明在5(数据读取)和2b(数据第二次写入)之间没有发生,因此2b可能在读取之前发生,并读取错误数据。
如果这是真的,那么将write()声明为synchronized有帮助吗?

llmtgqce

llmtgqce1#

在java中,你可以非常简单地实现一个共享缓冲区(或其他对象):

public class SharedBuffer {

  private volatile byte[] _buf;

  public void write(byte[] buf) {
    _buf = buf;
  }

  public byte[] read() {
    // maybe copy here if you are worried about passing out the internal reference
    return _buf;
  }
}

显然,这不是“seqlock”。

ql3eal8s

ql3eal8s2#

有点晚了,不过这是我感兴趣的主题,原代码可供参考

class Seqlock {
  private volatile long version = 0;
  private final byte[] data = new byte[10];

  void write(byte[] newData) {
    version++;  //
    System.arraycopy(newData, 0, data, 0, data.length);  // 2
    version++;  // 3
  }

  byte[] read() {
    long v1, v2;
    byte[] ret = new byte[data.length];
    do {
      v1 = version; // 4
      System.arraycopy(data, 0, ret, 0, data.length);  // 5
      v2 = version; // 6
    } while (v1 != v2 || (v1 & 1) == 1);
  }
}

易失性写入实际上是一个全隔离。这是防止(2)处的写入在硬件x86级别上移的原因。在x86上,JIT为易失性写入发出LOCK指令,这使得在继续(2)处的写入之前,所有先前的写入对其他内核可见。但是,JIT编译器仍然可以自由重新排序
在上述方法write的代码中,version++是版本的volatile read,然后是volatile write。它实际上是加载,然后是存储,如下所示

//version++ 
tmp = version;
version = tmp + 1;

在(2)处System.array拷贝可以被重新排序,使得拷贝可以发生在version被写入并对其他线程可见之前。2下面的代码片段应该说明什么是可能的。

tmp = version  
    System.arraycopy(newData, 0, data, 0, data.length);
    version = tmp + 1

如果你可以使用misc.unsafe,那么确保我们有正确顺序的一种方法是使用unsafe.storeFence(),这可以确保围栏之前的读写不会与围栏之后的读写重新排序。参见下面的代码片段(2)不能移动到storeFence之上,也不能越过(3)处的volatile write(volatile write在java中有release语义

void write(byte[] newData) {
    version++;  // 1
    Unsafe.storeFence():
    System.arraycopy(newData, 0, data, 0, data.length);  // 2
    version++;  // 3
  }

read中也有类似的问题。(5)可以发生在(6)之后的地方。请参阅下面的代码片段以查看可以发生的顺序

byte[] read() {
    long v1, v2;
    byte[] ret = new byte[data.length];
    do {
      v1 = version; // 4
   
      v2 = version; // 6
    System.arraycopy(data, 0, ret, 0, data.length);  // 5
    
    } while (v1 != v2 || (v1 & 1) == 1);
  }
}

不安全的.loadFence()可以确保避免这种情况。
loadFence确保在(5)读取的数据不能移动到围栏以下。

byte[] read() {
    long v1, v2;
    byte[] ret = new byte[data.length];
    do {
      v1 = version; // 4
   
      System.arraycopy(data, 0, ret, 0, data.length);  // 5
    unsafe.loadFence()
      v2 = version; // 6
    } while (v1 != v2 || (v1 & 1) == 1);
  }
}

相关问题