序
最近因为项目的需求,经常会面试一些新人,也就会问他们一些基本的问题,例如,HashMap和HashTable的区别是什么,一般人想到的就是HashMap不是线程安全,这点我想几乎来面试的人都知道,但是再深入问下为什么HashMap不是线程安全的,几乎没有人答上来,当然了,我也不会因为你回答不上来就认为能力不行,只能认为是这个题目是一道附加题,大家都懂得,下面我们就简单看下为什么HashMap不是线程安全的。
正文
例如我有几个线程同时给里面放入元素,key为线程的名字,value为一个对象,也可以是一个list,暂且不管,好了,现在我们一起看源代码吧。
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); //此时的hash值是根据传入的key值进行hash得到的,尽管有些人认为线程的名字命名的好的话就不会有hash相同的情况,即使如此也是有可能的,这里我们就假如hash的结果是相同的 int hash = hash(key); int i = indexFor(hash, table.length);//此时的i也是相同的。 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++; addEntry(hash, key, value, i);//跟进去看下 return null; }
进入addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //假设都是第一次进入这个方法的,那么此时table数组应该是空的。 createEntry(hash, key, value, bucketIndex); }
进入createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) { //加入有很小的概率两个线程同时进入这个方法,并且此时的hash和bucketIndex是一模一样的,那么此时就会有问题了,两个线程在同时在同一个数组的位置放入Entry链表,所以就出现了线程安全的问题 Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
同样的,在addEntry方法中的如果判断容量超过了限制,就会扩容,此时resize也是会有问题的
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, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
删除一个元素呢?其实在某种场景下也是会有问题的。
final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); //(1) Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; //(2) if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
在(1)处时,根据i获取了对应数组的链表的头部,然后在(2)处进行把链表的头部指定为当前头部的下一个元素,如果此时恰巧有另外一个线程在此处放入了一个元素,虽然几率不大,但是总有可能发生,因此还是线程不安全的。
再看下HashTable的源码:
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } }
public synchronized V remove(Object key) { Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { modCount++; if (prev != null) { prev.next = e.next; } else { tab[index] = e.next; } count--; V oldValue = e.value; e.value = null; return oldValue; } } return null; }
public synchronized V get(Object key) { Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return e.value; } } return null; }
全部都同步了,因此敢说HashTable是线程安全的。
PS: 本文是基于我们已经知道了HashMap的底层存储结构的前提来举例的,如果底层结构不了解,那么此文看起来就可能很迷糊。
相关推荐
HashMap为什么是线程不安全的?如何解决HashMap的线程不安全问题?
高级程序员必会的HashMap的线程安全问题,适用于0~2年的
3.为什么hashmap不安全 why 3.1 插入HashMap.put 3.1.1 HashMap 在扩容的时候 3.2 HashMap 在删除数据的时候 0.背景 经常会看到说HashMap是线程不安全的,ConcurrentHashMap是线程安全的等等说法,不禁有个疑问,...
NULL 博文链接:https://flyfoxs.iteye.com/blog/1198030
这就有可能导致A线程和B线程同时对一个数组扩容,A线程扩容后替换掉老数组,这时B线程使用的数组实际上是A线程扩容后的数组,就会产生线程安全问题。 死锁原因 比如,当前集合数组长度为2,已经有两个元素被放在了...
哈希映射线程测试使用 Maven 构建和运行 mvn exec:java
HashMap和HashTable的区别?但是如果想线程安全有想效率高?
Golang无锁线程安全的HashMap,为最快的读取访问进行了优化
HashMap 不是线程安全的;多线程环境下,建议使用 ConcurrentHashMap,或者使用 Collections.synchronizedMap(hashMap) 将 HashMap 转成线程同步的。 只能使用关联的键来获取值。 HashMap 只能存储对象,所以基本...
使用一个类的时候我们必须先弄清楚这个类是否是线程安全的。...一般来说,Java标准库中的类在其API文档中会说明其是否是线程安全的(没有说明其是否是线程安全的,则可能是也可能不是线程安全的)。
Java集合多线程安全 线程安全与不安全集合 线程不安全集合: ArrayList LinkedList HashMap HashSet TreeMap TreeSet StringBulider 线程安全集合: Vector HashTable Properties 集合线程安全...
HashMap的存储结构 HashMap内部采用数组和链表的方式存储数据,每个元素都包含...HashMap通过synchronized关键字实现线程安全,确保多线程环境下的数据一致性和并发访问的安全性,避免潜在的竞争条件和数据不一致问题。
阿里面试题:ConcurrentHashMap为什么是线程安全的? ConcurrentHashMap,其实是线程安全的HashMap,所以阅读ConcurrentHashMap,建议 先阅读一下两篇介绍HashMap的文章 你真的懂大厂面试题:HashMap吗? jdk1.7 ...
你为什么要用HashMap? 1.解决问题需要的数据结构是一种键值对的数据结构 2.HashMap是线程不安全的,其速度比较快 3.HashMap在存储key的值时,允许为NULL 4.对于输入数据的顺序与输出数据的顺序没有特别要求(如果有...
如果我么需要有一个线程安全的HashMap,可以使用Collections.synchronizedMap(Map m)方法获得线程安全的HashMap,也可以使用ConcurrentHashMap类创建线程安全的map。 存储的元素在jdk1.7当中是Entry作为存储的
HashMap是通过hash算法,基于数组、链表和红黑树实现的,hash算法是一种思想,只要符合该思想的算法都是hash算法,其核心就是给定一个key,通过hash可以对应一个h(key),举个例子就是当我们要存储一个key为字符串的...
因此,在使用HashMap时需要进行同步处理或者使用线程安全的HashMap实现类。 动态扩容:当HashMap中的元素数量超过了容量(默认为16)与负载因子(默认为0.75)的乘积时,HashMap会自动扩容,即创建一个新的数组,并将...
HashSet 是如何保证不重复的 向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合 equles ...HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程 环境下不安全)?
哈希图总览Golang无锁无线程安全HashMap,针对最快的读取访问进行了优化。用法为地图中的键设置值: m := &HashMap{}m.Set("amount", 123)从地图中读取键的值: amount, ok := m.Get("amount")使用地图来计数URL请求...
HashMap的长度(容量)为什么要设计成2的幂? HashTable的特点 TreeMap ArrayList的特点 Vector的特点 LinkedList的特点 Set ConcurrentModificationException异常 线程安全的集合 线程安全的 List ...