引言
- Hashmap和HashTable的区别:
- 简单的回答:
- HashMap是非线程安全的,HashTable是线程安全的(synchronized)
- HashMap内部的Key和Value允许空值,HashTable则都不可以
- HashMap效率比HashTable效率高
- Java中的另一个线程安全的与HashMap功能极其类似的类是什么?同样是线程安全,它与HashTable在同步上有什么不同?
下面开始本篇博客:
1.HashMap概述:
众所周知,java中存储数据主要用两种,即数组和链表。先来比较一下二者的区别:
- 数组:存储空间连续,所以寻址快,查询速度快,但是进行增删的话,需要移动很多元素,效率低下。
- 链表:存储空间不连续,所以查询慢,增删的时候,只需要移动指针即可,所以增删效率高。
先看一张HashMap的结构图:
由上图可知:
- HashMap是由数组+链表实现的
- 数组的初始长度为16,每个元素存储的是链表的头结点
- 键值所存放的数据结构实际上是一个它内部实现的Entry的内部类,数组实现的,属性有key、value和指向下一个Entry的next
- 另外:HashMap的hash算法是位运算,并不是取模运算,这样效率更高。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.HashMap初始化:
hashMap有两种常用的初始化方法:
- 不需要参数的初始化方式:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//初始数组长度
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2.需要参数的初始化方式:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
3.HashMap的put操作:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
3.1:由源码可以看出:hashMap是允许key为null的,会调用putForNullKey(value)方法:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
当进入putForNullKey(value)方法的时候,会首先遍历table[0]为链表头的链表,如果存在key为null的数据,就将Value替换成新的value,并返回旧的value,如果没有key为null的数据,就会调用addEntry方法,将这条数据新加入。
put时key为null情况的总结:
- 先在table[0]的链表中寻找null key,如果有null key就直接覆盖原来的value,返回原来的value;
- 如果在table[0]中没有找到,就进行头插,但是要先判断是否要扩容,需要就扩容,然后进行头插,此时table[0]就是新插入的null key Entry了
put操作时key不为null的情况:
- 由上面的源码可知:
- 先计算存入key的hash值:
int hash = hash(key.hashCode());
- 通过hash计算存放在数组中的位置:
int i = indexFor(hash, table.length);
- 判断key和数组元素位置为i的链表中是否存在相同的键,若相同,覆盖并返回原来的value;若不相同,调用addEntry()进行添加数据
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
- 至此,hashmap的put流程就介绍完了
4.HashMap的get操作:
话不多说,先看源码:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
- 如果key为null,调用getForNullKey()方法查询key为null的数据。
- 如果key不为null,先计算key的hashCode,通过hashcode查询以及最后的equals比较,找到对应的value并返回
- 需要注意的是,当找不到对应的value时,返回的是null,或者value本身的值就是null。
5.HashMap和HashTable的对比:
- hashMap和hashTable的底层实现是相同的,都是数组+链表;
- HashMap是线程不安全的,HashTable是线程安全的,因为内部的方法都被synchronized修饰;
- HashMap效率相比Hashtable效率要高很多;
- HashMap中的key和value允许为null,HashTable都不允许,会空指针异常;
- HashMap的初始容量为16,HashTable默认为11;
- HashMap扩容时乘2,使用位运算扩容,效率高;HashTable扩容时乘2再加1,使用取模运算;
- 二者hash运算源码:
//HashMap的散列函数,这里传入参数为键值对的key
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//返回hash值的索引,h & (length-1)操作等价于 hash % length操作, 但&操作性能更优
static int indexFor(int h, int length) {
// length must be a non-zero power of 2
return h & (length-1);
}
//HashTable的散列函数直接在put方法里实现了
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
6.HashTable和ConCurrentHashMap的对比:
首先先介绍一下ConCurrentHashMap:它是线程安全的HashMap的实现,内部大量利用了volatile、final、cas等lock-free技术来减少锁竞争对于性能的影响。
HashTable内部方法使用的是synchronized关键字,这其实就是对对象加锁,虽然解决了线程安全的问题,但会造成性能的下降;
ConCurrentHashMap算是 对HashTable存在问题的优化:其构造函数如下:默认传入的是16,0.75,16
public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2) {
//…
int i = 0;
int j = 1;
while (j < paramInt2) {
++i;
j <<= 1;
}
this.segmentShift = (32 - i);
this.segmentMask = (j - 1);
this.segments = Segment.newArray(j);
//…
int k = paramInt1 / j;
if (k * j < paramInt1)
++k;
int l = 1;
while (l < k)
l <<= 1;
for (int i1 = 0; i1 < this.segments.length; ++i1)
this.segments[i1] = new Segment(l, paramFloat);
}
public V put(K paramK, V paramV) {
if (paramV == null)
throw new NullPointerException();
int i = hash(paramK.hashCode()); //这里的hash函数和HashMap中的不一样
return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);
}
ConcurrentHashMap采用了分段锁的设计,上面代码的最后一行可以理解成将一个大的Map拆成多个小的HashTable,在put操作中,会根据hash(paramK.hashCode())来决定具体存放到哪一个segment中。如果查看segment的put操作,我们就会发现内部的同步机制是基于lock操作的,这样就保证了对Map的一部分(Segment)上锁,这样影响的只是将要放入同一个Segment的元素的put操作,在保证同步的时候,锁住的不是整个Map(Hashtable就是如此)。
7.HashTable和ConCurrentHashMap的对比:
1.hashMap线程不安全,ConCurrentHashMap线程安全。
2.HashMap键值允许为null,ConCurrentHashMap不允许。