为什么HashMap、HashSet是线程不安全的(JDK 1.8)

1. HashMap

put()方法为例,结合JDK源码分析

/**
 * Constructs an empty HashMap with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	if ((p = tab[i = (n - 1) & hash]) == null)
	// 两个不同的对象,可能得出相同的hash值
		tab[i] = newNode(hash, key, value, null);
	else {
		Node<K,V> e; K k;
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		else {
			for (int binCount = 0; ; ++binCount) {
				if ((e = p.next) == null) {
					p.next = newNode(hash, key, value, null);
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				}
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount;
	if (++size > threshold)
	// ++操作是线程不安全的
		resize();
	afterNodeInsertion(evict);
	return null;
}
  1. 当我们使用无参构造函数创建HashMap时,实际上是创建了一个初始容量为16个键值对的map。
  2. 当我们调用put()方法时,会为Key对象生成hashcode,并调用内部方法putVal()
  3. 第17行,if ((p = tab[i = (n - 1) & hash]) == null)
    代码这里判断是否出现hash碰撞。
    假设两个线程A、B并发进行put操作,并且key的hash函数返回值与数组长度-1按位与计算出的插入下标相同时:
    当线程A执行完这一行判断后,由于时间片耗尽导致被挂起;
    而线程B得到时间片后也执行了这一行代码,并在该下标处插入了元素;
    之后线程A再次获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入(本应该形成链表的下一节点),这就导致了线程B插入的数据被线程A覆盖。
  4. 第50行,if (++size > threshold)
    众所周知,++size这个操作是线程不安全的。
    假设两个线程A、B并发进行put操作,当前HashMap的size大小为10,当线程A执行到第50行代码时,从主内存中获得size的值为10后,但是由于时间片耗尽只好让出CPU,还未来得及进行+1操作;
    而线程B此时获得CPU资源,依然读出size的值为10,并完成+1操作将size=11写回主内存;
    之后线程A再次获得时间片并继续执行size+1的操作,将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,数据覆盖又导致了线程不安全。

2. HashSet

HashSet的本质是实现了一个HashMap

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

与HashMap类似的,HashSet也是线程不安全的。

你可能感兴趣的:(Java)