复习HashMap
在jdk1.8的ConcurrentHashMap也是变成跟HashMap一样的数据结构,所以开始之前先复习一下jdk1.8的HashMap。
HashMap没有任何锁机制,所以线程不安全
HashMap底层维护了Node数组+Node链表+红黑树。
HashMap初始化和扩容只能是2的乘方
HashMap负载因子阈值是数组的0.75
HashMap链表尾插法
HashMap是懒加载机制
HashMap单链表大于8,数组长度大于64变成红黑树提高链表的查找速度。
HashMap无序(根据hash值确定数组位置)不重复(重复就是替换)。
HashMap的key和value允许为null
ConcurrentHashMap和HashMap和Hashtable三者的区别
HashMap:线程不安全
Hashtable:线程安全但是效率低
ConcurrentHashMap:线程安全,相比Hashtable效率高
源码
1、插入
一些参数
static final int MOVED = -1; // hash for forwarding nodes 转发节点的哈希
static final int TREEBIN = -2; // hash for roots of trees 红黑树root的哈希
static final int RESERVED = -3; // hash for transient reservations 临时保留的哈希
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash 正常节点散列的可用位,这里在算hash值时用到
sizeCtl
- -1:正在扩容
- 0:当前还未初始化
- 大于0
- 数组建议大小
- 阈值
大于0:
/*
Table initialization and resizing control. When negative, the table is being initialized or resized: -1 for initialization, else -(1 + the number of active resizing threads). Otherwise, when table is null, holds the initial table size to use upon creation, or 0 for default. After initialization, holds the next element count value upon which to resize the table.
机翻:table初始化和调整大小控制。如果为负,则table正在初始化或调整大小:-1 用于初始化,否则 -(1 + 活动调整大小线程的数量)。否则,当table为空时,保留创建时使用的初始表大小,或默认为 0。初始化后,保存下一个元素计数值,根据该值调整table的大小。
*/
private transient volatile int sizeCtl;
有参构造
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
// 有参构造给值的时候,sizeCtl会计算出一个值,2次幂
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
Unsaft
unsaft:他是操作内存,或者是一些java操作不到的操作,通过JNI(Java Native Interface:java本地接口)的方法
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
// 保证拿到的是最新值
// 不加原子性,可能拿的时候,另外一个线程插入了一个元素,但是内存不可见出现问题
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
// 竞争成功返回true并修改,竞争失败返回false不修改
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
putVal方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 与hashmap为null给0不同,这里不能为null
if (key == null || value == null) throw new NullPointerException();
// 得到hash值:(h ^ (h >>> 16)) & HASH_BITS;与hahsmap比多了一个& HASH_BITS操作
// 扰动函数,尽量为了随机化
int hash = spread(key.hashCode());
// 一个计数器,用来计数是否有冲突,且冲突的次数是多少,如果太多了,需要从链表变为红黑树
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// Node<K,V> f 头节点
// int n 数组长度
// int i 具体的数组索引下标
// int fh hash值
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 初始化table,懒加载
tab = initTable();
// tabAt:返回tab下标的node;
// i = (n - 1) & hash:算出下标
// 为什么拿这个slot中的node也要保证原子性???:拿的过程中,可能其他线程插入了一个,node不为null,但是内存不可见,条件还是满足,进入了下面代码:但是其他添加元素的时候还是拿的null做cas,就算出现不可见性,其实问题也不大
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 空node直接用cas添加元素,不用加锁
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 当前在扩容迁移,并且迁移到当前我要插入的这个slot了
else if ((fh = f.hash) == MOVED)
// 当前已经在扩容和迁移了,我要尝试加入进去
// 并发迁移,多线程一起完成迁移工作
tab = helpTransfer(tab, f);
else {
// 代表当前slot已经有节点了,发生了冲突
// 并且没有扩容
V oldVal = null;
// 锁的是头节点f
synchronized (f) {
// 首先判断,当前这个f与最新的节点是否相同。如果不相同直接退出,然后继续for自旋,再去插入
// 这里可能已经迁移一次了,当前f与最新的slot不相同,所以需要重新自旋,再找位置插入
if (tabAt(tab, i) == f) {
// static final int MOVED = -1; // hash for forwarding nodes 转发节点的哈希
// static final int TREEBIN = -2; // hash for roots of trees 红黑树root的哈希
// 节点hash值大于等于0说明是链表 -2是红黑树
if (fh >= 0) {
// bincount计数,判断是否要转红黑树
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;
}
}
}
// 如果fh小于0就走这边。这边是树的操作
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;
}
}
}
}
// bincount==0:说明数组已经变了,根本没插入
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 尝试转为红黑树
treeifyBin(tab, i);
// 如果oldval存在,不为空的情况下,就是发生了覆盖问题,需要将之前的值返回给调用put方法
if (oldVal != null)
// 覆盖不会执行addCount
return oldVal;
break;
}
}
}
// 元素计数:走到这里代表已经插入一个节点了,总长度需要++一次
// binCount,能知道当前是否冲突了,并且冲突次数是多少
// 多线程下++是不安全的:++不是原子性,局部变量表保存变量,操作数栈操作数据后再返回给局部变量表
addCount(1L, binCount);
return null;
}
initTable方法
private final Node<K,V>[] initTable() {
// Node<K,V>[] tab
// int sc
Node<K,V>[] tab; int sc;
// 双重检测锁:double check lock
while ((tab = table) == null || tab.length == 0) {
// sizeCtl全局变量:他有多层含义,不同时期,有不同的表示
// 扩容
if ((sc = sizeCtl) < 0)
// while循环:竞争失败的会再次进入,
// 这里使用yield让出CPU的资源,不消耗cpu资源,因为有线程在扩容了,只需要等待即可
Thread.yield(); // lost initialization race; just spin
// 线程争抢扩容的cas操作
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 如果有建议大小就选择建议大小
// 如果没有设置建议大小,就走默认16的大小
/* 这里用的compareAndSwapInt,失败的直接往后走再次进入到while中,然后yield了,
不存在说有线程在等待,锁释放后进入到这里再多初始化一个数组,
已经是一个原子性操作了,那么为什么还要加一个判断????
*/
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 {
// 这个地方解释了double check lock为什么要加第二个不为null的判断
// 初始化table成功后,sizeCtl做了改变,可能有线程会再次进入初始化代码
sizeCtl = sc;
}
break;
}
}
return tab;
}
2、节点计数
用的longadder的代码,唯一区别是concurrenthashmap没用到钩子方法