前言: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.扩容转移元素时采用的是头插法。