HashMap因为多线程未同步时导致put进的元素get出来为null的分析
当你明明put进了一对非null key-value进了HashMap,某个时候你再用这个key去取的时候却发现value为null,再次取的时候却又没问题,都知道是HashMap的非线程安全特性引起的,分析具体原因如下:
Java代码
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
// indexFor方法取得key在table数组中的索引,table数组中的元素是一个链表结构,遍历链表,取得对应key的value
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
再看看put方法:
Java代码
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
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;
}
}
modCount++;
// 若之前没有put进该key,则调用该方法
addEntry(hash, key, value, i);
return null;
}
再看看addEntry里面的实现:
Java代码
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K, V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
里面有一个if块,当map中元素的个数(确切的说是元素的个数-1)大于或等于容量与加载因子的积时,里面的resize是就会被执行到的,继续resize方法:
Java代码
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int) (newCapacity * loadFactor);
}
resize里面重新new一个Entry数组,其容量就是旧容量的2倍,这时候,需要重新根据hash方法将旧数组分布到新的数组中,也就是其中的transfer方法:
Java代码
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K, V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K, V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
在这个方法里,将旧数组赋值给src,遍历src,当src的元素非null时,就将src中的该元素置null,即将旧数组中的元素置null了,也就是这一句:
Java代码
if (e != null) {
src[j] = null;
此时若有get方法访问这个key,它取得的还是旧数组,当然就取不到其对应的value了。
下面,我们重现一下场景:
Java代码
import java.util.HashMap;
import java.util.Map;
public class TestHashMap {
public static void main(String[] args) {
final Map<String, String> map = new HashMap<String, String>(4, 0.5f);
new Thread(){
public void run() {
while(true) {
System.out.println(map.get("name1"));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
for(int i=0; i<3; i++) {
map.put("name" + i, "value" + i);
}
}
}
Debug上面这段程序,在map.put处设置断点,然后跟进put方法中,当i=2的时候就会发生resize操作,在transfer将元素置null处停留片刻,此时线程打印的值就变成null了。
其它可能由未同步HashMap导致的问题:
1、多线程put后可能导致get死循环(主要问题在于put的时候transfer方法循环将旧数组中的链表移动到新数组)
2、多线程put的时候可能导致元素丢失(主要问题出在addEntry方法的new Entry<K,V>(hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失)
总结:HashMap在并发程序中会产生许多微妙的问题,难以从表层找到原因。所以使用HashMap出现了违反直觉的现象,那么可能就是并发导致的了
分享到:
相关推荐
在Java的HashMap中,多线程并发环境下的扩容操作可能会引发死锁问题。这主要发生在JDK 1.7版本,因为其扩容机制采用了头插法。以下详细解释这个问题及其解决方案。 首先,HashMap的扩容机制是在容量达到阈值时触发...
最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。 Hashtable和HashMap采用的hash/rehash算法都大概...
### HashMap多线程解决方案 #### 一、引言 在多线程环境下,Java的`HashMap`类在处理并发操作时容易出现线程安全问题。本文档深入探讨了`HashMap`在多线程环境中可能遇到的安全问题,并提出了一系列可行的解决方案...
答:在多线程环境下,可以使用ConcurrentHashMap,它是线程安全的HashMap实现。 五、HashMap与HashSet的关系 HashSet基于HashMap实现,每个元素作为HashMap的一个键,值为null。因此,HashSet的操作性能也依赖于...
这意味着在多线程环境中,对`HashTable`的操作不会导致数据不一致的情况发生。 - **HashMap**: 默认是非线程安全的。如果多个线程同时访问一个`HashMap`实例,且至少有一个线程修改了该`HashMap`,则必须通过外部...
- **HashMap**:在 Java 8 中引入了并行化能力,通过 `ConcurrentHashMap` 的实现方式,提高了多线程环境下的性能。 - **HashTable**:由于其同步策略,不适用于高并发场景。 9. **迭代器**: - **HashMap**:...
如果需要在一个多线程环境中使用`HashMap`,则可以通过`Collections.synchronizedMap(new HashMap())`的方式将其包装成线程安全的版本。这种方式相比于`Hashtable`更为灵活,因为可以在需要同步的时候才进行同步处理...
首先,HashMap是基于哈希表的Map接口非同步实现,它允许使用null值和null键,这意味着HashMap在设计时没有考虑多线程环境下的线程安全问题。在单线程环境下,HashMap提供了优秀的性能和访问速度。而如果需要线程安全...
为了在多线程环境中安全地使用`HashMap`,开发者需要自己负责同步,例如使用`Collections.synchronizedMap(new HashMap,V>())`创建线程安全的`HashMap`实例。 #### 2. 允许null值 - **HashTable**: 不支持`null`键...
相比之下,`HashMap`是非线程安全的,它没有同步任何方法,因此在多线程环境中使用时,可能需要显式地添加同步控制,例如通过`Collections.synchronizedMap()`方法来创建一个线程安全的`Map`。 #### 3. 允许null...
5. **线程不安全**:HashMap不是线程安全的,如果在多线程环境中使用,需要外部同步机制,或者使用ConcurrentHashMap。 6. **null键与null值**:HashMap允许键和值为null,但只有一个键可以为null,且该键对应的值...
在多线程环境下,若需保证线程安全,可以考虑使用 ConcurrentHashMap 替换 HashMap。而在列表操作中,根据插入位置和访问顺序,可以选择 ArrayList 或 LinkedList。了解这些基本数据结构的特点和用法,有助于我们在...
因此,在多线程环境中使用`HashMap`时,如果不采取额外的同步措施,可能会导致数据不一致或其他并发问题。 #### 2. 同步机制 - **Hashtable**: 使用内部同步机制来确保线程安全,这意味着在执行关键操作时会锁定...
HashMap 在多线程环境下使用时,需要注意线程安全问题,否则可能会导致程序崩溃或数据不一致。 * 使用 ConcurrentHashMap 替代 HashMap,可以解决线程安全问题。 * 使用线程安全的集合框架,例如 ...
HashMap是非同步的,意味着在多线程环境中,如果不进行适当的同步控制,可能会导致数据不一致。而HashTable是同步的,因此它在多线程环境下的安全性更高,但这也牺牲了性能。此外,HashMap允许键和值为null,而...
4. **并发问题**:HashMap不是线程安全的,这意味着在多线程环境中,同时对HashMap进行读写操作可能会导致数据不一致。如果需要线程安全的哈希表,可以使用`ConcurrentHashMap`。 5. **null值**:HashMap允许键和值...
5. **线程安全性**:HashMap本身不是线程安全的,如果在多线程环境中使用,需要外部同步机制来保证数据一致性。对于线程安全的需求,可以使用ConcurrentHashMap。 HashSet是基于HashMap实现的,它不存储值,只存储...
- `HashTable`是线程安全的,它的所有操作都是同步的,这意味着在多线程环境下,不同线程可以安全地共享`HashTable`实例,而不会出现数据不一致的情况。 - `HashMap`不是线程安全的,它没有进行同步控制。如果在多...
然而,这并不意味着在所有多线程场景下`ConcurrentHashMap`都优于`HashMap`,如果线程访问的键值对分布在不同段,`HashMap`的性能可能会更好,因为`ConcurrentHashMap`的分段锁在某些情况下可能导致过多的锁粒度。...
HashMap 不是同步的,所以在多线程环境下使用时同样需要同步控制。 Hashtable 类 Hashtable 是 HashMap 的前身,它也是键值对存储的散列表,但它是同步的。和 HashMap 不同的是,Hashtable 不允许 null 键和 null ...