我有一个大的数据映射(HashMap),保存在内存中,并由后台线程进行增量更新(基于传入消息):
<KEY> => <VALUE>
...
Run Code Online (Sandbox Code Playgroud)
最终用户将通过REST API查询它:
GET /lookup?key=<KEY>
Run Code Online (Sandbox Code Playgroud)
一旦收到特殊控制消息,即不会立即应用更新,而是分批进行更新,即
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
Run Code Online (Sandbox Code Playgroud)
我设计的架构如下:
<KEY> => <VALUE>
...
Run Code Online (Sandbox Code Playgroud)
将write-> read写入volatile字段会在边缘之前发生:
在随后每次对该字段进行读取之前,都会写入一个易失字段(第8.3.1.4节)。
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5
并且正确选择了宽限期,我希望在交换之后,最终用户请求(一次全部)将看到对任何应用到PassiveCopy(通过putAll())的更新。
确实是这样,还是有什么极端情况会使这种方法失败?
注意
我知道创建Map的副本(以便每次都将一个新的Map实例分配给activeCopy)是安全的,但是我不想这样做(因为它确实很大)。
除了对activeMapand的使用不一致activeCopy(只需删除activeCopy并仅在activeMapand之间交换passiveCopy)之外,您的方法是明智的。
这个答案引用了JLS:
如果 x 和 y 是同一线程的操作,并且 x 按程序顺序出现在 y 之前,则 hb(x,y) [x“发生在”y 之前]。
这个答案中还给出了一个例子。
由此我认为对易失性变量/字段的访问基本上是序列点;在您的情况下,因为交换是在程序代码中修改映射之后进行的,所以应该保证在实际执行对 volatile 字段的访问之前完成映射的修改。所以这里没有竞争条件。
但是,在大多数情况下,您应该使用synchronized显式锁来同步并发执行。使用这些代码进行编码的唯一原因是,如果您需要高性能,即大规模并行性,则线程阻塞锁是不可接受的,或者所需的并行性太高以至于线程开始匮乏。
也就是说,我相信你真的应该“投资”适当的互斥,最好使用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();
}
}
Run Code Online (Sandbox Code Playgroud)
然而,从您的代码中,我读到“读者”应该在其“生命周期”内看到地图的一致版本,上面的代码不能保证这一点,即如果一个“读者”访问地图两次,他可能会看到两个不同的地图。这可以通过让每个读取器在第一次访问映射之前获取读锁本身,并在最后一次访问映射后释放它来解决。这在您的情况下可能有效,也可能不起作用,因为如果读取器长时间持有锁,或者有许多读取器线程,它可能会阻止/饥饿尝试提交更新的写入器线程。
| 归档时间: |
|
| 查看次数: |
104 次 |
| 最近记录: |