Внутри ConcurrentHashMap.compute()
я увеличиваю и уменьшаю какое-то длинное значение, расположенное в общей памяти. Чтение, увеличение/уменьшение выполняется только в методе compute
для одного и того же ключа. Таким образом, доступ к длинному значению синхронизируется путем блокировки сегмента ConcurrentHashMap, поэтому увеличение/уменьшение является атомарным. У меня такой вопрос: гарантирует ли эта синхронизация на карте видимость длинного значения? Могу ли я полагаться на внутреннюю синхронизацию Map или мне следует сделать длинное значение volatile
?
Я знаю, что когда вы явно синхронизируете на блокировке, видимость гарантируется. Но у меня нет идеального понимания ConcurrentHashMap
внутренностей. Или, может быть, я могу доверять этому сегодня, но завтра внутренности ConcurrentHashMap
могут как-то измениться: монопольный доступ сохранится, но исчезнет видимость... и это аргумент, чтобы сделать мое длинное значение изменчивым.
Ниже я опубликую упрощенный пример. Судя по тесту, сегодня гонки нет. Но могу ли я доверять этому коду в долгосрочной перспективе без volatile
вместо long value
?
class LongHolder {
private final ConcurrentMap<Object, Object> syncMap = new ConcurrentHashMap<>();
private long value = 0;
public void increment() {
syncMap.compute("1", (k, v) -> {
if (++value == 2000000) {
System.out.println("Expected final state. If this gets printed, this simple test did not detect visibility problem");
}
return null;
});
}
}
class IncrementRunnable implements Runnable {
private final LongHolder longHolder;
IncrementRunnable(LongHolder longHolder) {
this.longHolder = longHolder;
}
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
longHolder.increment();
}
}
}
public class ConcurrentMapExample {
public static void main(String[] args) throws InterruptedException {
LongHolder longholder = new LongHolder();
Thread t1 = new Thread(new IncrementRunnable(longholder));
Thread t2 = new Thread(new IncrementRunnable(longholder));
t1.start();
t2.start();
}
}
UPD: добавил еще один пример, который ближе к коду, над которым я работаю. Я хотел бы удалить записи карты, когда никто другой не использует объект. Обратите внимание, что чтение и запись длинного значения происходит только внутри функции переназначения ConcurrentHashMap.compute
:
public class ObjectProvider {
private final ConcurrentMap<Long, CountingObject> map = new ConcurrentHashMap<>();
public CountingObject takeObjectForId(Long id) {
return map.compute(id, (k, v) -> {
CountingObject returnLock;
returnLock = v == null ? new CountingObject() : v;
returnLock.incrementUsages();
return returnLock;
});
}
public void releaseObjectForId(Long id, CountingObject o) {
map.compute(id, (k, v) -> o.decrementUsages() == 0 ? null : o);
}
}
class CountingObject {
private int usages;
public void incrementUsages() {
--usages;
}
public int decrementUsages() {
return --usages;
}
}
UPD2: Признаюсь, что не смог привести простейшие примеры кода ранее, выкладываю сейчас реальный код:
public class LockerUtility<T> {
private final ConcurrentMap<T, CountingLock> locks = new ConcurrentHashMap<>();
public void executeLocked(T entityId, Runnable synchronizedCode) {
CountingLock lock = synchronizedTakeEntityLock(entityId);
try {
lock.lock();
try {
synchronizedCode.run();
} finally {
lock.unlock();
}
} finally {
synchronizedReturnEntityLock(entityId, lock);
}
}
private CountingLock synchronizedTakeEntityLock(T id) {
return locks.compute(id, (k, l) -> {
CountingLock returnLock;
returnLock = l == null ? new CountingLock() : l;
returnLock.takeForUsage();
return returnLock;
});
}
private void synchronizedReturnEntityLock(T lockId, CountingLock lock) {
locks.compute(lockId, (i, v) -> lock.returnBack() == 0 ? null : lock);
}
private static class CountingLock extends ReentrantLock {
private volatile long usages = 0;
public void takeForUsage() {
usages++;
}
public long returnBack() {
return --usages;
}
}
}