【问题标题】:Java HashMap race conditionJava HashMap 竞争条件
【发布时间】:2011-10-20 03:25:58
【问题描述】:

我正在尝试找出这段代码中是否存在任何竞争条件。如果关键不是'Thread.currentThread',那么我会认为是的。但是既然线程本身就是关键,那怎么可能有竞争条件呢?没有其他线程可以更新 HashMap 中的相同键!

public class SessionTracker {

     static private final Map<Thread,Session>  threadSessionMap = new HashMap<Thread,Session>();

     static public Session get() {
         return threadSessionMap.get(Thread.currentThread());
     }

     static public void set(Session s) {
         threadSessionMap.put(Thread.currentThread(),s);
     }

     static public void reset() {
         threadSessionMap.remove(Thread.currentThread());
     }
}

【问题讨论】:

  • 请注意,除了答案中描述的并发问题之外,还有其他问题。您可能有可见性问题。 (也就是说,一个线程看到的映射的内部状态可能与另一个线程看到的映射的内部状态不同)。如果你在一个线程上调用set,然后另一个线程调用size(),结果可能会得到0。
  • 不管怎样,你要找的功能正是ThreadLocal提供的功能:download.oracle.com/javase/7/docs/api/index.html?java/lang/…

标签: java multithreading synchronization race-condition


【解决方案1】:

答案是肯定的,存在潜在的竞争条件:

  • 当两个线程同时调整大小 HashMap 时
  • 碰撞发生时。当两个元素映射到同一个单元格时,即使它们具有不同的哈希码,也会发生冲突。在冲突解决期间,可能存在竞争条件,并且添加的一个键/值对可能被另一个线程插入的另一对覆盖。

为了更好地解释第二点的意思,我查看了source code of HashMap in OpenJdk 7

389        int hash = hash(key.hashCode());
390        int i = indexFor(hash, table.length);

首先它计算你的键的哈希值(结合两个哈希函数),然后它映射到一个带有indexFor 的单元格,然后它检查该单元格是否包含相同的键或者已经被另一个键占用。如果是同一个key,只是覆盖value,这里没有问题。

如果它被占用,它会查看下一个单元格,然后再查看下一个单元格,直到找到一个空位置并调用addEntry(),如果数组的加载量超过某个loadFactor,它甚至可以决定调整数组的大小。

包含条目的table 只是Entry 的向量,它包含键和值。

146    /**
147     * The table, resized as necessary. Length MUST Always be a power of two.
148     */
149    transient Entry[] table;

在并发环境中,可能会发生各种邪恶的事情,例如,一个线程与第 5 号单元格发生冲突并寻找下一个单元格 (6) 并发现它为空。

与此同时,另一个线程由于indexFor 而获得索引 6,并且两者都决定同时使用该单元格,其中一个会覆盖另一个。

【讨论】:

  • 谢谢。所以我正在考虑使用'java.util.concurrent'中的'ConcurrentHashMap'。
  • 更好,但read the Javadoc更新操作之间允许的并发由可选的 concurrencyLevel 构造函数参数(默认 16)指导,该参数用作内部大小调整的提示。该表在内部进行了分区,以尝试允许指定数量的并发更新而不会发生争用。因为哈希表中的放置本质上是随机的,所以实际的并发性会有所不同。
  • 嗯,如果我将 'HashMap' 变量 'threadSessionMap' 设为 'volatile' 变量会怎样。这会锁定整个 Hashtable 吗?
  • @JavaLearner:不,绝对不是。在处理多个线程时,您必须锁定数据结构,或者使用为并发设计的数据结构。否则,您将面临几乎不可能追踪并永远困扰您的细微错误的风险......
  • @JavaLearner:您可以使用 ConcurrentHashMap。多线程使用是安全的。上面的引用不应该让你气馁:它只是说 ConcurrentHashMap 提供了一个配置参数来改进它的并发性——也就是说,使用默认配置,你可以拥有 100、1000 和你一样多的线程想要同时访问它。问题是会有一些争用(一些线程会阻塞)。如果你保持在 concurrencyLevel 线程之下,则有可能没有线程会阻塞,从而提高性能。它在任何情况下都有效跨度>
【解决方案2】:

在不深入讨论 Hashmap 实现的具体细节的情况下,我会说仍然存在错误的可能性,因为 Hashmap 类对于并发访问是不安全的。

虽然我同意一次只能对 单个键 进行 1 次修改,因为您使用的是 currentThread(),但仍有可能多个线程同时修改 Hashmap .除非你看具体的实现,否则不要假设只有并发访问同一个 key 会导致 Hashmap 出现问题,而并发修改不同的 key 不会。

想象一个情况,两个不同的键生成相同的哈希值,很容易看出并发修改仍然可能存在错误。

【讨论】:

  • 太棒了,没想到哈希冲突!我想使用“ConcurrentHashMap”应该可以解决这个问题。
【解决方案3】:

是的,这不是一件安全的事情(正如其他答案已经指出的那样)。完全更好的解决方案可能是使用ThreadLocal,这是一种比使用 Map 更自然的方式来保存线程本地数据。它有几个不错的功能,包括默认值以及线程终止时会删除这些值。

【讨论】:

  • 这对我有什么帮助?你能详细说明一下吗?
  • 您似乎正试图将会话与特定线程相关联。 ThreadLocal 为线程提供了这种存储。因此,您可以使用它来保持会话附加到拥有它的线程。
【解决方案4】:

根据 Pierre Hugues 所写的article,如果您在多个线程之间共享 hashmap,您的进程可能会由于无限循环而挂起并吃掉所有的 cpu 资源。

【讨论】:

    【解决方案5】:

    我同意之前的回答,即您的代码不是线程安全的,虽然使用 ConcurrentHashMap 可以解决您的问题,但这是 ThreadLocal 的完美用例。

    ThreadLocal 的简短介绍:

    ThreadLocal 将在内部为每个访问 ThreadLocal 的线程保存一个类的不同实例,从而解决任何并发问题。此外(根据情况,这可能是好/坏),存储在 ThreadLocal 中的值只能由首先填充该值的线程访问。如果是当前线程第一次访问ThreadLocal,则值为null。

    保存字符串值的 ThreadLocal 的简单示例:

    private static ThreadLocal<String> threadVar = new ThreadLocal<>();
    
    public void foo() {
        String myString = threadVar.get();
    
        if (myString == null) {
            threadVar.set("some new value");
            myString = threadVar.get();
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-21
      • 1970-01-01
      • 2022-01-23
      • 2018-10-08
      • 1970-01-01
      相关资源
      最近更新 更多