聊一下Map。主要有以下几个类:
(1)HashMap
(2)Hashtable
(3)ConcurrentHashMap
(4)LinkedHashMap
(5)WeakHashMap
环境是java8,上述hashMap和ConcurrentHashMap在java7的时候实现会有不同。
Map主要是用来存储<K,V>键值对,那么现在来一个一个看一下底层是如何进行实现的。
一、HashMap
这是平常最常用的一个类,也是非线程安全的。都知道是数组+链表实现的,那具体是如何实现呢?hash冲突如何进行解决呢?
(1)默认容量、加载因子
//默认容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//容量*加载因子,默认是12,扩容时会相应的变化
int threshold;
何为加载因子?
用于在元素个数超过(容量X加载因子)时,就会进行扩容。
(2)底层数据结构
是为一个Node类型的数据
transient Node<K,V>[] table;
而Node是一个实现了Map.Entry的类,这个是用来构成链表的
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
(3)put方法解析
put方法会调用putVal方法,所以主要来看这个东西。根据hash值计算索引(也即是数组中的下标),如果对应索引上为空,则new一个node填充,如果不为空,则看是否有hash冲突的情况,hash和key都一样,不做任何操作;只是hash一样,即是hash冲突,此时往链表尾部插入一个节点,同时如果链表长度大于8,会将链表转化成红黑树。(这是Java1.8优化的,是为了优化查询,如果链太长,会在查询的时候耗费大量时间,所以转成红黑树。这个也是java7和java8的区别)
【注意:在发生hash冲突的时候,不会对原有值进行操作。】
/**
*hash:key的hash值
*onlyIfAbsent:如果是true,当key已存在时不改变原有值
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
//如果table为null或者容量为0,触发resize
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据hash值计算索引,如果对应为null,新创建一个Node
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
HashMap.Node<K,V> e; K k;
//如果根据hash得到的索引有值,并且hash一样,key也一样
//则将原有值进行返回,并不做任何操作
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断对应的链表是否是红黑树
else if (p instanceof HashMap.TreeNode)
//调用红黑树的putTreeVal方法
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//此块是解决Hash冲突的关键
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//往链表中插入一个节点
p.next = newNode(hash, key, value, null);
//如果链表长度大于8,则将链表转化成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//记录当前遍历节点
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果当前容量已经超过了(容量*加载因子)
//进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
(4)resize方法,扩容
两倍扩容,同时扩容阙值也会变成之前的两倍,扩容之后重新计算所有元素的hash值并重新计算索引
Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//阙值也会相应的扩为原来的两倍
newThr = oldThr << 1; // double threshold
}
threshold = newThr;
//省略重新计算元素hash值的代码
}
二、Hashtable
一般这个会在面试的时候比较常问,比如HashMap跟Hashtable的区别?
主要有几个
(1)Hashtable是线程安全的,用Synchronized在方法层面进行加锁
(2)初始容量是11,加载因子一样,扩容为原有容量的两倍加1
protected void rehash() {
int oldCapacity = table.length;
int newCapacity = (oldCapacity << 1) + 1;
}
其他跟HashMap基本一样。
三、ConcurrentHashMap
这个属于JUC里面的。属于多线程并发安全。key和value都不能为空。
不一样的是其在设置值的时候用的是CAS(比较并交换,之前聊过)
casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))
Node类型数组是用volatile修饰的,
transient volatile Node<K,V>[] table;
跟HashMap同样是数组+链表的方式实现的,也会相应的在链表超过8的时候会转成红黑树。
接下来看一下put,put方法调用的putVal方法
V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//根据hash值得到数组对应下标的元素为null是,
//用CAS进行填充
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
//当hash=-1时,进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//用synchronized保证多线程并发
synchronized (f) {
if (tabAt(tab, i) == f) {
//省略,基本跟hashMap一样
}
}
if (binCount != 0) {
//如果链的长度大于8,则转化哼红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//扩容主要看此方法
addCount(1L, binCount);
return null;
}
扩容主要是在addCount方法里面
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//这里的CAS主要是看当前元素数量是否超过阈值(容量*加载因子)
//比如容量为16的话,BASECOUNT是阈值,baseCount是当前元素数量
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//省略无关代码
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//sizeCtl元素数量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
//省略。。。
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//最终扩容
transfer(tab, null);
s = sumCount();
}
}
}
最终扩容方法实在transfer方法里面
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//省略。。。
if (nextTab == null) { // initiating
try {
//n<<1,两倍扩容
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
}
//省略。。。
}
可以看到,扩容方式跟hashMap一样,也是两倍扩容。
这个比较复杂,基本就是跟Hashtable一样的,Hashtable也是线程安全的,但是Hashtable在读和写时都只能一个线程操作,而ConcurrentHashMap只在写的时候有加锁,读是分享的。
这个是java8的ConcurrentHashMap,而java7的实现是不一样的,java7是用Segment[]+HashEntry[]实现的,分段锁技术,先用key的hash值获取到哪一个Segment,然后再Segment中用hash值获取到具体的索引。但是查询效率低,所以改成java8的模式。
java7的结构:
java8的结构:(与HashMap是一样的)
四、LinkedHashMap
双向链表,继承自HashMap,没有重写put方法,初始容量,加载因子和扩容方式都跟HashMap一样。
双向链表是因为其Entry不同于HashMap,多了一个before引用。
结构如下:
五、WeakHashMap
弱键HashMap,具有跟HashMap一样的初始容量和加载因子,在系统GC的时候一定会被回收。之前在垃圾回收机制的时候说过弱引用的回收机制。
如果代码中有缓存需要的话,建议使用此类。
(1)底层也是数组加链表
//Entry数组
Entry<K,V>[] table;
//链表entry是继承自WeakReference的
class Entry<K,V> extends WeakReference<Object>
implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
(2)扩容也是两倍
上述就是Map的相关解析。难搞,英语不好看源码注释都看不太懂。
=======================================================
我是Liusy,一个喜欢健身的程序员。
提前祝大家伙中秋国庆快乐!!!
欢迎关注微信公众号【Liusy01】,一起交流Java技术及健身,获取更多干货。