HashTable源码分析
前言:Hashtable线程安全的集合类,虽然它线程安全,然而在日常开发中使用的频率很低,毕竟锁的颗粒度太大了。但是这并不妨碍我们对其内部原理进行了解。 注:jdk版本为1.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,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日,晚上。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |