java中HashMap和ConcurrentHashMap实现原理
Posted Leo Han
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java中HashMap和ConcurrentHashMap实现原理相关的知识,希望对你有一定的参考价值。
在java中,HashMap底层是通过 数组 + 链表实现的,如果当某个链表元素个数超过8个,则链表转化为红黑树。
其底层类似上述结构,一层采用数组结构,每个数组里面都是一个链表,当我们进行put的时候,计算key的hash值,和数组的长度-1取余得到待添加元素在数组中的位置,然后看该位置是否有元素,如果没有元素,那么作为链表的头加入,如果有,加入到已有的链表中,如果链表的元素超过8个,这时候将链表转换为红黑树。
transient Node<K,V>[] table;
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next)
super(hash, key, val, next);
.....
static class Node<K,V> implements Map.Entry<K,V>
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next)
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
如上,Node
代表的是普通链表,而TreeNode
则是HashMap中红黑树的实现。
另外在HashMap中有一个比较重要的属性就是扩充因子,表示的是当Map中的容量到达初始容量的多少进行扩容,默认情况下,扩充因子是0.75,初始大小是16
,当map大小超过容量的0.75时,会进行扩容,每次对之前容量*2 进行扩容,即: 16 ,32,64
如下是HashMap添加元素的核心方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 初始化数组
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 对应位置上没有元素,直接加入
tab[i] = newNode(hash, key, value, null);
else
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 如果链表头部是红黑树,按照红黑树逻辑插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else
// 普通链表插入,插入完之后判断链表是否达到转红黑树条件,如果达到了,转红黑树
for (int binCount = 0; ; ++binCount)
if ((e = p.next) == null)
p.next = newNode(hash, key, value, null);
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;
当使用红黑树进行插入的时候,我们知道红黑树是有序的,默认是使用hashCode来进行排序的,如果hashCode相同的话,那么还会判断Key是否是Comparable,如果不是,那么会判断二者是否是同一个类型,如果还不行,则调用System.identityHashCode来进行判断
JDK8中的HashMap采用了红黑树,避免了之前JDK7中使用普通链表当元素碰撞导致链表过长查询慢问题
。
HashMap是非线程安全的,当多线程访问的时候,会出现问题,基于此java提供了ConcurrentHashMap
可以并发操作的Map。java本身也提供了HashTable,是一个线程安全的Map,但是HashTable在并发的情况下加锁的粒度比较大是整个Map,并发量会下降。
而ConcurrentHashMap则是根据HashMap底层数据结构的特点采用了分段上锁的逻辑,我们知道,当多个线程进行put的时候,每个key最终会落到数组其中一个元素上,如果多个线程每个操作的都是不同的数组元素,那么不存在竞争关系,那么只要每次操作对单个数组元素加锁,如果需要扩充Map,那么全局加锁即可。
ConcurrentHashMap的处理逻辑大致如上,上锁是结合CAS+Synchronized锁机制。
final 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)
// 数组位置没有元素,通过cas设置头结点
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
else if ((fh = f.hash) == MOVED)
// map在扩容,这里也会帮助扩容,思路和put差不多,只处理单个数组元素
tab = helpTransfer(tab, f);
else
V oldVal = null;
// 已经存在链表,通过synchronized 上锁处理
synchronized (f)
if (tabAt(tab, i) == f)
if (fh >= 0)
binCount = 1;
for (Node<K,V> e = f;; ++binCount)
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek))))
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
Node<K,V> pred = e;
if ((e = e.next) == null)
pred.next = new Node<K,V>(hash, key,
value, null);
break;
else if (f instanceof TreeBin)
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null)
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
if (binCount != 0)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
addCount(1L, binCount);
return null;
可以看到,在对应位置没有元素时是通过cas来设置,但是存在元素插入的时候,则是通过synchronized的,这主要是,没有元素的时候设置是一个比较快的过程,这时候如果存在并发,通过cas,并发线程不会自旋等待很久,但是如果有元素进行插入的时候,尤其如果是红黑树还需要进行旋转,这时候如果继续cas空转等待的话,浪费cpu和时间,所以直接通过synchronized来上锁,如果等待时间比较长,锁升级为重量级锁,线程直接阻塞出让CPU。
对于初始化数组,直接通过cas来进行设置:
private final Node<K,V>[] initTable()
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0)
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1))
try
if ((tab = table) == null || tab.length == 0)
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
finally
sizeCtl = sc;
break;
return tab;
以上是关于java中HashMap和ConcurrentHashMap实现原理的主要内容,如果未能解决你的问题,请参考以下文章