【问题标题】:Java concurrency: safe publication of arrayJava并发:数组的安全发布
【发布时间】:2019-01-24 11:58:58
【问题描述】:

我的问题非常基本:一旦我通过一个或多个线程写入了一些数组值(阶段 1),我如何“发布”我的数组以使所有更改对其他线程可见(阶段 2)?

我有代码先写所有数组,然后读所有数组,然后再写所有,然后再读,等等。我想在多个线程中进行,所以首先要使用多个线程数组写入阶段,然后多个线程将执行数组读取阶段等。
我关心的是如何在每个写入阶段之后安全地发布数组写入。

考虑以下简化的线程不安全代码,它只使用一个线程执行一个写入阶段,然后使用多个线程执行一个读取阶段:

    ExecutorService executor = Executors.newFixedThreadPool(5);
    double[] arr = new double[5];
    for (int i=0; i<5; ++i) {
        arr[i] = 1 + Math.random();
    }
    for (int i=0; i<5; ++i) {
        final int j=i;
        executor.submit(() -> System.out.println(String.format("arr[%s]=%s", j, arr[j])));
    }

代码通常会打印非零值,但我知道它有时也会打印零,因为写入线程未正确发布数组,因此某些写入可能对其他线程不可见。

我想解决这个问题并以线程安全的方式正确编写上述代码,即确保我的所有写入对读取线程都是可见的。

1.你能建议最好的方法吗?
由于性能(以及代码清晰度),并发集合和 AtomicXxxArray 对我来说不是一个选项,因为我有 2D 数组等。

2。我可以想到以下可能的解决方案,但我不是 100% 确定它们会起作用。您能否就以下解决方案提出建议?

解决方案 1: 分配给最终数组
理由:我希望最终字段始终使用最新的写入正确初始化,包括其所有递归依赖项。

    for (int i=0; i<5; ++i) {
        arr[i] = 1 + Math.random();
    }
    final double[] arr2 = arr;  //<---- safe publication?
    for (int i=0; i<5; ++i) {
        final int j=i;
        executor.submit(() -> System.out.println(String.format("arr[%s]=%s", j, arr2[j])));
    }

解决方案 2:闩锁
理由:我希望闩锁在写入线程和读取线程之间建立完美的先发生关系。

    CountDownLatch latch = new CountDownLatch(1); //1 = the number of writing threads
    for (int i=0; i<5; ++i) {
        arr[i] = Math.random();
    }
    latch.countDown();    //<- writing is done
    for (int i=0; i<5; ++i) {
        final int j=i;
        executor.submit(() -> {
            try {latch.await();} catch (InterruptedException e) {...} //happens-before(writings, reading) guarantee?
            System.out.println(String.format("arr[%s]=%s", j, arr[j]));
        });
    }

更新:这个答案https://stackoverflow.com/a/5173805/1847482 建议以下解决方案:

volatile int guard = 0;
...
//after the writing is done:
guard = guard + 1; //write some new value

//just before the reading: read the volatile variable, e.g.
guard = guard + 1; //includes reading
... //do the reading

此解决方案使用以下规则:“如果线程 A 写入了一些非易失性内容和一个易失性变量之后,那么线程 B 保证也能看到易失性内容的变化,如果它先读取 volatile 变量”。

【问题讨论】:

  • 为什么不创建自己的线程安全对象来操作数组?
  • 因为我不想为线程争用付费。对我来说,同步访问大型数组的成本太高了。
  • 所有线程都必须同时写入同一个数组吗?难道不能在每个线程中创建一个本地数组,然后在所有线程完成写入后将它们同步合并为一个吗?
  • 是的,这可能是一种选择,但无论如何写作都不是问题。我最终可以在一个线程中编写,好的。但是我需要以某种方式安全地发布数组,以便读取线程可以看到更改。请参阅我的代码示例 - 它已经只有一个编写线程。
  • ReadWriteLock附加到数组,线程写入或读取数组必须先获得锁。

标签: java multithreading concurrency java-memory-model


【解决方案1】:

您的第一个示例非常安全,因为任务源自编写器线程。正如docs 所说:

在将Runnable 提交给Executor 之前线程中的操作发生之前开始执行。

【讨论】:

  • 很好的链接,谢谢!实际上,即使我让我的示例更复杂一点并且不使用提交 Runnable 的同一线程编写,您的链接也会给出答案:要成功“发布”我的更改,我只需要使用任何现有的同步结构数组写入和读取之间,如CountDownLatch、lock、Phaser等
猜你喜欢
  • 1970-01-01
  • 2017-10-08
  • 1970-01-01
  • 2013-11-14
  • 1970-01-01
  • 2020-08-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多