1.hashmap
当冲突的数据较少时使用的是链式冲突避免的方式,当数据量很大的时候用的是红黑树。
hashmap放入数据:put方法
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 若哈希表的数组tab为空,则 通过resize() 创建
// 所以,初始化哈希表的时机 = 第1次调用put函数时,即调用resize() 初始化创建
// 关于resize()的源码分析将在下面讲解扩容时详细分析,此处先跳过
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算插入存储的数组索引i:根据键值key计算的hash值 得到
// 此处的数组下标计算方式 = i = (n - 1) & hash,同JDK 1.7中的indexFor(),上面已详细描述
// 3. 插入时,需判断是否存在Hash冲突:
// 若不存在(即当前table[i] == null),则直接在该数组位置新建节点,插入完毕
// 否则,代表存在Hash冲突,即当前存储位置已存在节点,则依次往下判断:a. 当前位置的key是否与需插入的key相同、b. 判断需插入的数据结构是否为红黑树 or 链表
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // newNode(hash, key, value, null)的源码 = new Node<>(hash, key, value, next)
else {
Node<K,V> e; K k;
// a. 判断 table[i]的元素的key是否与 需插入的key一样,若相同则 直接用新value 覆盖 旧value
// 判断原则:equals()
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// b. 继续判断:需插入的数据结构是否为红黑树 or 链表
// 若是红黑树,则直接在树中插入 or 更新键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); ->>分析3
// 若是链表,则在链表中插入 or 更新键值对
// i. 遍历table[i],判断Key是否已存在:采用equals() 对比当前遍历节点的key 与 需插入数据的key:若已存在,则直接用新value 覆盖 旧value
// ii. 遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
// 注:新增节点后,需判断链表长度是否>8(8 = 桶的树化阈值):若是,则把链表转换为红黑树
else {
for (int binCount = 0; ; ++binCount) {
// 对于ii:若数组的下1个位置,表示已到表尾也没有找到key值相同节点,则新建节点 = 插入节点
// 注:此处是从链表尾插入,与JDK 1.7不同(从链表头插入,即永远都是添加到数组的位置,原来数组位置的数据则往后移)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 插入节点后,若链表节点>数阈值,则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash); // 树化操作
break;
}
// 对于i
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 更新p指向下一个节点,继续遍历
p = e;
}
}
// 对i情况的后续操作:发现key已存在,直接用新value 覆盖 旧value & 返回旧value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 替换旧值时会调用的方法(默认实现为空)
return oldValue;
}
}
++modCount;
// 插入成功后,判断实际存在的键值对数量size > 最大容量threshold
// 若 > ,则进行扩容 ->>分析4(但单独讲解,请直接跳出该代码块)
if (++size > threshold)
resize();
afterNodeInsertion(evict);// 插入成功时会调用的方法(默认实现为空)
return null;
}
hashmap取出数据:get方法
/**
* 函数原型
* 作用:根据键key,向HashMap获取对应的值
*/
map.get(key);
/**
* 源码分析
*/
public V get(Object key) {
Node<K,V> e;
// 1. 计算需获取数据的hash值
// 2. 通过getNode()获取所查询的数据 ->>分析1
// 3. 获取后,判断数据是否为空
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 分析1:getNode(hash(key), key))
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1. 计算存放在数组table中的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 4. 通过该函数,依次在数组、红黑树、链表中查找(通过equals()判断)
// a. 先在数组中找,若存在,则直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// b. 若数组中没有,则到红黑树中寻找
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// c. 若红黑树中也没有,则通过遍历,到链表中寻找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
2.concurrentHashmap
concurrentHashMap大量使用UnSafe中的方法来保证其原子性。用到了volatile+CAS来保证并发的可靠性。
concurrentHashMap在有冲突的节点上加上同步锁来保证其并发的可靠性。相比jdk1.7在每个bucket操作,粒度更小了。
put方法:
final V putVal(K key, V value, boolean onlyIfAbsent) {
//与HashMap不同,ConcurrentHashMap不允许null作为key或value
if (key == null || value == null) throw new NullPointerException();
//计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//若table为空的话,初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//若当前数组i位置上的节点为null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS插入节点(V:当前数组i位置上的节点; O:null; N:新Node对象)
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//当前正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//锁住当前数组i位置上的节点
synchronized (f) {
//判断是否节点f是否为当前数组i位置上的节点,防止被其它线程修改
if (tabAt(tab, i) == f) {
//当前位置桶的结构为链表
if (fh >= 0) {
binCount = 1;
//遍历链表节点
for (Node e = f;; ++binCount) {
K ek;
//若hash值与key值相同,进行替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
//若链表中找不到,尾插节点
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
//当前位置桶结构为红黑树,TreeBin哈希值固定为-2
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
//遍历红黑树上节点,更新或增加节点
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//若链表长度超过8,将链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//节点数+1,若超过阈值则扩容
addCount(1L, binCount);
return null;
}
/**
* hash算法
*/
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
get方法:
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
//计算hash值
int h = spread(key.hashCode());
根据hash值确定节点位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//桶首节点的key与查找的key相同,则直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//树节点场景
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//链表场景
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
3.hashtable
hashtable直接计算是否冲突,冲突则直接覆盖原来的值或者扩容,不冲突直接加入其中
hashtable的同步锁是一进入hashtable就会被锁上,相对来说并发性能较低
hashtable会遍历查看是否已经存在需要添加的key-value,若已经存在则用新值替换老值,并返回老值,否则新增节点,这个操作主要是在addEntry方法中进行,如下是addEntry方法的源码,其流程是判断当前元素个数是否大于扩容阈值,若大于则rehash,否则新增节点并将该节点添加到对应的位置
hashtable的put方法:
public synchronized V put(K key, V value) {// 加锁同步,保证Hashtable的线程安全性
// Make sure the value is not null
if (value == null) {// 不同于HashMap,Hashtable不允许空的value
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode(); // key 的哈希值,同时也暗示Hashtable不同于HashMap,其不允许空的key
int index = (hash & 0x7FFFFFFF) % tab.length; // 取余计算节点存放桶位,0x7FFFFFFF 是最大的int型数的二进制表示
// 先查找Hashtable上述桶位中是否包含具有相同Key的K/V对
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 向Hashtable中插入目标K/V对
addEntry(hash, key, value, index);
return null;
}
//下面是调用的private void addEntry(int hash, K key, V value, int index) 方法,此处直接粘贴过来
private void addEntry(int hash, K key, V value, int index) {
modCount++;// 发生结构性改变,modCount加1
Entry<?,?> tab[] = table;
if (count >= threshold) { //在插入目标K/V对前,先检查是否需要扩容(不同于HashMap的插入后检查是否需要扩容)
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length; // 扩容后,重新计算K/V对插入的桶位
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e); // 将K/V对链入对应桶中链表,并成为头结点
count++; // Hashtable中Entry数目加1
}
hashtable的get方法:
public synchronized V get(Object key) { // 不同于HashMap,Hashtable的读取操作是同步的
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; // 定位K/V对的桶位
for (Entry<K, V> e = tab[index]; e != null; e = e.next) { // 在特定桶中依次查找指定Key的K/V对
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null; // 查找失败
}
4.hashset
hashset是直接有hashmap实现的
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
添加元素也是直接由hashmap的put方法实现,只不过有PRESENT构造一个空的value
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
PRESENT的本质就是一个空的object
private static final Object PRESENT = new Object();
参考文献:
https://blog.csdn.net/carson_ho/article/details/79373134
https://juejin.im/post/5b53d1adf265da0f70070e3d
https://blog.csdn.net/justloveyou_/article/details/72862373
https://www.jianshu.com/p/6c95f8216950