本文主要介绍HashTable以及其与HashMap的异同点等方面,HashTable源码分析主要从继承结构,基本属性,构造方法,核心方法四个方面介绍,以下为正文:
-
HashTable简介
HashTable的底层同样基于数组+链表实现的,存储的元素也是key-value键值对,但HashTable是JDK1.0引入的类,是线程安全的,可以用于多线程环境中,具体内容详见源码分析。
-
继承结构
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable { }
由源码可以看出:HashTable继承于Dictionary,这是一个比较早的实现类(since JDK1.1),含有一些特殊方法,keys() /elements() 枚举类型遍历 ;实现了Map接口,Cloneable接口,Serializable接口(支持实现序列化);
-
基本属性
//数组table,内有静态内部类Entry
private transient Entry<K,V>[] table;
//hashTable中的数据个数
private transient int count;
//阈值
private int threshold;
//加载因子(默认0.75f)
private float loadFactor;
//版本控制(修改次数),用于实现fast-fail机制
private transient int modCount = 0;
//版本序列号
private static final long serialVersionUID = 1421746759512286392L;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
-
构造方法(4个)
一、//指定容量和加载因子的构造函数
public Hashtable(int initialCapacity, float loadFactor) {
//参数合法性校验
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry[initialCapacity];//创建数组
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//阈值初始化
initHashSeedAsNeeded(initialCapacity);
}
二、//指定容量的构造函数
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);//加载因子默认0.75
}
三、//默认构造函数
public Hashtable() {
this(11, 0.75f);//初始容量默认11,加载因子默认0.75
}
四、//包含“子Map”的构造函数
public Hashtable(Map<? extends K, ? extends V> t) {//集合
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
-
核心方法
1、添加元素put()方法
public synchronized V put(K key, V value) {//方法修饰符为synchronized,表明线程安全
// Make sure the value is not null 确保value不为null,否则抛出空指针异常
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.//确保key不在hashTable中
Entry tab[] = table;
int hash = hash(key);//hash过程(hashSeed ^ k.hashCode();key若为null,那么null.hashcode会抛出异常,表明key不能为null)
int index = (hash & 0x7FFFFFFF) % tab.length;//确定其在table[]中的具体位置,先对hash取正,再对数组长度取余;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
//key重复覆盖返回,只用equals判断key是否相等,原因,hashTable中key不为null;HashMap中用"=="||"equals"判断原因是,其key可为null;
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {//元素超过阈值,调用rehash()方法
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.创建新节点,插入index位置(头插),将e设为下一个元素
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
return null;
}
//hash()
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();
}
//rehash()
protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;//扩容 2*原数组长度+1
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<K,V>[] newMap = new Entry[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
boolean rehash = initHashSeedAsNeeded(newCapacity);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
if (rehash) {
e.hash = hash(e.key);
}
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newMap[index];
newMap[index] = e;
}
}
在put过程,可以看出其数据结构与HashMap相同,是一个Entry数组;有一点明显区别的是:HashTable再hash过程后计算索引时, int index = (hash & 0x7FFFFFFF) % tab.length,其中hash & 0x7FFFFFFF是为了避免负值出现,对数组长度取余是为了限定在数组范围内,而HashMap中计算索引: h & (length-1)是对(hash & 0x7FFFFFFF) % tab.length的优化,因为位运算比取余运算的效率高。
还有一点,HashMap与HashTable的最大值是不同的,HashMap的最大值是1<<30;为int范围内2的次幂最大值,而HashTable的最大值,是指虚拟机实现对array的长度有限制。
2、删除remove()方法
public synchronized V remove(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
3、获取元素get()
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
//首先通过 hash()方法求得 key 的哈希值,然后根据 hash 值得到 index 索引(上述两步所用的算法与 put 方法都相同)。然后迭代链表,返回匹配的 key 的对应的 value;找不到则返回 null。
-
HashMap与HashTable的异同点
区别:
继承结构:
HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。
key和value的null值问题
HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。
线程安全
Hashtable 方法是同步,而HashMap则不是。Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。
hash函数不同
HahTable直接使用key的hashcode值,而hashMap对key的hash值重新计算,详见源码;
扩容方式不同,初始容量不同
扩容:
hashMap resize(2 * table.length) 2倍扩容
hashTale int newCapacity = (oldCapacity << 1) + 1 2倍+1扩容
初始数组容量:
HashMap 16;求数组容量为2的次幂;
HashTable 11;不要求数组容量为2的次幂;
最大值限制不同
HashMap 最大容量为 1<<30(即2^30)
HashTable 最大容量指虚拟机实现对array的长度有限制,也就是不超过内存即可;
特有方法
HashTable:特有方法contains();
速度效率问题:
单线程hashMap快于hashTable
原因:synchronized涉及用户态和内核态切换,用时较长;多线程情况下,相同条件下,hashMap效率高,但还是使用hashTable(线程安全)
相同:
1. 它们都是存储键值对(key - value)的散列表,而且都是采用链地址法 实现的。
2. 添加键值对:通过 key 计算出哈希值,再计算出数组的索引,根据索引去遍历单链表,如果链表中已经存在这个key 值,则覆盖原来的value,否则,加入新的节点。
3. 删除键值对: 通过 key 计算出哈希值,再计算出数组的索引,根据索引去遍历单链表,从单链表中删除这个键值对。
-
遍历方式
1、 迭代器遍历
Iterator遍历三种(键值对,键,值遍历)
//第一种hashtable遍历方式
System.out.println("第一种遍历方式");
for(Iterator<String> iterator=hashtable.keySet().iterator();iterator.hasNext();){
String key=iterator.next();
System.out.println("key-----"+key);
System.out.println("value--------"+hashtable.get(key));
}
//第二种hashtable遍历方式
System.out.println("第二种遍历方式");
for(Iterator<Entry<String, String>> iterator=hashtable.entrySet().iterator();iterator.hasNext();){
Entry<String,String> entry=iterator.next();
System.out.println("key---------"+entry.getKey());
System.out.println("value------------"+entry.getValue());
}
//第三种hashtable遍历方式
System.out.println("第三种遍历方式");
for(Map.Entry<String, String> entry: hashtable.entrySet()){
System.out.println("key---------"+entry.getKey());
System.out.println("value--------"+entry.getValue());
}
2、枚举遍历
//枚举遍历(2种)
System.out.println("第一种遍历方式");
Enumeration<String> e = hashtable.keys();
while(e.hasMoreElements()){
String key = e.nextElement();
System.out.println("key"+" "+key);
System.out.println("value"+" "+hashtable.get(key));
}
//获取所有值
System.out.println("第二种遍历方式");
Enumeration<String> e1 = hashtable.elements();
while(e1.hasMoreElements()){
String s = (String) e1.nextElement();
System.out.println(s);
}
}
-
线程安全问题
HashTable是线程安全类,而HashMap是非线程安全类,那么HashMap非线程安全的原因是什么呢?如何解决呢?
HashMap会在并发执行put操作时引起死循环,CPU利用率接近100%,原因是多线程会导致链表形成环链,导致死循环,实际上环链问题是在出现在transfer()方法扩容操作时出现的;在自动调整大小机制期间,如果线程尝试放入或获取对象,则映射可能使用旧索引值,并且不会找到条目所在的新存储桶。最糟糕的情况是2个线程同时放入数据,2个put()调用同时调整Map的大小。由于两个线程同时修改了链接列表,因此Map最终可能会在其链接列表中出现内部循环。如果尝试使用内部循环获取列表中的数据,则get()将永远不会结束。详细问题可以参考以下博客:
https://coolshell.cn/articles/9606.html
http://firezhfox.iteye.com/blog/2241043
如何在多线程下使用HashMap?(3种方式)
1、HashTable
由于HashTable是线程安全类,put与get等方法都用synchronized关键字修饰,保证线程安全;当一个线程访问HashTable的同步方法时,其他线程也要访问同步方法,会被阻塞;比如,一个线程要访问put方法,其他线程不但不可以访问put方法,连get方法也不可以访问,所以效率很低,很少使用;
2、ConcurrentHashMap(CHM)
CHM引入了分割,并提供了HashTable支持的所有的功能。在CHM中,支持多线程对Map做读操作,并且不需要任何的blocking。这得益于CHM将Map分割成了不同的部分,在执行更新操作时只锁住一部分。根据默认的并发级别(concurrency level
),Map被分割成16个部分,并且由不同的锁控制。这意味着,同时最多可以有16个写线程操作Map。试想一下,由只能一个线程进入变成同时可由16个写线程同时进入(读线程几乎不受限制),性能的提升是显而易见的。但由于一些更新操作,如put(),remove(),putAll(),clear()只锁住操作的部分,所以在检索操作不能保证返回的是最新的结果。
另一个重要点是在迭代遍历CHM时,keySet返回的iterator是弱一致和fail-safe的,可能不会返回某些最近的改变,并且在遍历过程中,如果已经遍历的数组上的内容变化了,不会抛出ConcurrentModificationExceptoin的异常。
CHM默认的并发级别是16,但可以在创建CHM时通过构造函数改变。毫无疑问,并发级别代表着并发执行更新操作的数目,所以如果只有很少的线程会更新Map,那么建议设置一个低的并发级别。另外,CHM还使用了ReentrantLock来对segments加锁。
3、SynchronizedMap
源码(此处基于JDK1.8)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<>(m); }
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private static final long serialVersionUID = 1978198479659022715L; 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); mutex = this; } SynchronizedMap(Map<K,V> m, Object mutex) { this.m = m; this.mutex = mutex; } public int size() { synchronized (mutex) {return m.size();} } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty();} } public boolean containsKey(Object key) { synchronized (mutex) {return m.containsKey(key);} } public boolean containsValue(Object value) { synchronized (mutex) {return m.containsValue(value);} } 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);} } public V remove(Object key) { synchronized (mutex) {return m.remove(key);} } public void putAll(Map<? extends K, ? extends V> map) { synchronized (mutex) {m.putAll(map);} } public void clear() { synchronized (mutex) {m.clear();} } //其余方法省略
可以看出:调用该方法会返回一个SynchronizedMap类的对象,而SynchronizedMap类中使用了Synchronized同步关键字保证对Map的操作是线程安全的。
使用该方法:
Map<String,String> hashmap = Collections.synchronizedMap(new HashMap<>());
关于三种方法的效率问题,大家可以进行代码测试,结果:CHM的效率远高于其他两种方法。