背景
我有一个大型数据映射 (HashMap),保存在内存中,由后台线程增量更新(基于传入的消息):
<KEY> => <VALUE>
...
然后最终用户将通过 REST API 查询它:
GET /lookup?key=<KEY>
更新不会立即应用,而是分批应用,一旦收到特殊控制消息,即
MESSAGE: "Add A"
A=<VALUE> //Not visible yet
MESSAGE: "Add B"
B=<VALUE> //Not visible yet
MESSAGE: "Commit"
//Updates are now visible to the end-users
A=<VALUE>
B=<VALUE
我设计的架构如下:
volatile Map passiveCopy = new HashMap();
volatile Map activeCopy = new HashMap();
Map<String,Object> pendingUpdates;
//Interactive requests (REST API)
Object lookup(String key) {
activeCopy.get(key);
}
//Background thread processing the incoming messages.
//Messages are processed strictly sequentially
//i.e. no other message will be processed, until
//current handleMessage() invocation is completed
//(that is guaranteed by the message processing framework itself)
void handleMessage(Message msg) {
//New updates go to the pending updates temporary map
if(msg.type() == ADD) {
pendingUpdates.put(msg.getKey(),msg.getValue());
}
if(msg.type() == COMMIT) {
//Apply updates to the passive copy of the map
passiveCopy.addAll(pendingUpdates);
//Swap active and passive map copies
Map old = activeCopy;
activeCopy = passiveCopy;
passiveCopy = old;
//Grace period, wait for on-the-air requests to complete
//REST API has a hard timeout of 100ms, so no client
//will wait for the response longer than that
Thread.sleep(1000);
//Re-apply updates to the now-passive (ex-active) copy of the map
passiveCopy.addAll(pendingUpdates);
//Reset the pendingUpdates map
pendingUpdates.clear();
}
}
问题
对 volatile 字段进行写->读会产生一个先行边:
A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5
并且正确选择了宽限期,我希望应用到 passiveCopy 的任何更新(通过 putAll())将变得可见 交换后最终用户请求(一次全部)。
这真的是一个案例,还是有任何极端案例会使这种方法失败?
注意
我知道创建 Map 的副本(这样每次都会将一个新的 Map 实例分配给 activeCopy),这样做是安全的,但我不想这样做(因为它真的很大) .
最佳答案
除了您对 activeMap
和 activeCopy
的使用不一致(只需删除 activeCopy
并且只在 activeMap
和passiveCopy
),你的方法是明智的。
This answer引用 JLS:
If x and y are actions of the same thread and x comes before y in program order, then hb(x,y) [x "happens before" y].
this answer 中也给出了示例.
据我所知,访问可变变量/字段基本上是序列点;在你的例子中,因为交换是在程序代码中修改 map 之后,所以应该保证 map 的修改之前访问volatile 字段实际执行。所以这里没有竞争条件。
但是,在大多数情况下,您应该使用synchronized
或显式锁来同步并发执行。围绕使用它们进行编码的唯一原因是,如果您需要高性能,即大规模并行性,线程阻塞锁是 Not Acceptable ,或者所需的并行性太高以至于线程开始饿死。
就是说,我认为您真的应该“投资”适当的互斥,最好使用 ReadWriteLock
.因为 synchronized
(由 ReadWriteLock
内部使用)意味着内存屏障,所以您不再需要 volatile
。
例如:
final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
final Lock readLock = rwLock.getReadLock();
final Lock writeLock = rwLock.getWriteLock();
Map passiveCopy = new HashMap();
Map activeMap = new HashMap();
final Map<String,Object> pendingUpdates = new HashMap();
//Interactive requests (REST API)
Object lookup(String key) {
readLock.lock();
try {
return activeMap.get(key);
} finally {
readLock.unlock();
}
}
//Background thread processing the incoming messages.
//Messages are processed strictly sequentially
//i.e. no other message will be processed, until
//current handleMessage() invocation is completed
//(that is guaranteed by the message processing framework itself)
void handleMessage(Message msg) {
//New updates go to the pending updates temporary map
if(msg.type() == ADD) {
pendingUpdates.put(msg.getKey(),msg.getValue());
}
if(msg.type() == COMMIT) {
//Apply updates to the passive copy of the map
passiveCopy.addAll(pendingUpdates);
final Map tempMap = passiveCopy;
writeLock.lock();
try {
passiveCopy = activeMap;
activeMap = tempMap;
} finally {
writeLock.unlock();
}
// Update the now-passive copy to the same state as the active map:
passiveCopy.addAll(pendingUpdates);
pendingUpdates.clear();
}
}
然而,从您的代码中,我读到“读者”应该在其“生命周期”期间看到一致版本的 map ,而上述代码并不能保证这一点,即如果一个“读者”两次访问 map ,他可能会看到两张不同的 map 。这可以通过让每个读者在第一次访问 map 之前获取读锁本身并在最后一次访问 map 后释放它来解决。这在您的情况下可能有效,也可能无效,因为如果读者长时间持有锁,或者有很多读者线程,它可能会阻止/使试图提交更新的作者线程饿死。
关于java - 使用 volatile 更新和交换 HashMap,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56680402/