There are two issues here to take care of:
- The update of the amount value must be atomic.
If you have declared:
class Account { long amount; }
Changing the field value is not atomic on 32 bit systems. It is atomic on 64 bit systems. See: Are 64 bit assignments in Java atomic on a 32 bit machine?
So, the best way would be to change the declaration to "volatile long amout;" Then the update of the value is always atomic, plus, the volatile ensures that the others Threads/CPUs see the changed value.
That means for updating the single value, you don't need the synchronized block.
- Race between inserting and modify
With your synchronized statements you just solve the first problem. But there are multiple races in your code.
See this code:
synchronized (cache.get(id)) {
return cache.get(id).getAmount();
}
You obviously assume that cache.get(id) returns the same object instance if called for the same id. That is not the case, since a cache essentially does not guarantee this.
Guava Cache blocks until the loading is complete. Other Cache may or may not block, meaning if requests come in in parallel multiple loads will be called resulting in multiple changes of the stored cache value.
Still, Guava Cache is a cache, so the item may be evicted from the cache any time, so for the next get another instance is returned.
Same problem here:
public void addAmount(Integer id, Long value) throws Exception {
Account account = cache.get(id);
/* what happens if lots of requests come in and another
threads evict the account object from the cache? */
synchronized (account) {
. . .
In general: Never synchronize on an object that life cycle is not in your control. BTW: Other cache implementations may store just the serialized object value and return another instance on each request.
Since you have a cache.put after the modify, your solution will probably work. However, the synchronize does just fulfill the purpose of flushing the memory, it may or may not really do the locking.
The update of the cache happens after the value changed in the database. This means an application may read a former value even if it is changed in the database already. This may lead to inconsistencies.
Solution 1
Have a static set of lock objects that you chose by the key value e.g. by locks[id % locks.length]. See my answer here: Guava Cache, how to block access while doing removal
Solution 2
Use database transactions, and update with the pattern:
Transaction.begin();
cache.remove(id);
accountDAO.addAmount(id, value);
Transaction.commit();
Do not update the value directly inside the cache. This will lead to update races and needs locking again.
If the transactions are solely handled in the DAO, this means for your software architecture that the caching should be implemented in the DAO and not outside.
Solution 3
Why not just store the amount value in the cache? If it is allowed that the cache results may be inconsistent with the database content while updating, the simplest solution is:
public AccountServiceImpl() {
cache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.concurrencyLevel(4)
.maximumSize(10000)
.recordStats()
.build(new CacheLoader<Integer, Account>() {
@Override
public Account load(Integer id) throws Exception {
return accountDAO.getAmountById(id);
}
});
}
Long getAmount(Integer id) {
return cache.get(id);
}
void addAmount(Integer id, Long value) {
accountDAO.addAmount(id, value);
cache.remove(id);
}