【问题标题】:Java concurrent visibility of primitive array writes原始数组写入的 Java 并发可见性
【发布时间】:2013-02-05 18:58:40
【问题描述】:

我最近在我的代码库中发现了这个 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关系:

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

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

听起来像:

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

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

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

【问题讨论】:

  • 在Thread 2中的publisher.sync();不应该是publisher.syncChanges()吗?

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


【解决方案1】:

就可见性而言,您只需要在任何 volatile 字段上进行 volatile 读写操作。这会工作

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

【讨论】:

    【解决方案2】:

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

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

    所以,代码可以单独依赖这些方法提供的同步,将字段本身设为final。

    【讨论】:

    • 即使对于矩阵的单次更新,这个Publisher 类也什么都不做。不能保证其他线程在调用synchChanges() 后会看到更新,因为synchChanges() 不检查数据是否已发布。
    • @RussellZahniser 不是“什么都不做;”它确保更新跨线程可见。我知道它没有为读者提供任何手段来预测其在收到更新时的操作。但是,如果您在另一个线程中所做的只是显示“当前”状态,可能会定期在计时器上显示,则可能没有必要等待这种情况。虽然它不等待接收消息,但此机制确实确保发送消息。如果没有内存屏障,您甚至无法确定这一点。
    • 但它实际上并没有“确保跨线程可见更新”。读者可能会也可能不会看到更新。为了达到预期的效果,您需要像 volatile boolean isPublished 这样的东西,作者仅在完成写入时才设置 true,并且永远不会再次写入,并且读者只有在看到 true 时才会采取行动。
    • 可见和被看见是不同的。没有Publisher,更新不可见。如果不测试接收,可能会在看到更新之前发生操作。这种区别是否重要取决于应用程序。然而,说出版商什么都不做是错误的。
    • “什么都不做”我的意思是无论读者是否调用synch(),它都可能看到作者所做的部分、全部或任何更改。更新可能在不调用 synch() 的情况下可见,如果调用 synch(),它们可能也可见,但调用对此没有影响。
    【解决方案3】:

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

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

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

    见this article

    【讨论】:

    • 由于波动性,其他线程不保证读取 volatile 变量的更新值吗?这就是 volatile 的含义...来自您的文章:“Java volatile 修饰符是保证线程之间发生通信的特殊机制的一个示例。当一个线程写入 volatile 变量,而另一个线程看到该写入时,第一个线程告诉第二个内存的所有内容,直到它执行对该 volatile 变量的写入。”
    • 啊,我明白你的意思了。我不在乎读者可能会先于作者。编写器处于后台线程的无限循环中,而读取器是响应请求的 HTTP 工作者。
    【解决方案4】:

    您正确地提到了发生在关系之前的规则 #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。

    【讨论】:

    • 在任何处理发生之前进行读取是完全可以的。我不是想在这里模拟一个闩锁。写入在后台连续发生,读取来自 HTTP 工作线程池。
    猜你喜欢
    • 2016-09-23
    • 1970-01-01
    • 2012-08-22
    • 1970-01-01
    • 2010-12-26
    • 2020-10-05
    • 2013-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多