Использование volatile для обеспечения видимости общих (но не одновременных) данных в Java
Я пытаюсь реализовать быструю версию LZ77, и у меня есть вопрос к параллельному программированию.
Сейчас у меня есть final byte[] buffer
и final int[] resultHolder
оба одинаковой длины. Программа делает следующее:
Основной поток записывает весь буфер, затем уведомляет потоки и ожидает их завершения.
Один рабочий поток обрабатывает часть буфера, сохраняя результаты в той же части держателя результатов. Рабочая часть является эксклюзивной. После этого основной поток уведомляется, а рабочий останавливается.
Когда все рабочие сделали паузу, основной поток считывает данные в resultHolder и обновляет буфер, а затем (при необходимости) процесс начинается снова с пункта 1.
Важные вещи в менеджере (основной поток) объявлены следующим образом:
final byte[] buffer = new byte[SIZE];
final MemoryHelper memoryHelper = new MemoryHelper();
final ArrayBlockingQueue<Object> waitBuffer = new ArrayBlockingQueue<Object>(TOT_WORKERS);
final ArrayBlockingQueue<Object> waitResult = new ArrayBlockingQueue<Object>(TOT_WORKERS);
final int[] resultHolder = new int[SIZE];
MemoryHelper просто оборачивает изменяемое поле и предоставляет два метода: один для чтения и один для записи в него.
Код run() работника:
public void run() {
try {
// Wait main thread
while(manager.waitBuffer.take() != SHUTDOWN){
// Load new buffer values
manager.memoryHelper.readVolatile();
// Do something
for (int i = a; i <= b; i++){
manager.resultHolder[i] = manager.buffer[i] + 10;
}
// Flush new values of resultHolder
manager.memoryHelper.writeVolatile();
// Signal job done
manager.waitResult.add(Object.class);
}
} catch (InterruptedException e) { }
}
Наконец, важная часть главной темы:
for(int i=0; i < 100_000; i++){
// Start workers
for (int j = 0; j < TOT_WORKERS; j++)
waitBuffer.add(Object.class);
// Wait workers
for (int j = 0; j < TOT_WORKERS; j++)
waitResult.take();
// Load results
memoryHelper.readVolatile();
// Do something
processResult();
setBuffer();
// Store buffer
memoryHelper.writeVolatile();
}
Синхронизация на ArrayBlockingQueue работает хорошо. Мое сомнение в использовании readVolatile()
а также writeVolatile()
, Мне сказали, что запись в изменчивое поле сбрасывает в память все ранее измененные данные, а затем читает их из другого потока, делая их видимыми.
Так достаточно ли в этом случае обеспечения правильной видимости? Реального одновременного доступа к одним и тем же областям памяти никогда не бывает, поэтому энергозависимое поле должно быть намного дешевле, чем ReadWriteLock.
1 ответ
Тебе даже не нужно volatile
здесь, потому что BlockingQueue
s уже обеспечивают необходимые гарантии видимости памяти:
Эффекты согласованности памяти: Как и в других параллельных коллекциях, действия в потоке перед помещением объекта в
BlockingQueue
действия, предшествующие выполнению, после доступа или удаления этого элемента изBlockingQueue
в другой теме.
В общем, если у вас уже есть какая-то синхронизация, вам, вероятно, не нужно делать ничего особенного, чтобы обеспечить видимость памяти, потому что это уже гарантировано используемыми вами примитивами синхронизации.
Тем не мение, volatile
операции чтения и записи могут использоваться для обеспечения видимости памяти, когда у вас нет явной синхронизации (например, в алгоритмах без блокировки).
PS
Также похоже, что вы можете использовать CyclicBarrier
вместо вашего решения с очередями оно специально разработано для подобных сценариев.