如何解决与 ConcurrentHashMap 的 .compute() 同步是否保证可见性?
在 ConcurrentHashMap.compute()
内,我递增和递减位于共享内存中的一些长值。读取、递增/递减仅在 compute
方法中对同一键执行。
所以对 long 值的访问是通过锁定 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:添加另一个更接近我正在处理的代码的示例。当没有其他人使用该对象时,我想删除地图条目。请注意,long 值的读取和写入仅发生在 ConcurrentHashMap.compute
的重映射函数内部:
public class ObjectProvider {
private final ConcurrentMap<Long,CountingObject> map = new ConcurrentHashMap<>();
public CountingObject takeObjectForId(Long id) {
return map.compute(id,v) -> {
CountingObject returnLock;
returnLock = v == null ? new CountingObject() : v;
returnLock.incrementUsages();
return returnLock;
});
}
public void releaSEObjectForId(Long id,CountingObject o) {
map.compute(id,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,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;
}
}
}
解决方法
不,这种方法行不通,甚至对于 volatile 都行不通。您必须使用 AtomicLong
、LongAdder
或类似方法来确保线程安全。如今,ConcurrentHashMap
甚至无法使用分段锁。
此外,您的测试也不能证明任何事情。根据定义,并发问题不会每次都发生。甚至不是每百万次。
您必须使用合适的并发 Long
累加器,例如 AtomicLong
或 LongAdder
。
不要被 compute
文档中的那句话所迷惑:
整个方法调用以原子方式执行
这确实对副作用有效,就像您在 value++
中所做的那样;它仅适用于 ConcurrentHashMap
的内部数据。
您错过的第一件事是 locking
中的 CHM
,实现发生了很大变化(正如另一个答案所指出的那样)。但即使没有,你的理解是:
我知道当你在锁上显式同步时,可见性是有保证的
有缺陷。 JLS
表示当reader
和writer
使用相同的锁时,这是有保证的;在您的情况下,这显然不会发生;因此,没有任何保证。通常,happens-before
保证(您在此处需要)仅适用于读取器和写入器的配对。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。