Java HashMap竞态条件

7

我正在尝试确定这段代码是否会出现竞争条件。如果键不是'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作为结果。 - Bruno Reis
4
你所需要的功能正是 ThreadLocal 提供的,具体请参考官方文档:http://download.oracle.com/javase/7/docs/api/index.html?java/lang/ThreadLocal.html - Bruno Reis
5个回答

14
答案是肯定的,有可能存在竞态条件:
  • 两个线程同时进行HashMap的调整大小(resizing)
  • 冲突(collisions)发生时。即使它们具有不同的哈希码,两个元素映射到同一单元格时可能会发生碰撞。在冲突解决期间,可能会出现竞争条件,其中一个添加的键/值对可能会被另一个线程插入的另一对键/值对覆盖。

为了更好地解释第二点,我查看了OpenJdk 7中HashMap的源代码

389        int hash = hash(key.hashCode());
390        int i = indexFor(hash, table.length);
首先,它会计算您的键的哈希值(结合两个哈希函数),然后使用 indexFor 映射到一个单元格,接着它会检查该单元格是否包含相同的键或已经被其他键占用。如果是相同的键,则只需替换该键对应的值,这里没有问题。
如果该单元格被占用,它会查看下一个单元格,直到找到一个空位置并调用addEntry(),如果数组的负载因子超过某个特定的loadFactor,则addEntry()甚至可以决定调整数组大小。
我们的包含条目的table只是一个Entry向量,其中保存了键和值。
146    /**
147     * The table, resized as necessary. Length MUST Always be a power of two.
148     */
149    transient Entry[] table;
在并发环境下,各种不好的事情都可能发生,例如一个线程得到单元格编号5的冲突,并查找下一个单元格(6),结果发现它是空的。同时,另一个线程得到了6作为indexFor的结果,两个线程都决定同时使用该单元格,其中一个会覆盖另一个。

谢谢。所以我在考虑使用'java.util.concurrent'中的'ConcurrentHashMap'。 - JavaLearner
更好,但是请阅读Javadoc。允许的更新操作并发性由可选的concurrencyLevel构造函数参数(默认值为16)指导,该参数用作内部大小的提示。表被内部分区以尝试允许指定数量的并发更新而不产生争用。因为在哈希表中的位置基本上是随机的,所以实际并发性会有所变化。 - stivlo
嗯,如果我将“HashMap”变量“threadSessionMap”设置为“volatile”变量,那么整个Hashtable会被锁定吗? - JavaLearner
@JavaLearner:绝对没有错。当处理多个线程时,您必须锁定数据结构,或使用专为并发设计的数据结构。否则,您就会冒着无法跟踪的微妙错误的风险,并且一直困扰着你... - Steven Schlansker
1
@JavaLearner:你可以使用ConcurrentHashMap。它可以被多个线程安全地使用。上面的引用不应该让你感到沮丧:它只是说ConcurrentHashMap提供了一个配置参数来改善它的并发性——也就是说,使用默认配置,你可以有100、1000或更多线程同时访问它。问题在于会有一些争用(一些线程会被阻塞)。如果你保持在concurrencyLevel线程以下,就有可能没有任何线程会被阻塞,从而提高性能。无论哪种情况都可以工作。 - Bruno Reis
显示剩余2条评论

2

不详细介绍Hashmap实现的具体细节,我可以说一下由于Hashmap类不支持并发访问,因此仍存在错误的可能性。

虽然我同意每次只能对一个键进行修改,但由于您使用了currentThread(),因此仍有多个线程同时修改Hashmap的可能性。除非您查看特定的实现,否则不要假设仅对同一键的并发访问会导致Hashmap出现问题,而对不同键的并发修改不会。

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


太棒了,我没有想到哈希冲突!我猜使用“ConcurrentHashMap”应该可以解决这个问题。 - JavaLearner

1

是的,这不是一个安全的做法(正如其他答案已经指出的那样)。完全更好的解决方案可能是使用ThreadLocal,它是一种比使用Map更自然的方式来保留线程本地数据。它有一些不错的特性,包括默认值和当线程终止时值被删除。


那会如何帮助我?你能详细说明一下吗? - JavaLearner
看起来你正在尝试将会话与特定线程关联起来。ThreadLocal为线程提供了这种存储方式。因此,你可以使用它来将会话附加到拥有它的线程。 - Jason

0

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

关于ThreadLocal的简短介绍:

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

以下是一个简单的ThreadLocal示例,它保存String值:

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();
    }
}

0
根据Pierre Hugues所写的文章,如果在多个线程之间共享哈希映射表,由于无限循环,您的进程可能会挂起并占用所有CPU资源。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接