У меня в классе есть два следующих метода:
private MyDef myDef;
private FutureTask<MyDef> defFutureTask;
public synchronized void periodEviction() {
myDef = null;
}
public MyDef loadMyItems() {
// if it's not ready use a future - it will block until the results are ready
if (this.myDef == null) { // this will still not be thread safe
Callable<MyDef> callableDef = ()->{ return this.loadFromDatabase(); };
FutureTask<MyDef> defTask = new FutureTask<>(callableDef);
this.defFutureTask = defTask;
defFutureTask.run();
}
try {
// wait until's it's ready
this.myDef = this.qDefFuture.get();
} catch(InterruptedException e) {
log.error(this.getClass(), "Interrupted whilst getting future..");
} catch(ExecutionException e) {
log.error(this.getClass(), "Error when executing callable future");
}
return this.myDef;
}
Я хотел сделать следующее:
1) Выполняйте удаление кеша с помощью periodEviction() примерно каждые час.
2) В противном случае используйте кешированное значение после завершения загрузки базы данных.
Я считаю, что неправильно понял будущее Java, поскольку не смог ответить на вопрос: «Что происходит, когда потоки A, B и C одновременно вызывают loadMyItems()?»
Значит ли это, что без чего-то вроде исполнителя эта реализация все еще не является потокобезопасной?




Супер простой подход - объявить loadMyItems как synchronized. Но если у класса есть другие методы, которые обращаются к myDef, вам также придется объявить эти synchronized. Иногда это приводит к очень крупной блокировке и снижению производительности.
Если вы ищете самый чистый / самый быстрый код, вместо объявления periodEviction как synchronized объявите myDef как AtomicReference:
private final AtomicReference<MyDef> myDef = new AtomicReference<>();
Тогда тело periodEviction:
synchronized (myDef) {
myDef.set(null);
}
А корпус loadMyItems:
synchronized (myDef) {
if (myDef.get() == null) {
// perform initialization steps, ending with:
myDef.set(this.qDefFuture.get());
}
return myDef.get();
}
Если несколько потоков вызывают loadMyItems в одно и то же время, myDef будет инициализирован только один раз, и все они получат один и тот же возвращенный объект (если только вызов periodEviction каким-то образом не попал в середину).
Еще более простой подход - вообще не кэшировать объект, а просто сохранить Future.
private CompletableFuture<MyDef> defFuture;
public synchronized void periodEviction() {
// evict by triggering the request anew
defFuture = CompletableFuture.supplyAsync(this::loadFromDatabase);
}
public synchronized Optional<MyDef> loadMyItems() {
try {
return Optional.of(this.defFuture.get());
} catch(InterruptedException e) {
log.error(this.getClass(), "Interrupted whilst getting future..");
} catch(ExecutionException e) {
log.error(this.getClass(), "Error when executing callable future");
}
return Optional.empty();
}
С оговоркой, что это будет запускать запрос к базе данных каждый период выселения, а не по требованию.
Что происходит, так это то, что три потока запускают асинхронный запрос, каждый возвращает свой результат, а последний, который установил значение, будет в кеше в течение следующего часа. Чтобы сделать его потокобезопасным, превратите
loadMyItemsвsynchronized.