То, что вы написали, не является потокобезопасным. На самом деле, вы наткнулись на распространенную ошибку, которая является довольно известной проблемой. Это называется проблема двойной проверки блокировки , и многие такие решения, как ваше (и есть несколько вариантов этой темы), имеют проблемы.
Существует несколько возможных решений, но им проще всего просто использовать ScheduledThreadExecutorService и перезагружать то, что вам нужно, каждую минуту или так часто, как вам нужно. Когда вы перезагружаете его, он помещает его в кеш-результат, и вызовы для него просто возвращают последнюю версию. Это потокобезопасно и легко реализуемо. Конечно, он загружается не по требованию, но, кроме начального значения, вы никогда не будете терять производительность при получении значения. Я бы назвал это чрезмерной загрузкой, а не отложенной загрузкой.
Например:
public class Cache<T> {
private final ScheduledExecutorsService executor =
Executors.newSingleThreadExecutorService();
private final Callable<T> method;
private final Runnable refresh;
private Future<T> result;
private final long ttl;
public Cache(Callable<T> method, long ttl) {
if (method == null) {
throw new NullPointerException("method cannot be null");
}
if (ttl <= 0) {
throw new IllegalArgumentException("ttl must be positive");
}
this.method = method;
this.ttl = ttl;
// initial hits may result in a delay until we've loaded
// the result once, after which there will never be another
// delay because we will only refresh with complete results
result = executor.submit(method);
// schedule the refresh process
refresh = new Runnable() {
public void run() {
Future<T> future = executor.submit(method);
future.get();
result = future;
executor.schedule(refresh, ttl, TimeUnit.MILLISECONDS);
}
}
executor.schedule(refresh, ttl, TimeUnit.MILLISECONDS);
}
public T getResult() {
return result.get();
}
}
Это требует небольшого объяснения. По сути, вы создаете универсальный интерфейс для кэширования результата Callable, который будет загружать ваш документ. Отправка Callable (или Runnable) возвращает будущее. Вызов блоков Future.get () до тех пор, пока он не вернется (не завершится).
Итак, что это делает, так это реализует метод get () в терминах Future, чтобы начальные запросы не заканчивались ошибкой (они будут блокироваться). После этого каждые миллисекунды 'ttl' вызывается метод обновления. Он отправляет метод в планировщик и вызывает Future.get (), который возвращает результат и ожидает его завершения. После завершения он заменяет элемент «result». Подпоследовательность вызовов Cache.get () вернет новое значение.
В ScheduledExecutorService есть метод scheduleWithFixedRate (), но я избегаю его, потому что, если Callable занимает больше времени, чем запланированная задержка, у вас будет многократный запуск в одно и то же время, а затем придется беспокоиться об этом или регулировать. Процессу проще представить себя в конце обновления.