java - 使用 volatile 更新和交换 HashMap

标签 java volatile

背景

我有一个大型数据映射 (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),这样做是安全的,但我不想这样做(因为它真的很大) .

最佳答案

除了您对 activeMapactiveCopy 的使用不一致(只需删除 activeCopy 并且只在 activeMappassiveCopy),你的方法是明智的。

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/

相关文章:

java - while '(' 后的空语句预期错误

c - 在 C 中重新排序对多个 volatile 变量的访问

c++ - 在 C++ 中为 volatile 数组赋值

Java JLayeredPane 内容在刷新时闪烁

Java图像发送网络

java - Android 使用 3G 三角测量的位置坐标精度较低

c# - volatile 是否会阻止引入的读取或写入?

c - volatile 指针指向非 volatile 数据

c++ - 标准在哪里定义 volatile 变量可以更改?

java - 在Java中优化Collat​​z猜想