Параллелизм Java: безопасная публикация массива - PullRequest
0 голосов
/ 24 января 2019

Мой вопрос чрезвычайно прост: как только я записал некоторые значения массива одним или несколькими потоками (фаза 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 гарантированно увидит также изменения энергозависимого содержимого, если он читает переменная переменная ".

1 Ответ

0 голосов
/ 24 января 2019

Ваш первый пример совершенно безопасен, потому что задачи происходят из потока писателя.Как говорят docs :

Действия в потоке перед отправкой Runnable в Executor , произошедшие до , начинается его выполнение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...