Уместно ли использовать AtomicReference.compareAndSet для установки ссылки на результаты вызова базы данных?

Я реализую простой кеш с кешем, хранящимся как AtomicReference.

private AtomicReference<Map<String, String>> cacheData;

Объект кэша должен заполняться (лениво) из таблицы базы данных.

Я предоставляю метод для возврата данных кэша вызывающей стороне, но если данные пустые (т. е. не загружены), то код должен загрузить данные из базы данных. Чтобы избежать синхронизации, я подумал об использовании метода compareAndSet():

public Object getCacheData() {
  cacheData.compareAndSet(null, getDataFromDatabase()); // atomic reload only if data not set!
  return Collections.unmodifiableMap(cacheData.get());
}

Можно ли использовать compareAndSet таким образом, т.е. включить вызов базы данных как часть атомарного действия? Это лучше/хуже, чем просто синхронизация метода?

Большое спасибо за любой совет..


person MandyW    schedule 20.10.2012    source источник


Ответы (1)


Вы не достигаете ожидаемого поведения. Это выражение:

cacheData.compareAndSet(null, getDataFromDatabase())

будет всегда сначала вызывать getDataFromDatabase(). Это означает, что не имеет значения, кэшировались данные или нет. Если это так, вы все равно вызываете базу данных, но отбрасываете результаты. Кэш работает, но производительность такая же низкая.

Вместо этого рассмотрите это:

if(cacheData.get() == null) {
    cacheData.compareAndSet(null, unmodifiableMap(getDataFromDatabase()));
}
return cacheData.get());

Это не идеально (все еще getDataFromDatabase() может вызываться несколько раз в начале), но позже будет работать, как и ожидалось. Также я переместил Collections.unmodifiableMap() раньше, чтобы вам не приходилось переносить одну и ту же карту снова и снова.

Что приводит нас к еще более простой реализации (не нужны synchronized или AtomicReference):

private volatile Map<String, String> cacheData;

if(cacheData == null) {
  cacheData = unmodifiableMap(getDataFromDatabase());
}
return cacheData;
person Tomasz Nurkiewicz    schedule 20.10.2012
comment
Спасибо за очень быстрый ответ Томаш, я очень ценю это! ОК, отлично, теперь я понимаю, что это не очень хорошая модель! Просто из интереса, как вы узнали, что getDataFromDatabase() будет вызываться перед проверкой нуля? Я не мог видеть это в Javadoc для метода. - person MandyW; 21.10.2012
comment
@MandyW: так работает Java. Параметры передаются по значению (всегда!) и должны быть оценены до входа в метод. В Scala у вас есть передача по имени, которая позволяет избежать этой проблемы. - person Tomasz Nurkiewicz; 21.10.2012
comment
спасибо за альтернативу .. это предпочтительнее, чем синхронизация getData ()? Я не вижу большого преимущества в производительности. - person MandyW; 21.10.2012
comment
@MandyW: говоря, что вы не видите преимущества в производительности, вы его измерили? volatile обычно должен быть намного быстрее, чем synchronized, особенно при большой параллельной нагрузке. - person Tomasz Nurkiewicz; 21.10.2012
comment
так что на самом деле я больше думал о том, чтобы избежать блокировки всех моих вызывающих потоков, если, скажем, вызов базы данных имеет какую-то проблему, например. ждет около минуты. Я предположил, что синхронизация всего метода будет означать, что ни один поток не сможет добиться какого-либо прогресса.. используя ваш фрагмент кода для Atomic compareAndSet, я в лучшем положении? Блокируются ли другие потоки в ожидании завершения записи Atomic или все они по отдельности продолжают вызывать getDataFromDatabase(), а затем, в конце концов (при условии, что db возвращается в нормальное состояние) одному потоку удастся успешно установить cacheData? - person MandyW; 21.10.2012