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