Явная параллельная видимость записи примитивных массивов
Недавно я нашел этот камень в моей базе кода:
/** This class is used to "publish" changes to a non-volatile variable.
*
* Access to non-volatile and volatile variables cannot be reordered,
* so if you make changes to a non-volatile variable before calling publish,
* they are guaranteed to be visible to a thread which calls syncChanges
*
*/
private static class Publisher {
//This variable may not look like it doing anything, but it really is.
//See the documentaion for this class.
private volatile AtomicInteger sync = new AtomicInteger(0);
void publish() {
sync.incrementAndGet();
}
/**
*
* @return the return value of this function has no meaning.
* You should not make *any* assumptions about it.
*/
int syncChanges() {
return sync.get();
}
}
Это используется как таковое:
Тема 1
float[][] matrix;
matrix[x][y] = n;
publisher.publish();
Тема 2
publisher.syncChanges();
myVar = matrix[x][y];
Тема 1 - поток обновления фона, который работает непрерывно. Тема 2 - рабочий поток HTTP, который не заботится о том, чтобы то, что он читает, каким-либо образом последовательным или атомарным, только то, что записи "в конечном итоге" попадают туда и не теряются в качестве предложений богам concurrency.
Теперь это вызывает все мои предупреждающие звонки. Пользовательский алгоритм concurrency, написанный глубоко внутри несвязанного кода.
К сожалению, исправление кода не является тривиальным. Поддержка Java для параллельных примитивных матриц не является хорошей. Похоже, что самый лучший способ исправить это - использовать ReadWriteLock
, но это, вероятно, будет иметь отрицательные последствия для производительности. Правильность важна, очевидно, но, похоже, я должен доказать, что это неправильно, прежде чем просто вырвать ее из чувствительной к производительности области.
Согласно документации java.util.concurrent, создайте следующие отношения happens-before
:
Каждое действие в потоке происходит - перед каждым действием в этом потоке, которое приходит позже в порядке программы.
Записывается в нестабильное поле - перед каждым последующим чтением этого же поля. Записи и чтения изменчивых полей имеют аналогичные эффекты согласованности памяти как входные и выходящие мониторы, но не влекут за собой блокировку взаимного исключения.
Итак, это звучит так:
- выполняется матрица write-before publish() (правило 1)
- publish() происходит до syncChanges() (правило 2)
- syncChanges() происходит до чтения матрицы (правило 1)
Таким образом, код действительно установил цепочку событий для этой матрицы.
Но я не уверен. concurrency сложно, и я не эксперт в области.
Что я пропустил? Действительно ли это безопасно?
Ответы
Ответ 1
С точки зрения видимости, все, что вам нужно, это volatile write-read, в любом изменчивом поле. Это будет работать
final float[][] matrix = ...;
volatile float[][] matrixV = matrix;
Тема 1
matrix[x][y] = n;
matrixV = matrix; // volatile write
Тема 2
float[][] m = matrixV; // volatile read
myVar = m[x][y];
or simply
myVar = matrixV[x][y];
Но это полезно только для обновления одной переменной. Если потоки записи записывают несколько переменных, и прочитанный поток читает их, читатель может видеть несогласованную картину. Обычно он обрабатывается блокировкой чтения и записи. Copy-on-write может быть подходящим для некоторых шаблонов использования.
У Doug Lea есть новый "StampedLock" http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/StampedLock.html для Java8, который представляет собой версию блокировки чтения и записи, которая намного дешевле для чтения замки. Но это гораздо труднее использовать. В основном читатель получает текущую версию, затем переходите и читайте кучу переменных, а затем снова проверяйте версию; если версия не изменилась, во время сеанса чтения не было одновременных записей.
Ответ 2
Это действительно безопасно для публикации одиночных обновлений в матрице, но, конечно, это не дает никакой атомарности. Независимо от того, зависит ли это от вашего приложения, но его следует, вероятно, задокументировать в этом классе.
Однако он содержит некоторую избыточность и может быть улучшен путем создания поля sync
final
. Доступ к этому полю volatile
является первым из двух барьеров памяти; по контракту вызов incrementAndGet()
оказывает такое же влияние на память, как на запись и чтение на изменчивой переменной, а вызов get()
имеет тот же эффект, что и чтение.
Таким образом, код может опираться только на синхронизацию, предоставляемую этими методами, и сделать само поле final
.
Ответ 3
Использование volatile
- не волшебная пуля, чтобы синхронизировать все. Гарантируется, что, если другой поток прочитает обновленное значение изменчивой переменной, они также будут видеть каждое изменение, внесенное в нелетучую переменную до этого. Но ничего не гарантирует, что другой поток прочитает обновленное значение.
В примере кода, если вы делаете несколько записей в matrix
, а затем вызываете publish()
, а другой поток вызывает synch()
, а затем читает матрицу, тогда другой поток может видеть некоторые, все или ничего изменений:
- Все изменения, если они читают обновленное значение из publish()
- Ни одно из изменений, если оно читает старое опубликованное значение, и ни одно из изменений не просочилось через
- Некоторые изменения, если они читают ранее опубликованное значение, но некоторые из изменений просочились через
См. в этой статье
Ответ 4
Вы правильно упомянули правило № 2 о происшествии перед отношениями
Записывается в нестабильное поле - перед каждым последующим чтением этого же поля.
Однако это не гарантирует, что publish() будет вызываться до syncChanges() на абсолютной временной шкале. Немного изменим ваш пример.
Тема 1:
matrix[0][0] = 42.0f;
Thread.sleep(1000*1000); // assume the thread was preempted here
publisher.publish(); //assume initial state of sync is 0
Тема 2:
int a = publisher.syncChanges();
float b = matrix[0][0];
Какие существуют опции для переменных a и b?
- a равно 0, b может быть 0 или 42
- a - 1, b - 42 из-за отношения "бытие-до"
- a больше 1 (по какой-то причине поток 2 был медленным, и Thread 1 повезло публиковать обновления несколько раз), значение b зависит от бизнес-логики и способа обработки матрицы - зависит ли она от предыдущего состояния или нет?
Как с этим бороться? Это зависит от бизнес-логики.
- Если Thread 2 проверяет состояние матрицы время от времени, и отлично, чтобы иметь некоторые устаревшие значения между ними, если в конце будет обработано правильное значение, а затем оставьте его как есть.
- Если Thread 2 не заботится о пропущенных обновлениях, но он всегда хочет следить за обновленной матрицей, тогда используйте коллекции copy-on-write или используйте ReaderWriteLock, как было упомянуто выше.
- Если Thread 2 действительно заботится об отдельных обновлениях, тогда его следует обрабатывать более разумно, вы можете рассмотреть шаблон wait()/notify() и уведомлять Thread 2 всякий раз, когда обновляется матрица.