【问题标题】:How data is stored/accessed and preventing race conditions in maps, java如何存储/访问数据并防止地图中的竞争条件,java
【发布时间】:2020-12-04 09:09:51
【问题描述】:

我们有这样的案例。

class A{
 class foo{
    //Map with a lot of entries
    private HashMap<String,String> dataMap; 

    public updater(){
        // updates dataMap
        // takes several milliseconds
    }

    public someAction(){
        // needs to perform read on dataMap
        // several times, in a long process
        // which takes several milliseconds
    }
}

问题是,someAction 和 updater 都可以同时调用,someAction 是一个更频繁的方法。如果调用 updater,它可以从 dataMap 中替换很多值。我们需要 readAction 的一致性。如果方法以旧 dataMap 开始,则所有读取都应使用旧 dataMap。

class foo{
    //Map with a lot of entries
    private HashMap<String,String> dataMap; 


    public updater(){
        var updateDataMap = clone(dataMap); // some way to clone data from map
        // updates updateDataMap instead of dataMap
        // takes several milliseconds
        this.dataMap = updateDataMap;       
    }

    public someAction(){
        var readDataMap = dataMap;
        // reads from readDataMap instead of dataMap
        // several times, in a long process
        // which takes several milliseconds
    }
}

这能确保一致性吗?我相信克隆方法会在内存中分配不同的区域,并且会从那里发生新的引用。是否会对性能产生任何影响?而oldDataMap的内存用完后会释放吗?

如果这是正确的方法,有没有其他有效的方法可以达到同样的效果?

【问题讨论】:

  • 您需要将dataMap 声明为volatile。在每次更新时克隆整个地图是否比使用并发地图更好或更差,取决于实际用例。
  • 将(最终)释放不再可访问的内存。
  • 性能取决于几个因素,包括地图深层副本的大小、可用内存量以及update()readAction() 地图所需的时间。您应该指定应用程序中的映射一致性是否意味着 updater() 已从头到尾运行查看部分 updater() 结果是可以的,只要每个 set(key, value) 操作是原子的。在第二种情况下,使用synchronizedMap(dataMap) 包装地图避免了复制的开销,并使volatile 不再需要。
  • 此代码不安全。即使映射内部没有可变状态,您也有多个线程访问dataMap 变量而没有协调。这里可能出错的地方比人们最初想象的要多得多。 (即使有使dataMap 易变的“修复”,并且地图一旦发布就永远不会修改,您仍然面临丢失更新的风险。)
  • @BrianGoetz 你说的是多线程调用updater() 还是别的什么的场景?

标签: java parallel-processing java-11 openjdk-11


【解决方案1】:

我相信您的方法会奏效,因为来自updater() 的所有更改都将发生在(深层)副本中,并且someAction() 将无法看到,直到在单个操作中更新引用。

我了解您并不关心someAction()是否看到最新版本的地图内容,只要地图是一致的,即在中间不被观察到更新了。在这种情况下,您的someAction() 无法查看不完整的地图。

请注意,最多 1 个线程应该能够调用 updater() - 两个线程同时调用它意味着只有一个线程可以编写更新的地图。我建议进行以下更改:

// no synchronization needed at this level, but volatile is important
private volatile HashMap<String,String> dataMap = new HashMap<>;

// if two threads attempt to call this at once, one blocks until the other finishes
public synchronized updater() {
    var writeDataMap = clone(dataMap);  // a deep copy

    // update writeDataMap - guaranteed no other thread updating
    // ... long operation

    dataMap = writeDataMap;             // switch visible map with the updated one 
}

public someAction() {
    var readDataMap = dataMap;

    // process readDataMap - guaranteed not to change while being read
    // ... long operation
}

这里的重要关键字是volatile,以确保其他线程在updater() 完成其工作后立即访问更新的地图。 同步的使用只是防止多个updater()线程相互干扰,主要是防御性的。

【讨论】:

  • 我不明白让它变得易变有什么影响。 JVM 是否会在一个长时间操作中多次从原始指针重新获取数据,当我们添加 volatile 时会停止?
  • @VaibhawVaibhaw 没有volatile,无法保证其他线程会注意到更新的字段,但更糟糕的是,其他线程可以看到更新的字段(地图参考),同时看到超出-地图本身字段的日期值。
  • 我以指针的方式思考。 A 指向内存中的一个位置,比如 MEM-1,B = A 发生,意味着 B 现在指向 MEM-1,然后 A 与一个新映射相关联,比如在 MEM-2 内存位置上,然后 B 将继续使用 MEM -1,对吧?为什么 B 会突然开始指向 MEM-2?抱歉,但我无法理解可能发生这种情况的场景@Holger
  • @VaibhawVaibhaw 我从未说过 B 可以“突然”使用新地图。 someAction() 包含对 dataMap 的读取。如果没有volatile,此读取可能会或可能不会读取另一个线程写入它的新值。无法保证它将读取哪个映射引用,也无法保证写入线程此时是否已完成构造函数。这就是 racy read 的意义所在。不要试图用操作的顺序来推理,对于一个不合情理的阅读,不存在顺序。
  • @VaibhawVaibhaw 它会以这种方式工作变量dataMap被声明为volatile...
【解决方案2】:

如果您想避免复制,可以使用java.util.concurrent.locks.ReentrantReadWriteLock 来保护对您的地图的访问。使用updater 中的写锁和someAction 中的读锁。

【讨论】:

    猜你喜欢
    • 2016-03-21
    • 1970-01-01
    • 1970-01-01
    • 2019-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-10
    • 2011-09-22
    相关资源
    最近更新 更多