hashMap,hashtable和ConcurrentMap的区别
1.HashTable
(1)继承于Dictionary,实现了Map,Cloneable,Java.io.Serializable接口
(2)底层数组+链表实现,无论key还是value都不能为null,同步线程安全,实现线程安全的方式是锁住整个hashtable,效率低,concurrentMap做了相关优化。
(3)初始容量为11 扩容:newsize=oldsize*2+1
(4)两个参数影响性能:初始容量,加载因子(默认0.75)
(5)计算index方法:index=(hash&0x7FFFFFFF)%tab.length
hashtable的底层源码:
(1)HashTable继承于Dictionary,实现了Map,Cloneable,Java.io.Serializable接口
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {}
(2)HashTable两个参数影响性能:初始容量,加载因子(默认0.75)
public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); }
(3)HashTable初始容量为11 扩容:newsize=oldsize*2+1
// 调整Hashtable的长度,将长度变成原来的(2倍+1) // (01) 将“旧的Entry数组”赋值给一个临时变量。 // (02) 创建一个“新的Entry数组”,并赋值给“旧的Entry数组” // (03) 将“Hashtable”中的全部元素依次添加到“新的Entry数组”中 protected void rehash() { int oldCapacity = table.length; Hashtable.Entry<?,?>[] oldMap = table; // overflow-conscious code int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Hashtable.Entry<?,?>[] newMap = new Hashtable.Entry<?,?>[newCapacity]; modCount++; threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; for (int i = oldCapacity ; i-- > 0 ;) { for (Hashtable.Entry<K,V> old = (Hashtable.Entry<K,V>)oldMap[i]; old != null ; ) { Hashtable.Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Hashtable.Entry<K,V>)newMap[index]; newMap[index] = e; } } }
(4) 计算index方法:index=(hash&0x7FFFFFFF)%tab.length
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 = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null; }
2.HashMap
(1)底层数组+链表+红黑树实现,可以存在null键和null值,线程不安全
(2)初始size为16 扩容:newsize=oldsize*2,size一定为2的n次幂
(3)扩容针对整个map,每次和扩容时,原数组的元素重新计算存放位置,并重新插入。
(4)插入元素后才判断是否需要扩容,若再无插入,无效扩容
(5)加载因子:默认0.75
(6)计算index方法:index=hash&(tab.length-1)
(7)空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
注意:
(1)JDK1.8以后,hashmap的底层结构,由原来单纯的数组+链表,改为链表长度为8时,开始由链表转变为红黑树
(2)至于为什么要将链表在长度为8时转变为红黑树呢?
原因是链表的时间复杂度是O(n),红黑树的时间复杂度O(logn),很显然,红黑树的复杂度是优于链表的
因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好
一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树。
也就是大部分情况下,hashmap还是使用的链表,如果是理想的均匀分布,节点数不到8,hashmap就自动扩容了
3.ConcurrentMap
(1)底层采用分段的数组+链表实现,线程安全。
(2)通过把整个map分为N个Segment,可以提供相同的线程安全效率提升N倍,默认16倍。
(3)读操作不加锁,修改操作加分段锁,允许多个修改操作并行发生。
(4)扩容:段内扩容(段内元素超过该段对应的Entry数组的0.75,触发扩容,而不是整段扩容),插入前检测是否需要扩容,避免无效扩容。
(有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁)。
(5)存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。
(6)ConcurrentHashMap是使用了锁分段技术来保证线程安全的。
锁分段技术是什么呢?
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap和Hashtable的不同点:
(1)ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。
(2)Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
(3)ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有
16个写线程执行,并发性能的提升是显而易见的。