【问题标题】:ConcurrentHashMap iteration guarranteeConcurrentHashMap 迭代器保证
【发布时间】:2016-01-16 10:08:56
【问题描述】:

鉴于 ConcurrentHashMap javadocs 状态:

"迭代器和枚举返回反映状态的元素 在迭代器创建时或之后的某个时间点的哈希表”

我想我可以保证在下面的示例中,一个或两个线程将调用 fireAllFinished()。有没有调用fireAllFinished()的情况?

ConcurrentHashMap<String, Boolean> taskToFinished = new ConcurrentHashMap();
    taskToFinished.put("taskA", false);
    taskToFinished.put("taskB", false);

public void checkForAllFinished() {
    boolean allFinished = true;
    for (Boolean taskFinished = tasksToFinished.values()) {
        if (!taskFinished) {
            allFinished = false;
            break;
        }
    }
    if (allFinished) {
       fireAllFinished()
    }
}

//Thread1
public void run() {
    taskToFinished.put("taskA", true);
    checkForAllFinished();
}

//Thread1
public void run() {
    taskToFinished.put("taskB", true);
    checkForAllFinished();
}

(我省略了一些线程创建代码。希望意图清楚)

更新:我已经看到了这个更笼统的问题:Is iterating ConcurrentHashMap values thread safe?,但想确认我的具体观点为

“在某个时候”

在处理乱序运行代码的多核机器时通常是一个不精确的概念,两个线程可能“同时”更新映射的不同段,并且设计上没有办法锁定整个 ConcurrentHashMap。

【问题讨论】:

  • 此程序无法运行。方法checkForAllFinished 有一个局部变量allFinished,它是本地的。局部变量始终是线程安全的,因为它们位于堆栈上并且永远不会被其他线程看到。因此,您的 checkForAllFinished 方法将无法按预期工作。如果您将 allFinished 设为实例字段,则需要将其同步或设为 volatile 以防止数据过时。
  • @scottb 你说的没有错。
  • 如果您正在等待完成一组任务,为什么不使用Future
  • @Jason - 我创建了这个简化的示例来专注于一个特定的点。在实际系统中,每个任务都有一个状态工作流,其整体批处理状态源自任务,并且每个任务都可以重新运行。未来可能会有所帮助,但我仍然希望能回答最初的问题。

标签: java multithreading synchronization concurrenthashmap


【解决方案1】:

阅读ConcurrentHashMap的文档...

检索反映了最近完成的更新操作在开始时保持的结果。 (更正式地说,给定键的更新操作与报告更新值的键的任何(非空)检索具有发生前的关系。)

对于 putAll 和 clear 等聚合操作,并发检索可能仅反映插入或删除某些条目。类似地,迭代器、拆分器和枚举返回的元素反映了在迭代器/枚举创建时或之后的某个时刻哈希表的状态。

措辞不明确,但最近完成at or since应该意味着map操作和迭代器创建是顺序一致的。

使用您的示例,如果我们调用 map put A,并进行值检查 B 你有......

T1A -> B

T2A -> B

A发生在B之前,但T1T2同时发生。顺序一致的意思是,只要 A 发生在 B 之前,一些 两者之间的有效序列就需要发生。但是,T1T2 之间的任何排序都是有效的。

例如

T1:A -> T1:B -> T2:A -> T2:B

T1:A -> T2:A -> T2:B -> T1:B

因此,当代码实际运行时,任何有效的排序都可能发生,但 T1:BT2:B(检查)必须最后发生。因此,fireAllFinished 被调用一次或两次。线性同步将进一步限制为所有事件之间的显式排序。

虽然迭代整个地图可能有点贵,但使用AtomicInteger 或其他同步机制(如ConcurrentLinkedQueue)可能更简单。

【讨论】:

  • 我同意杰森的观点。迭代器将只保证所有已完成操作的地图快照,不保证您将在同一迭代器中看到以后的更改。因此,为了回答您的示例,您的程序可能不会在两个线程上调用 fireAllFinished 方法,因为其中一个线程可能在创建迭代器后更新了映射
  • 我只关心 fireAllFinished 被调用的次数为零。如果它被调用一次或两次,那很好。
  • 我认为线性同步意味着永远不会出现两个线程都调用 fireAllFinished() 的情况。
  • @barclar 我通读了源码,迭代器只是顺序一致,但这仍然确保方法至少执行一次。我正在更新答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-22
  • 1970-01-01
  • 2013-10-12
  • 1970-01-01
  • 2014-01-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多