15

我最近在我的代码库中发现了这个 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 对并发原始矩阵的支持不好。看起来解决此问题的最清晰方法是使用 a ReadWriteLock,但这可能会对性能产生负面影响。显然,正确性更重要,但似乎我应该在将其从性能敏感区域中剥离出来之前证明这是不正确的。

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

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

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

所以听起来像:

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

所以代码确实已经为矩阵建立了一个happens-before链。

但我不相信。并发很难,而且我不是领域专家。我错过了什么?这真的安全吗?

4

4 回答 4

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];

但这仅适用于更新一个变量。如果写入线程正在写入多个变量,而读取线程正在读取它们,则读取器可能会看到不一致的画面。通常它由读写锁处理。Copy-on-write 可能适用于某些使用模式。

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

于 2013-02-05T19:22:31.473 回答
4

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

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

所以,代码可以单独依赖这些方法提供的同步,自己制作字段final

于 2013-02-05T19:53:29.767 回答
2

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

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

  • 所有更改,如果它从 publish() 读取更新的值
  • 没有任何更改,如果它读取旧的发布值并且没有任何更改泄漏
  • 一些变化,如果它读取之前发布的值,但是一些变化已经通过

这篇文章

于 2013-02-05T19:29:05.843 回答
1

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

对 volatile 字段的写入发生在对同一字段的每次后续读取之前。

但是,它不能保证在绝对时间轴上的 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 不关心错过的更新,但它总是想观察最新的矩阵,那么使用写时复制集合或使用上面提到的 ReaderWriteLock。
  • 如果线程 2 确实关心单个更新,那么它应该以更智能的方式处理,您可能需要考虑 wait() / notify() 模式并在矩阵更新时通知线程 2。
于 2013-02-06T06:52:51.250 回答