Проверка владения 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;
  }
}

person Rens van der Heijden    schedule 13.12.2016    source источник
comment
Блокировка, как видно из ее названия, является повторно используемой. Так что приобрести его снова, когда он у вас уже есть, совсем не проблема. То, что у вас есть, прекрасно.   -  person JB Nizet    schedule 13.12.2016


Ответы (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(), когда блокировка не удерживается, но я думаю, что это приемлемый риск.

person Gray    schedule 13.12.2016
comment
Нет "синхронизации памяти". Счетчики удержания потоков хранятся для каждого потока. java.util.concurrent в значительной степени, если не полностью, без синхронизации. - person user207421; 14.12.2016
comment
Я не думаю, что ты прав @EJP. В классе Sync есть volatile int state, который попадает в tryAcquireShared(...), поэтому, когда я его читаю, есть доступ volatile при каждом вызове ReadLock.lock(), хотя я согласен с тем, что счетчики удержания являются локальными для потока. - person Gray; 14.12.2016