前面已经学习了HashMap的底层实现原理,并且从源码的角度详细的分析了其中的属性字段、构造方法、常用的get和put操作,以及扩容机制。HashMap作为一个最为常用的Map接口的实现类,一个广为人知的特点就是:线程不安全。多线程之间的运行可能会导致底层形成循环链表,或者造成数据的丢失和覆盖。如果想要使用一种线程安全的Map结构,那么可以选择synchronizedMap、HashTable和ConcurrentHashMap,下面分别从源码剖析一下synchronizedMap和Hashtable的源码实现。
Hashtable与HashMap都是用来存储key-value类型数据,在正式理解Hashtable的实现前,首先看一下它和hashMap的区别之处,带着疑问看源码总是更好的。两者的主要区别如下:
Hashtable的key和value都不允许为null,而HashMap两者都不做限制
Hashtable线程安全,hashMap做不到线程安全
HashMap使用iterator进行迭代遍历,它是一种fail-fast迭代器;而Hashtable使用enumeration迭代器,它属于fail-fast迭代器
Java中的fail-fast和fast-safe机制
HashTable继承了Dictionary类,hashMap继承了AbstractMap类
下面依然从字段、构造方法和常用方法三个方面,对Hashtable的源码进行解读。
Hashtable中定义的属性字段如下所示:
// 继承父类Dictionary
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
// 不可序列化的Entry类型的table,用于存储数据
private transient Entry<?,?>[] table;
// Hashtable中数据数量
private transient int count;
// 扩容阈值
private int threshold;
// 加载因子
private float loadFactor;
// 修改次数
private transient int modCount = 0;
// 序列化ID
private static final long serialVersionUID = 1421746759512286392L;
// 设置table的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 默认存储结构
private transient volatile Set<K> keySet;
private transient volatile Set<Map.Entry<K,V>> entrySet;
private transient volatile Collection<V> values;
}
属性字段基本上和HashMap中相差不多,不过并没有直接设置初始化容量大小。
Hahstable同样提供了四个构造方法,如下所示:
// 全参构造
public Hashtable(int initialCapacity, float loadFactor) {
// 如果初始容量小于0直接抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 如果加载因子小于0,或者为null,同样直接抛异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
// 如果设置初始容量0中,则取1,保证table至少能存储一个数据
if (initialCapacity==0)
initialCapacity = 1;
// 初始化加载因子
this.loadFactor = loadFactor;
// 初始化table
table = new Entry<?,?>[initialCapacity];
// 设置扩容阈值,如果超过了设置的MAX_ARRAY_SIZE就不能再扩容了
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 自定义初始化容量,加载因子默认为0.75
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
// 无参构造,容量初始化默认为11,加载因子为0.75
public Hashtable() {
this(11, 0.75f);
}
// 使用已有的Map实现初始化Hashtable
public Hashtable(Map<? extends K, ? extends V> t) {
// 如果map的大小小于11,则设置初始容量为11,否则设置为已有map的大小为初始容量
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
首先,看一下get()
的执行过程,源码如下所示:
// 线程安全通过在方法上使用synchronized关键字实现
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 首先获取key对应的哈希值
int hash = key.hashCode();
// 计算key在table中的索引
// 取模
int index = (hash & 0x7FFFFFFF) % tab.length;
// 如果对应的桶存在链表,则遍历链表查找key对应的元素
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
// 如果链表中某个节点的哈希值相同并相等,返回对应的value
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
// 否则返回null
return null;
}
get()
整体的逻辑还是很简单的,主要就是先找key在table中位置,然后遍历可能存在的链表进行查找。
put()
的源码如下所示:
public synchronized V put(K key, V value) {
// 如果value为null,直接抛空指针异常
if (value == null) {
throw new NullPointerException();
}
// 首先根据key获取它的哈希值
Entry<?,?> tab[] = table;
int hash = key.hashCode();
// 计算key在table中的索引
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 获取table中对应位置的链表
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
// 如果key已经存在,则覆盖旧的value
V old = entry.value;
entry.value = value;
return old;
}
}
// 否则直接添加到相应的位置上
addEntry(hash, key, value, index);
return null;
}
addEntry
完成key不在table时的put操作 ,源码如下:
private void addEntry(int hash, K key, V value, int index) {
// 修改次数增1
modCount++;
Entry<?,?> tab[] = table;
// 如果此时table中的元素数量已经大于或等于阈值,则需要进行扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
// 重新计算key在新table中的索引位置
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 新建一个Entry对象,将其放在链表的头部
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
这里使用的Entry的构造函数如下:
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
因此,可以看到Hashtable在put操作中使用的是头插法。如果table给定的index位置没有插入元素,则直接将其放在index位置,那么它的next指针指向null;否则将其插入到链表的头部,它的next指针指向之前的头部元素。
rehash()
对应Hashtable的扩容机制,它的实现源码如下所示:
protected void rehash() {
// 首先获取扩容前table的大小
int oldCapacity = table.length;
// 保存旧的table
Entry<?,?>[] oldMap = table;
// 新的容量为旧容量 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
// 如果扩容后容量超过了设置的MAX_ARRAY_SIZE,并且旧容量已经是MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
return;
// 那么新的容量只能是MAX_ARRAY_SIZE
newCapacity = MAX_ARRAY_SIZE;
}
// 创建新的table
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
// 修改次数加1
modCount++;
// 重新计算扩容阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
// 更新table
table = newMap;
// 将旧table中元素按照新的index放置到新table中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
// 这里使用的仍然是头插法
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
从扩容的源码实现中可以看出,Hashtable扩容后的容量大小有两种情况:
而且,在将旧的table中元素重新放入新table时使用的依然是头插法。
HashTable是一个线程安全的哈希表,它通过使用synchronized关键字来对方法进行加锁,从而保证了线程安全。但这也导致了在单线程环境中效率低下等问题。
public synchronized int size(){}
public synchronized boolean isEmpty() {}
public synchronized Enumeration<K> keys() {}
public synchronized Enumeration<V> elements() {}
public synchronized boolean contains(Object value) {}
public synchronized boolean containsKey(Object key) {}
public synchronized V get(Object key) {}
public synchronized V put(K key, V value) {}
public synchronized V remove(Object key) {}
public synchronized void putAll(Map<? extends K, ? extends V> t) {}
public synchronized void clear() {}
public synchronized Object clone() {}
public synchronized String toString() {}
public synchronized boolean equals(Object o) {}
public synchronized int hashCode() {}
public class HashtableDemo {
public static void main(String[] args) {
Hashtable<Integer, String> map = new Hashtable<>();
map.put(2, "Ball");
map.put(23, "James");
map.put(24, "Kobe");
map.put(11, "Yao");
map.put(1, "Forlogen");
// 四种遍历方式
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
System.out.println("key = " + entry.getKey() + " value = " + entry.getValue());
}
System.out.println("--------------------");
Set<Integer> set = map.keySet();
for (Integer key : set) {
System.out.println("key = " + key + " value = " + map.get(key));
}
System.out.println("--------------------");
Enumeration e = map.elements();
while(e.hasMoreElements()){
System.out.println(e.nextElement());
}
System.out.println("--------------------");
map.forEach((k, v) -> System.out.println("key = " + k + " value = " + v));
}
}
Colletions工具类中的synchronizedMap可以用于创建线程安全的Map集合,它的字段定义和构造方法源码如下所示:
// 实现了Map接口
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
// 被final修饰的普通Map对象
private final Map<K,V> m; // Backing Map
// 互斥锁对象
final Object mutex; // Object on which to synchronize
// 不指定锁对象
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
// 不指定锁对象时,锁对象就是SynchronizedMap本身
mutex = this;
}
// 自定义锁对象
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
// 否则,锁对象为传入的mutex
this.mutex = mutex;
}
get()
和put()
的源码如下:
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
可以看到,方法使用的仍然是一般Map中的get()
和put()
,不过在真正执行前需要synchronized进行加锁,只有获得了锁对象的操作才能操作Map。其他方法的实现同样需要synchronized进行加锁,详细内容可自行走读SynchronizedMap源码。