一、ConcurrentHashMap的线程安全性
ConcurrentHashMap的线程安全性是指:Map的数据结构不会被破坏,比如对具有相同hash值而不相等的key进行put操作的时候,可以保证两个key都被放在链表(红黑二叉树)中。
ConcurrentHashMap的线程安全指的是,它的每个方法单独调用(即原子操作)都是线程安全的,但是代码总体的互斥性并不受控制。
比如如下代码,实际上并不是原子操作,它包含了三步:
- map.get
- 加1
- map.put
map.put(KEY, map.get(KEY) + 1)
其中第1和第3步,单独来说都是线程安全的,由ConcurrentHashMap保证。但是由于在上面的代码中,map本身是一个共享变量。当线程A执行map.get的时候,其它线程可能正在执行map.put,这样一来当线程A执行到map.put的时候,线程A的值就已经是脏数据了,然后脏数据覆盖了真值,导致线程不安全
简单地说,ConcurrentHashMap的get方法获取到的是此时的真值,但它并不保证当你调用put方法的时候,当时获取到的值仍然是真值
如下是ConcurrentHashMap线程安全和不安全的实列。注意ThreadPool的awaitTermination的必要性,主线程需要等待线程池中所有的线程都运行完毕后才去拿ConcurrentHashMap中key为“count”的值,这样才能保证拿到的是最终的值。
package com.itheima.security.springboot.map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TestConcurrentHashMap {
private static final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newFixedThreadPool(10);
//不安全的列子
/*for (int i = 0; i < 8; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
testUnsafe1();
}
});
}*/
//安全的列子
for (int i = 0; i < 8; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
testSafe2();
}
});
}
executorService.shutdown();
//awaitTermination时,主线程会处于一种等待的状态,等待线程池中所有的线程都运行完毕后才继续运行。
executorService.awaitTermination(1, TimeUnit.DAYS);
System.out.println(map.get("count"));
}
private static void testUnsafe1() {
Integer count = map.get("count");
if (count == null) {
map.put("count", 1);
} else {
map.put("count", count + 1);
}
}
private static void testSafe2() {
for (; ; ) {
Integer count = map.get("count");
if (count == null) {
//不存在,则那么会向map中添加该键值对,并返回null;如果已经存在,那么不会覆盖已有的值,直接返回已经存在的值。
if (map.putIfAbsent("count", 1) == null) {
break;
}
} else {
if (map.replace("count", count, count + 1)) {
break;
}
}
}
}
}
二、ConcurrentHashMap的putVal方法
ConcurrentHashMap的put方法和putIfAbsent方法都调用了putVal方法,如下:
public V put(K key, V value) { return putVal(key, value, false); }public V putIfAbsent(K key, V value) { return putVal(key, value, true); }
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key和value的值不允许为null
if (key == null || value == null) throw new NullPointerException();
//计算出key的hash值
int hash = spread(key.hashCode());
//申明出一个标识,暂时不管
int binCount = 0;
//自旋,申明tab,为当前ConcurrentHashMap的数组
for (Node<K,V>[] tab = table;;) {
//f:当前索引位置的数据(node)
Node<K,V> f; int n, i, fh;
//如果数组为null或者数组的长度为0
if (tab == null || (n = tab.length) == 0)
//初始化数组
tab = initTable();
//数组已经初始化了,将数据插入到map中
//tabAt(数组,i):获取数组中i索引位置的数据(tab[i])
//(n - 1) & hash:数组长度-1 & key的hash值,计算出来的结果作为索引,配合tab[i]方法获取这个索引位置的数据
//==null:说明当前索引位置没有数据,将数据插入到当前位置
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//casTabAt(数组,i,2,3):以CAS的方式,将数组tab中i位置的数据由2改为3
//插入成功返回true,跳出for循环;插入失败返回false
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)
tab = helpTransfer(tab, f);
else {
//出现了hash冲突,需要将数据挂到链表上或者添加到红黑二叉树中
V oldVal = null;
//锁住当前桶的位置(f:当前索引位置的数据(node))
//f:当前桶的位置的数据(node)
synchronized (f) {
//获取数组中i索引位置的数据(tab[i]),判断和锁定的数据(node)是不是同一个
if (tabAt(tab, i) == f) {
//fh当前桶数据的hash值,fh >= 0,当前桶下是链表或者为null;
if (fh >= 0) {
//表示设置为1
binCount = 1;
//把当前桶位置的数据(node):f赋值给e
for (Node<K,V> e = f;; ++binCount) {
K ek;
//把当前桶位置的数据(node)的hash值与key的hash值相等
if (e.hash == hash &&
//是相同的key,则不是添加二十修改操作!!!
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//获取当前位置的旧值
oldVal = e.val;
//是否是putIfAbsent操作?onlyIfAbsent为false:覆盖原有的value;如果为true,什么都不做
if (!onlyIfAbsent)
e.val = value;
//跳出循环
break;
}
Node<K,V> pred = e;
//说明不是修改,而是追加操作
if ((e = e.next) == null) {
//如果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) {
//如果binCount大于等于8,如果大于等于8则需要判断是否需要将链表转换成红黑二叉树
if (binCount >= TREEIFY_THRESHOLD)
//将链表转换成红黑二叉树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
三、ConcurrentHashMap的散列算法spread
static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; }
1.为什么要h >>> 16
h为key的hashCode,为32位,位了尽量避免hash冲突,我们就需要让hashCode的高16位也参与运算,所以对hashCode右移16位(去掉低位,高位变低位,高位补0)。
然后在与原来的hashCode进行异或运算,其结果在与0x7fffffff进行与运算
要与0x7fffffff进行与运算的原因?是为了保证key的hashcode的值一定为正数,因为key的hashcode值为负数由其他的意义,如下:
static final int MOVED = -1; // hash for forwarding nodes 数据正在迁移 static final int TREEBIN = -2; // hash for roots of trees 数据下面是一颗树 static final int RESERVED = -3; // hash for transient reservations 该位置已经被预定
2.tabAt(tab, i = (n - 1) & hash)
在获取数组中i索引位置数据的时候,使用数组长度-1(n-1)的方式,因为数组长度为2的n次方倍,所以数组长度的二进制数低位一般为0。一个为0的数与其他数(0或1)进行与(&)运算的时候,都为0。所以为了让数据在tab[]中更加的分散,使用(n - 1) & hash方式。
四、initTable()方法
sizeCtl四个值得解释: -1:表示map正在初始化; 小于-1:表示正在扩容;如-2表示有一个线程正在扩容,-3表示有两个线程正在扩容 0:还没有初始化; 正数:如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值。
//初始化数组
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//判断数组是否已经初始化
while ((tab = table) == null || tab.length == 0) {
//sizeCtl赋值给sc,判断是否小于0(正在初始化或者扩容)
if ((sc = sizeCtl) < 0)
//礼让一下(让线程由运行状态变成就绪状态,可能又立马获得CPU的时间片)
Thread.yield(); // lost initialization race; just spin
//以cas的方式把SIZECTL设置成-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//CAS成功说明我要开始初始化了
try {
//再次做了一次判断:判断数组是否已经初始化
if ((tab = table) == null || tab.length == 0) {
//获取数组初始化的长度;如果sc>0(如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值),那么就以sc为长度;否则就取得默认值16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
//table已经初始化好了
table = tab = nt;
//sc>0(如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值)
//计算扩容的阈值,赋值给sc(0.75)
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
五、将链表转换成红黑二叉树treeifyBin(tab, i)
/**
* Replaces all linked nodes in bin at given index unless table is
* too small, in which case resizes instead.
*/
//转红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//获取数组的长度是否小于64,小于64的话尝试扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//尝试扩容
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
tryPresize(n << 1):尝试扩容
/**
* tryPresize(n << 1):长度扩容为之前的2倍
* Tries to presize table to accommodate the given number of elements.
*
* @param size number of elements (doesn't need to be perfectly accurate)
*/
private final void tryPresize(int size) {
//对数组扩容长度做一波判断;达到最大值则取最大值;然后是保证长度是2的n次方幂:tableSizeFor(size + (size >>> 1) + 1)
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
//sizeCtl四个值得解释:
//-1:表示map正在初始化;
//小于-1:表示正在扩容;
//0:还没有初始化;
//正数:如果没有初始化,代表要初始化得长度;如果已经初始化了,代表扩容得阈值。
while ((sc = sizeCtl) >= 0) {
//有两种可能,第一种没有初始化数组(putAll方法);第二种数组已经初始化(正常流程过来的)
Node<K,V>[] tab = table; int n;
//初始化数组的操作
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
//如果扩容长度已经小于扩容阈值(扩容完事了)
//数组长度大于等于最大长度
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//开始扩容
else if (tab == table) {
//得到一个扩容戳,(长度32位的数值,高16位做扩容标识,低16位做扩容线程数)
int rs = resizeStamp(n);
//sc<0表示已经开始扩容了,帮助扩容。
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//暂时没有线程在扩容,暂时设置SIZECTL标志,开始扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}