Проверка наличия readLock в `ReentrantReadWriteLock`
У меня есть несколько тесно связанных вопросов, которые возникли при попытке использовать ReentrantReadWriteLock
контролировать доступ к довольно сложной структуре данных, которая имеет ряд различных операций чтения и записи. Согласно примерам в документации, я извлек блокировку чтения и записи и использую их для (насколько я могу судить) успешного управления одновременным доступом к этой структуре данных. Однако при отладке другой проблемы я заметил, что иногда я получаю более одной блокировки чтения в одном и том же потоке. Основная причина этого заключается в том, что у меня есть ряд сложных запросов, которые вызывают более простые (см. Пример ниже). Сложный запрос можно рассматривать как транзакцию, т. Е. Между getVersion()
и доступ к data
в myQuery
, Это текущее решение работает, но это означает, что в некоторых местах кода у меня будет несколько блокировок чтения, которыми обладает один и тот же поток. Я заметил, что эквивалент записи имеет isHeldByCurrentThread()
метод, но это странно отсутствует в readLock
, Я не мог узнать следующее:
- это плохо (плохой стиль, производительность или риск будущих ошибок) иметь несколько блокировок чтения в одном потоке?
- это плохо использовать
WriteLock.isHeldByCurrentThread
проверить на наличие ошибок (например, ожидая блокировки записи, но ее нет, или в качестве условия для снятия блокировки записи)? - Есть ли лучшая практика, чтобы решить, как действовать, если это проблема? Я передаю
lockedAquired
логическое для методов, которые могут быть вызваны из кода, который уже имеет блокировку, я должен гарантировать, что они получены уникальным образом, или я должен использоватьReadLock.tryLock()
?
Вот пример кода:
public class GraphWorldModel {
//unfair RW Lock (default, see javadoc)
protected final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(false);
protected final ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
protected final ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();
protected long versionID = 0;
protected Object data = null;
@Override
public long getVersion() {
long result = -1;
readLock.lock();
try {
result = this.versionID;
} finally {
readLock.unlock();
}
return result;
}
public ResultObject myQuery() {
//do some work
readLock.lock();
try {
long version = getVersion();
ResultObject result = new ResultObject(this.data, version);
//note: querying version and result should be atomic!
} finally {
readLock.unlock();
}
//do some more work
return result;
}
}
1 ответ
- это плохо (плохой стиль, производительность или риск будущих ошибок) иметь несколько блокировок чтения в одном потоке?
Я бы сказал, что это сомнительный стиль. Во-первых, как указывает @JBNizet, у вас нет проблем с попыткой снова получить блокировку, которой вы уже владеете. Блокировка просто увеличивает количество считывателей на единицу, а затем уменьшает его при окончательной разблокировке.
Тем не менее, это означает, что вы должны пересечь барьер памяти (volatile
читать), чтобы обновить статистику общей блокировки, что означает снижение производительности. Это зависит от того, как часто этот код выполняется, от того, будет ли он иметь какие-либо заметные различия в производительности.
С точки зрения ошибок, однако, есть большая ошибка. Если вы говорите только о блокировках только для чтения, то я не думаю, что есть больше шансов ошибок. На самом деле попытка исправить проблему двойной блокировки (см. Ниже), вероятно, имеет больший риск. Но в вашем коде логика try / finally обеспечивает правильное использование блокировок и разблокировку по завершении.
Однако, если вы говорите о смешивании блокировок чтения и записи, вы должны понимать, что следующий код блокируется:
ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
ReadLock readLock = rrwl.readLock();
WriteLock writeLock = rrwl.writeLock();
readLock.lock();
writeLock.lock();
Или, по крайней мере, он останавливается, пока какой-то другой код не разблокирует read-lock
, Если вы собираетесь смешивать блокировки чтения и записи в своем коде, вы должны защитить от двойной блокировки.
- Является ли плохим использование WriteLock.isHeldByCurrentThread для проверки на наличие ошибок (например, ожидание блокировки записи, но ее нет, или в качестве условия для снятия блокировки записи)?
Это добавит много логики в ваш код, что более рискованно, но, возможно, того стоит, если производительность сильно возрастает. Тем не менее, см. Ниже для альтернативы.
- Есть ли лучшая практика, чтобы решить, как действовать, если это проблема? Передать ли логическое значение lockedAquired методам, которые могут быть вызваны из кода, уже имеющего блокировку, я должен гарантировать, что они получены уникальным образом, или мне следует использовать ReadLock.tryLock()?
Что я хотел бы сделать, это создать метод ядра под названием getVersionLocked()
, Что-то вроде:
@Override
public long getVersion() {
readLock.lock();
try {
// NOTE: I refactored this to return out of the try/finally which is a fine pattern
return getVersionLocked();
} finally {
readLock.unlock();
}
}
public ResultObject myQuery() {
//do some work
readLock.lock();
ResultObject result;
try {
long version = getVersionLocked();
result = new ResultObject(this.data, version);
//note: querying version and result should be atomic!
} finally {
readLock.unlock();
}
//do some more work
return result;
}
// NOTE: to call this method, we must already be locked
private long getVersionLocked() {
return this.versionID;
}
Затем вы можете принять решение вызвать заблокированную или разблокированную версию вашего метода и выполнить только readLock()
один раз.
Это более рискованно в том, что вы можете позвонить getVersionLocked()
когда замок не удерживается, но я думаю, что это приемлемый риск.