Java并发:数组的安全发布

时间:2019-01-24 11:58:58

标签: java multithreading concurrency java-memory-model

我的问题是非常基本的:一旦我通过一个或多个线程(阶段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变量。”

1 个答案:

答案 0 :(得分:3)

您的第一个示例是绝对安全的,因为任务源自writer线程。正如docs所说:

  

Runnable提交Executor之前,在线程中执行操作。 开始执行。