如何证明Java中的HashMap不是线程安全的

2022-09-02 00:13:58

我正在开发一个应用程序,该应用程序已使用“共享”状态。我需要通过单元测试证明它在多线程环境中会遇到问题。HashMap

我试图通过检查应用程序的大小和元素来检查单线程环境和多线程环境中应用程序的状态。但这似乎无济于事,状态总是一样的。HashMap

有没有其他方法可以证明这一点,或者证明在地图上执行操作的应用程序可以很好地处理并发请求?


答案 1

这很容易证明。

不久

哈希映射基于数组,其中每个项目表示一个存储桶。随着添加更多键,存储桶会增长,并且在某个阈值处,阵列将以更大的大小重新创建,以便其存储桶分布更均匀(性能注意事项)。在数组重新创建期间,数组变为空,这将导致调用方获得空结果,直到重新创建完成。

细节和证明

这意味着有时会在内部调用以使底层数组更大。HashMap#put()HashMap#resize()

HashMap#resize()为字段分配一个容量更大的新空数组,并用旧项填充该数组。当发生此填充时,基础数组不包含所有旧项,并且使用现有密钥进行调用可能会返回 。tableHashMap#get()null

下面的代码演示了这一点。您很可能会遇到异常,这意味着 不是线程安全的。我选择目标键作为 - 这样它将是数组中的最后一个元素,因此是重新填充期间的最后一个元素,这增加了进入的可能性(要查看原因,请参阅实现)。HashMap65 535nullHashMap#get()HashMap#put()

final Map<Integer, String> map = new HashMap<>();

final Integer targetKey = 0b1111_1111_1111_1111; // 65 535
final String targetValue = "v";
map.put(targetKey, targetValue);

new Thread(() -> {
    IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue"));
}).start();


while (true) {
    if (!targetValue.equals(map.get(targetKey))) {
        throw new RuntimeException("HashMap is not thread safe.");
    }
}

一个线程将新键添加到映射中。另一个线程不断检查 是否存在。targetKey

如果算上这些例外,我会绕过.200 000


答案 2

很难模拟Race,但看看OpenJDK源代码的HashMap方法:put()

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);

    //Operation 1       
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    } 

    //Operation 2
    modCount++;

    //Operation 3
    addEntry(hash, key, value, i);
    return null;
}

如您所见,涉及3个未同步的操作。复合操作是非线程安全的。因此,从理论上讲,它被证明是线程安全的。put()HashMap


推荐