原始数组写入的Java并发可见性

时间:2013-02-05 18:58:40

标签: java concurrency memory-barriers java-memory-model

我最近在我的代码库中找到了这个gem:

/** 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,但这可能会对性能产生负面影响。显而易见,正确性更为重要,但似乎我应该在将其从性能敏感区域中删除之前证明这是 正确。

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

  

线程中的每个动作都发生在该线程中的每个动作之前,该动作在程序的顺序中稍后出现。

     

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

所以听起来像是:

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

因此代码确实为矩阵建立了一个先发条件链。

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

4 个答案:

答案 0 :(得分:4)

就可见性而言,您需要的是在任何易失性字段上的易读写入。这可以工作

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,它是一个读写锁的版本,对于读锁来说要便宜得多。但它也很难使用。基本上读者获取当前版本,然后继续读取一堆变量,然后再次检查版本;如果版本没有更改,则在读取会话期间没有并发写入。

答案 1 :(得分:4)

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

但是,它包含一些冗余,可以通过制作sync字段final来改进。此字段的volatile访问权限是两个内存屏障中的第一个;通过契约,调用incrementAndGet()对内存的影响与写入和读取volatile变量的影响相同,调用get()与读取具有相同的效果。

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

答案 2 :(得分:2)

使用volatile并不是同步所有内容的灵丹妙药。保证如果另一个线程读取volatile变量的更新值,它们也会看到在此之前对非volatile变量进行的每个更改。但没有什么可以保证其他线程会读取更新后的值

在示例代码中,如果您对matrix进行多次写入然后调用publish(),而另一个线程调用synch()然后读取矩阵,那么另一个线程可能会看到一些,全部或没有变化:

  • 所有更改,如果它从publish()
  • 读取更新的值
  • 没有任何更改,如果它读取旧的已发布值且没有任何更改泄漏
  • 一些更改,如果它读取以前发布的值,但某些更改已泄露

请参阅this article

答案 3 :(得分:1)

你正确地提到了之前关系发生的规则#2

  

在对该相同字段的每次后续读取之前发生对易失性字段的写入。

但是,它不保证在绝对时间轴上的syncChanges()之前调用publish()。让我们稍微改变你的例子。

主题1:

matrix[0][0] = 42.0f;
Thread.sleep(1000*1000); // assume the thread was preempted here
publisher.publish(); //assume initial state of sync is 0 

主题2:

int a = publisher.syncChanges();
float b = matrix[0][0];

a和b变量的选项有哪些?

  • a为0,b可以为0或42
  • a是1,b因为之前发生的关系而是42
  • a大于1(由于某种原因,线程2很慢而线程1很幸运地多次发布更新),b的值取决于业务逻辑和矩阵的处理方式 - 它是否依赖于先前的状态或不?

如何处理?这取决于业务逻辑。

  • 如果线程2不时地轮询矩阵的状态,并且在它们之间有一些过时的值是完全正确的,如果最终将处理正确的值,则保持原样。
  • 如果线程2不关心错过的更新,但它总是想要观察最新的矩阵,那么使用copy-on-write集合或使用上面提到的ReaderWriteLock。
  • 如果线程2确实关心单个更新,那么它应该以更智能的方式处理,您可能需要考虑wait()/ notify()模式并在更新矩阵时通知线程2。