前言:Hashtable线程安全的集合类,虽然它线程安全,然而在日常开发中使用的频率很低,毕竟锁的颗粒度太大了。但是这并不妨碍我们对其内部原理进行了解。
注:本文jdk源码版本为jdk1.8.0_172。
1.Hashtable基本概念
Hashtable与HashMap一样,都是以键值对的形式存储数据。但是Hashtable的键值不能为null,而HashMap的键值是可以为null的。Hashtable线程安全,因为它的元素操作方法上都加了synchronized关键字,这就导致锁的粒度太大,因此日常开发中一般建议使用ConcurrentHashMap。注意Hashtable的映射关系不是有序的,毕竟是以hashCode散列存储。
首先来看Hashtable的继承关系:
1 public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
Hashtable继承了Dictionary,实现了Map、Cloneable、Serializable接口。
HashTable构造函数
Hashtable共有4个构造函数。
#1.默认构造函数
1 public Hashtable() { 2 this(11, 0.75f); 3 }
分析:
从默认构造函数可知:Hashtable的默认容量为11,扩容因子为0.75。
#2.确定容量的构造函数
1 public Hashtable(int initialCapacity) { 2 this(initialCapacity, 0.75f); 3 }
其他构造函数可翻看相关源码,还是比较简单的。
2.put操作
1 public synchronized V put(K key, V value) { 2 // Make sure the value is not null 3 // 如果value值为空,直接抛出空指针异常 4 if (value == null) { 5 throw new NullPointerException(); 6 } 7 8 // Makes sure the key is not already in the Hashtable. 9 Entry<?,?> tab[] = table; 10 // 如果key为null,这里同样会抛出空指针 11 int hash = key.hashCode(); 12 // 通过hash值与table长度取余,确定元素的位置 13 int index = (hash & 0x7FFFFFFF) % tab.length; 14 @SuppressWarnings("unchecked") 15 // 取出当前位置上的元素 16 Entry<K,V> entry = (Entry<K,V>)tab[index]; 17 // 如果当前位置上存在值,则进行循环,因为位置上的值是以链表形式存储的 18 for(; entry != null ; entry = entry.next) { 19 // 查找是否具有相同hash值和key的元素,有则替换 20 if ((entry.hash == hash) && entry.key.equals(key)) { 21 V old = entry.value; 22 entry.value = value; 23 return old; 24 } 25 } 26 // 添加元素 27 addEntry(hash, key, value, index); 28 return null; 29 }
分析:put操作的逻辑比较简单明了,通过元素的hash值确定元素在数组上的位置,然后判断是否需要对原值进行替换,如果不进行替换则直接进行插入操作。
注意:整个put操作的流程和HashMap类似,但从以上源码可以看出Hashtable是不允许[key,value]为null。
addEntry函数(插入元素),这里会涉及扩容,因此还是很有必要看下
1 private void addEntry(int hash, K key, V value, int index) { 2 // modCount++表示进行了修改 3 modCount++; 4 5 Entry<?,?> tab[] = table; 6 // 如果元素总量大于threshold 7 // threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); 8 // 在容量不超过最大值的时候,阈值等于容量与扩容因子的乘积 9 if (count >= threshold) { 10 // Rehash the table if the threshold is exceeded 11 // 扩容 12 rehash(); 13 14 tab = table; 15 hash = key.hashCode(); 16 // 重新计算元素的位置 17 index = (hash & 0x7FFFFFFF) % tab.length; 18 } 19 20 // Creates the new entry. 21 @SuppressWarnings("unchecked") 22 // 取出当前位置上的元素 23 Entry<K,V> e = (Entry<K,V>) tab[index]; 24 // 进行插入操作,从这里可以看出新的元素总是在链表头的位置 25 tab[index] = new Entry<>(hash, key, value, e); 26 count++; 27 }
分析:
在该函数中涉及扩容,但是由于put操作为线程安全,所以扩容时也是线程安全的。扩容要求:当前元素个数大于等于容量与扩容因子的乘积。还有一点需注意插入的新节点总是在链表头。
接下来看下addEntry函数中的重点rehash(扩容函数)
1 protected void rehash() { 2 // 旧的容量大小 3 int oldCapacity = table.length; 4 Entry<?,?>[] oldMap = table; 5 6 // overflow-conscious code 7 // 扩容后新的容量等于原来容量的2倍+1 8 int newCapacity = (oldCapacity << 1) + 1; 9 // 容量大小控制,不要超过最大值 10 if (newCapacity - MAX_ARRAY_SIZE > 0) { 11 if (oldCapacity == MAX_ARRAY_SIZE) 12 // Keep running with MAX_ARRAY_SIZE buckets 13 return; 14 newCapacity = MAX_ARRAY_SIZE; 15 } 16 // 创建新的数组 17 Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; 18 19 modCount++; 20 // 更新扩容因子 21 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); 22 table = newMap; 23 // 数组转移 这里是从数组尾往前搬移 24 for (int i = oldCapacity ; i-- > 0 ;) { 25 for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { 26 Entry<K,V> e = old; 27 old = old.next; 28 // 计算元素在新数组中的位置 29 int index = (e.hash & 0x7FFFFFFF) % newCapacity; 30 // 进行元素插入,注意这里是头插法,元素会倒序 31 e.next = (Entry<K,V>)newMap[index]; 32 newMap[index] = e; 33 } 34 } 35 }
分析:
整个扩容过程比较简单,注意新的数组大小是原来的2倍加1,还有一点比较重要扩容时插入新元素采用的是头插法,元素会进行倒序。
3.get操作
分析完put操作后,我们再来看下get操作,get操作相对来说就简单许多了
1 public synchronized V get(Object key) { 2 Entry<?,?> tab[] = table; 3 int hash = key.hashCode(); 4 // 计算元素的位置 5 int index = (hash & 0x7FFFFFFF) % tab.length; 6 for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { 7 // 遍历 寻找hash和key相同的元素 8 if ((e.hash == hash) && e.key.equals(key)) { 9 return (V)e.value; 10 } 11 } 12 // 如果未发现元素,则返回null 13 return null; 14 }
分析:
get操作比较简单,通过hash值与key进行查找,找到立即返回,未找到则返回null。
总结
本文对Hashtable的主要源码进行了分析,总体来看Hashtable还是比较简单,这里总结一下侧重点:
#1.Hashtable线程安全、元素无序(因为以hashCode为基准进行散列存储),不允许[key,value]为null。
#2.Hashtable默认容量为11,与HashMap不同(默认容量16),扩容时容量增长为2*n+1(HashMap直接增长为2倍)。
#3.扩容转移元素时采用的是头插法。
by Shawn Chen,2019.08.20日,晚上。