Hashtable源码分析

前言: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日,晚上。

posted @ 2019-08-20 20:51  developer_chan  阅读(808)  评论(0编辑  收藏  举报