基元数组写入的 Java 并发可见性

我最近在我的代码库中发现了这个宝石:

/** This class is used to "publish" changes to a non-volatile variable.
 *
 * Access to non-volatile and volatile variables cannot be reordered,
 * so if you make changes to a non-volatile variable before calling publish,
 * they are guaranteed to be visible to a thread which calls syncChanges
 *
 */
private static class Publisher {
    //This variable may not look like it's doing anything, but it really is.
    //See the documentaion for this class.
    private volatile AtomicInteger sync = new AtomicInteger(0);

    void publish() {
        sync.incrementAndGet();
    }

    /**
     *
     * @return the return value of this function has no meaning.
     * You should not make *any* assumptions about it.
     */
    int syncChanges() {
        return sync.get();
    }
}

这是这样使用的:

线程 1

float[][] matrix;
matrix[x][y] = n;
publisher.publish();

线程 2

publisher.syncChanges();
myVar = matrix[x][y];

线程 1 是连续运行的后台更新线程。线程 2 是一个 HTTP 工作线程,它不关心它读取的内容是否以任何方式一致或原子,只关心写入“最终”到达那里,并且不会丢失为并发神。

现在,这触发了我所有的警钟。在不相关代码深处编写的自定义并发算法。

不幸的是,修复代码并非易事。Java 对并发基元矩阵的支持并不好。看起来解决这个问题的最清晰方法是使用 ,但这可能会对性能产生负面影响。显然,正确性更重要,但似乎我应该在将其从性能敏感区域中剥离出来之前证明这是不正确的。ReadWriteLock

根据 java.util.concurrent 文档,以下创建关系:happens-before

线程中的每个操作都发生在该线程中的每个操作之前,这些操作在程序顺序的后面出现。

对易失性字段的写入发生在每次后续读取该字段之前。易失性字段的写入和读取与进入和退出监视器具有相似的内存一致性效果,但不需要互斥锁定。

所以听起来像这样:

  • 矩阵写入发生之前发布() (规则 1)
  • publish() 发生在同步之前更改() (规则 2)
  • syncChanges() 发生在矩阵读取之前(规则 1)

因此,代码确实为矩阵建立了一个先发生之前链。

但我不相信。并发性很难,我不是领域专家。我错过了什么?这确实安全吗?


答案 1

在可见性方面,您所需要的只是在任何易失性字段上进行易失性写读。这将起作用

final    float[][] matrix  = ...;
volatile float[][] matrixV = matrix;

线程 1

matrix[x][y] = n;
matrixV = matrix; // volatile write

线程 2

float[][] m = matrixV;  // volatile read
myVar = m[x][y];

or simply
myVar = matrixV[x][y];

但这只适用于更新一个变量。如果写入器线程正在写入多个变量,并且读取线程正在读取它们,则读取器可能会看到不一致的图片。通常它由读写锁处理。写入时复制可能适用于某些使用模式。

Doug Lea为Java8提供了一个新的“StampedLock”http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/StampedLock.html,这是一个读写锁的版本,对于读锁定来说要便宜得多。但它也更难使用。基本上,阅读器获取当前版本,然后继续读取一堆变量,然后再次检查版本;如果版本未更改,则在读取会话期间没有并发写入。


答案 2

这对于将单个更新发布到矩阵看起来是安全的,但当然它不提供任何原子性。这是否可行取决于您的应用程序,但它可能应该记录在这样的实用程序类中。

但是,它包含一些冗余,可以通过使字段进行改进。此字段的访问是两个内存屏障中的第一个;根据合约,调用对内存的影响与对易失性变量的写入和读取具有相同的影响,并且调用与读取具有相同的效果。syncfinalvolatileincrementAndGet()get()

因此,代码可以仅依靠这些方法提供的同步,并使字段本身。final


推荐