ConcurrentHashMap
这里主要分析的 jdk1.8中的ConcurrentHashMap,他是java之父Doug Lea之作,很多优秀的开源框架如tomcat、spring、中都大量用到了该类。
【基础好的同学往下走~】====================
首先,看jdk的源码不仅需要了解map是什么,为了更好的理解它到底怎么做到的无锁化还是线程安全的,最好对以下有所了解:
- 了解 他的数据结构:哈希表(散列表),知道它的原理和为什么。
- 线程的概念,生命周期、多线程、什么是线程不安全,为什么会有线程不安全,如何解决。
- 了解操作系统,cpu、内存、java代码如何转成字节码转成汇编转成二进制机器指令的,如何操作内存的。
- synchronize 关键字,java锁机制。
- volatie关键字,内存模型,storeload?
- DCL?为什么双重锁验证?单例模式中的dcl。
- CAS原理,如何保证?有什么缺陷?(最好顺便了解一下AQS)
- ---------------------------------------------------以上大多为理论,最好知晓
- java的锁?
- HashMap 的 API,大概源码和思路?
- ConcurrentHashMap API?
有基础的,没看过源码的直接看这里
看源码的思路?---------
了解他的API
了解数据结构和基本原理
(如果是大型框架:了解他的大概模型)
寻找切入点
(这里抛砖引玉,可以举一反三:初始化 , 经常用的api ,结束点等)
比如ConcurrentHashMap经常用到的API:从new,put,get方法开始看
基础好的直接进入正题-----------------------:
先来几个最简单的面试结论热身(前提jdk1.8):
- ConcurrentHashMap底层数据结构是哈希表(数组 + 链表 + 红黑树)
- put的返回值
- 默认大小为16
- 每次扩容大小为当前的两倍(table[ ] 的长度永远为2的n次幂)
- 默认扩容阈值为0.75
- 存放100个key,value对 不指定阈值时,初始大小为256
- Map<String, String> map = new ConcurrentHashMap<String, String>(100);之后调用 map.size的值为0
- 使用自定义key类型,需要实现hashcode() 和 equals() 方法
- 链表长度>8时链表有可能转为红黑树
- 链表加入元素为尾插法
- 红黑树结点 < 6时转为链表
- 整个API几乎没什么锁
- 扩容时,可多个线程一起扩容
- 扩容时每个线程获取的任务量和CPU有关
- sizeCtl 含义很多
- .....
知道它是什么,就要问一下为什么,对照上面几个简单结论反问一下自己为什么选用这个,为什么这么做(热身):
- 哈希表的查找效率比链表、数组、树快很多,时间复杂度为O(1)
- 如果put的key相同,则返回 oldValue 旧的value值,否则为null,使hashmap使用更灵活
- 16是研究结论
- 数组大小永远为2的n次幂是为了后面快速的hash计算,用位运算代替 取余,详细参考下文内容
- 0.75是参考值,可修改,0.75的计算为(当前大小 - 当前大小无符号右移2位)也比较快
- 长度始始终为2的n次幂,所以需要至少128的大小,未指定阈值,采用默认值0.75,100/128 > 0.75 会触发扩容,因此为256
- 初始化在第一次put,无论你是否指定他大小,优点参考懒加载的优点
- 在第一次hash时候,调用hashcode 方法确定位置,hashcode 相同时会调用equals() 进行比较,因此需要实现
- 链表长度>8,且table长度 > 64 时才转化为红黑树,否则触发扩容,研究结论,8:参考泊松分布,8的概率<千万分之一
- 尾插法不如头插法快,但头插法可能造成死循环,同时为了兼容红黑树,因此尾插法更合适
- < 6 而不是 7 是为了有弹性,避免频繁发生 链表 ---- 红黑树的转换
- 重量锁会严重降低并发性能,采用CAS来替代锁
- 有一个线程触发扩容,其他使用ConcurrentHashMap线程不会袖手旁观,会进来帮助一起扩容,每个线程领取一份扩容任务,大家一起做,扩容会更快。
- 充分压榨CPU的性能,杜绝性能浪费
- 1.减少内存占用 2.体现写代码人技能巧夺天工
应届生知道以上几个结论一般简单面试够了,但若想碾压面试官,还得继续往下看--
面试题是不断变化的,想要兵来将挡水来土掩就得看源码,看他到底是如何做的。
下面以put为例,简单分析:
put可以放键值对、也可以放map,这里只看最简单的键值对
public V put(K key, V value) {
return putVal(key, value, false);
}
发现put调用了putVal 方法,且最后一个参数为false,意思是如果key存在,则更新value值,返回旧val值。如果putVal最后一个参数为true如果key存在,则只返回旧val值。
下面是putVal(),刚开始看源码时不要看到函数就点进去一直点,更不要断点一步一步看,先看个大概思路
小白一定要看我写的注释!
final V putVal(K key, V value, boolean onlyIfAbsent) {
//ConcurrentHashMap中key和value都不允许为空
if (key == null || value == null) throw new NullPointerException();
//key的哈希值,忍住好奇!先不看其原理
int hash = spread(key.hashCode());
//冲突次数 或者说 链表存放尝试次数
int binCount = 0;
for (Node<K,V>[] tab = table;;) {//相当于while true
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) {//如果table对应位置为空,DCL机制存放到相应位置,并跳出循环
//暂时省略下面代码...
}
addCount(1L, binCount);//map中的key个数+1
return null;
}
好的,上面我了解了没有冲突时候他是怎么放的,很简单,继续看。一定要看注释!
final V putVal(K key, V value, boolean onlyIfAbsent) {
//ConcurrentHashMap中key和value都不允许为空
if (key == null || value == null) throw new NullPointerException();
//key的哈希值,忍住好奇!先不看其原理
int hash = spread(key.hashCode());
//冲突次数 或者说 链表存放尝试次数
int binCount = 0;
for (Node<K,V>[] tab = table;;) {//相当于while true
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) {//如果table对应位置为空,DCL机制存放到相应位置,并跳出循环
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 {//如果不走上面,一定会走这,意思是table数组,想放的哪个位置已经有人了
V oldVal = null;//用于保存旧的value值
synchronized (f) {//这里加了个锁,这个f是table[]的一个小格子,很微小,只锁定了hashcode相同的key的操作
if (tabAt(tab, i) == f) {//DCL机制,再来看看现在的f是不是之前的f,因为之前没加锁,有可能会不一样嘛
if (fh >= 0) {//fh就是f的hashcode值,上面有,为什么要判断这,小本本上记一下,我这里疑惑。
binCount = 1;//尝试次数为1了,因为放到table对应位置,发现不为null才来的这里
for (Node<K,V> e = f;; ++binCount) {//这个for循环没必要刚开始就仔细看,我猜它的作用就是把新的key放到链表尾部
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) {//把新的key放到链表尾部
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}//if (fh >= 0)的结束处
//else if ...我看了if了,先不看你
}
}
if (binCount != 0) {//发现这里判断binCount ,按照刚才的思路,binCount 肯定大于0,所以肯定会执行,那就看看
if (binCount >= TREEIFY_THRESHOLD)//点TREEIFY_THRESHOLD发现它是8,英文介绍说他是转成红黑树的阈值,和刚才的热身题对应起来了。吆西。
treeifyBin(tab, i);//转成红黑树,咋转的先不看,好奇心害死猫,但mark一下,是在这里转的
if (oldVal != null)//如果是覆盖了原来的值,那就返回原来的值
return oldVal;//返回
break;
}
}
}
addCount(1L, binCount);//map中的key个数+1
return null;
}
好的,如果上面明白了,那么,继续往下看,来看看刚才注释掉的else if 是干嘛的,一定要看注释!
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;
}
}
读者:嗯哼,putVal我搞定了,大概知道它咋做的了,我真牛逼!
对,是很厉害了,但这是个大概,我们刚才有个地方不是记在小本本上了?还不懂呢,嗯嗯,继续看,但是,先不看这。
再来看一下我刚才看过的这个putVal()中调用了哪些我不知道的函数,帮你们MARK列一下:
先从代码里看,有利于各位保持思路:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//············### MARK ### ···············
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();//············### MARK ### ···············
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//············### MARK ### ···············
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);//············### MARK ### ···············
else {
V oldVal = null;
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;//············### MARK ### ···············
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);//············### MARK ### ···············
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);//············### MARK ### ···············
return null;
}
帮你们列好了,还有这些不懂呢:
- spread(key.hashCode());------------------计算hash
- initTable();------------------初始化,
- tabAt(tab, i = (n - 1) & hash))------------------查看内存中最新值
- casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))------------------用CAS的方式插入新结点
- helpTransfer(tab, f);------------------???
- putTreeVal(hash, key,value)--------------把新结点放到红黑树里
- treeifyBin(tab, i);--------------------把 tab 第 i 个节点下的链表转成红黑树
- addCount(1L, binCount);------------------执行完putVal,相当于count ++
本篇完成,下篇继续,喜欢点赞关注~
ConcurrentHashMap源码分析,轻取面试Offer(二)
注:本篇为CSDN--Star_Java原创,转载请注明出处!