如何使对其他线程可见的数组写入

时间:2016-08-29 09:06:30

标签: java arrays multithreading concurrency

我有一个基本类型int的输入数组,我想使用多个线程处理这个数组,并将结果存储在相同类型和大小的输出数组中。以下代码在内存可见性方面是否正确?

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ArraySynchronization2
{
    final int width = 100;
    final int height = 100;

    final int[][] img = new int[width][height];
    volatile int[][] avg = new int[width][height];

    public static void main(String[] args) throws InterruptedException, ExecutionException
    {
        new ArraySynchronization2().doJob();;
    }

    private void doJob() throws InterruptedException, ExecutionException
    {
        final int threadNo = 8;
        ExecutorService pool = Executors.newFixedThreadPool(threadNo);

        final CountDownLatch countDownLatch = new CountDownLatch(width - 2);

        for (int x = 1; x < width - 1; x++)
        {
            final int col = x;
            pool.execute(new Runnable()
            {
                public void run()
                {
                    for (int y = 0; y < height; y++)
                    {
                        avg[col][y] = (img[col - 1][y] + img[col][y] + img[col + 1][y]) / 3;
                    }
                    // how can I make the writes to the data in avg[][] visible to other threads? is this ok?
                    avg = avg;
                    countDownLatch.countDown();
                };
            });
        }

        try
        {
            // Does this make any memory visibility guarantees?
            countDownLatch.await();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }

        // can I read avg here, will the results be correct?
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                System.out.println(avg[x][y]);
            }
        }

        pool.shutdown();
        pool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);

        // now I know tasks are completed and results synchronized (after thread death), but what if I plan to reuse the pool?
    }
}

我不想在CountDownLatch上进行同步。我想知道如何使输出数组的写入对其他线程可见。假设我有一个我想要处理的数组(例如图像),我可以在多个单独的任务中执行此操作,这些任务将输入数组的块处理到输出数组中,写入之间没有相互依赖关系。输出。完成所有计算后,我希望输出数组中的所有结果都可以读取。我怎么能实现这样的行为?我知道使用submit和Future.get()代替执行是可以实现的,我想知道如何正确实现这种低级机制?另请参阅代码附近的评论中提出的问题。

2 个答案:

答案 0 :(得分:3)

嗯,只是想知道你是否真的需要一个闩锁。数组本身是内存中的保留块,每个单元都是专用的内存地址。 (顺便说一下,将它标记为volatile只会将引用标记为数组为volatile,而不是数组的单元格,请参阅here)。因此,只有当多个线程写入访问同一个单元时,才需要协调对单元的访问。

问题是,你真的这样做了吗?或者目标应该是:尽可能避免协调访问,因为这是有代价的。

在你的算法中,你对行进行操作,那么为什么不对行进行并行化,这样每个线程只读取&amp;计算整个数组的行分段值并忽略其他行?

  • thread-0 - &gt;第0,8,15行......
  • thread-1 - &gt;第1,9,16行......
  • ...

基本上这个(没有经过测试):

for (int n = 0; n < threadNo; n++)  { //each n relates to a thread
    pool.execute(new Runnable() {
        public void run() {
            for (int row = n; row < height; row += threadNo) { //proceed to the next row for the thread
                for (int col = 1; col < width-1; col++) {
                    avg[col][row] = (img[col - 1][row] + img[col][row] + img[col + 1][row]) / 3;
                   }
                }
            };
        });
    }

因此,他们可以在整个阵列上运行而无需进行同步。通过在关闭池之后将循环打印出结果将确保所有计算线程都已完成,并且唯一必须等待的线程是主线程。

这种方法的另一种方法是为每个线程创建一个大小为100/ThreadNo的avg数组,以便每个线程在数组上对其进行写操作,然后将数组合并到{{1成一个数组。

如果您打算重复使用该池,则应使用System.arraycopy()代替执行,并在提交的Futures上调用submit

get()

如果您想要读取数组的中间状态,您可以直接读取它,如果单个单元格上的数据不一致是可接受的,或者使用AtomicIntegerArray,如Nicolas Filotto建议的那样。

- 编辑 -

在编辑使用闩锁的宽度而不是原始的线程编号和所有讨论后,我想添加几个单词。

正如@jameslarge所指出的,它是关于如何建立一个&#34;发生之前&#34;在操作B(即读取)之前发生操作A(即写入)的关系或如何保证。因此,需要协调两个线程之间的访问。有几个选项

  • volatile关键字 - 不会对数组起作用,因为它只标记引用而不是标记为volatile
  • 同步 - 悲观锁定(Set<Future> futures = new HashSet<>(); for(int n = 0; ...) { futures.add(pool.submit(new Runnable() {...})); } for(Future f : futures) { f.get(); //blocks until the task is completed } 修饰符或语句)
  • CAS - 乐观锁定,由很多并发实现使用

然而,每个同步点(悲观或乐观)都建立了先发生关系。您选择哪一个,取决于您的要求。

您希望实现的是主线程的读取操作与工作线程的写入操作之间的协调。您如何实施,取决于您和您的要求。 CountDownLatch向下计算作业总数是一种方式(顺便说一句,闩锁使用状态属性synchronized)。 CyclicBarrier也可能是一个值得考虑的结构,特别是如果您想要读取一致的中间状态。或者是future.get()或者...... 所有这些都归结为工作者线程必须发出信号,他们已完成写入,读者线程可以开始阅读。

但请注意使用睡眠而不是同步。睡眠不会在关系之前建立发生,并且使用睡眠进行同步是典型的并发错误模式。即在最坏的情况下,睡眠在任何工作完成之前执行。

答案 1 :(得分:1)

您需要使用的是AtomicIntegerArray而不是简单的volatile int array。实际上,它意味着在您的情况下用于以原子方式更新数组元素并由所有线程可见。