【发布时间】:2021-03-10 21:03:05
【问题描述】:
在ConcurrentHashMap.compute() 内,我递增和递减位于共享内存中的一些长值。读取、递增/递减仅在 compute 方法中对同一键执行。
因此对 long 值的访问是通过锁定 ConcurrentHashMap 段来同步的,因此递增/递减是原子。我的问题是:地图上的这种同步是否保证长期价值的可见性?我可以依赖 Map 的内部同步还是应该做多值volatile?
我知道,当您在锁上显式同步时,可以保证可见性。但我对ConcurrentHashMap 的内部结构并不完全了解。或者也许我今天可以信任它,但明天ConcurrentHashMap 的内部结构可能会发生某种变化:独占访问权将被保留,但可见性将消失……这是使我的长期价值不稳定的论据。
下面我将发布一个简化的示例。根据测试,今天没有比赛条件。但是,如果没有 volatile for 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:添加另一个更接近我正在处理的代码的示例。当没有其他人使用该对象时,我想删除地图条目。请注意,long 值的读取和写入仅发生在 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;
}
}
}
【问题讨论】:
标签: java java.util.concurrent concurrenthashmap memory-visibility