之前介绍了Java并发的基础知识和使用案例分析,接下来我们正式地进入Java并发的源码分析阶段,本文作为源码分析地开篇,源码参考JDK1.8

OverView:

  JDK1.8源码中的注释提到:ConcurrentHashMap是一种提供完整的并发检索和对于并发更新有高预测性的散列表,遵循了与HashMap相同的功能性规格,并包含与HashTable每个方法都对应的方法.虽然所有操作都是线程安全的,但检索操作并不牵涉任何锁,不支持任何锁住整个散列表来保护所有的访问.

  ConcurrentHashMap不会和HashTable一样,滥用synchronized关键字,从而令许多操作会锁住整个容器,而是采用分段锁技术,它的加锁粒度更细,并发访问环境下能获得更好高的吞吐量;

ConcurrentHashMap的数据结构如下图所示

JDK1.8虽然已经摒弃Segment来存儲,但他仍然兼容Segment,只是它被用在序列化实现上了,直接用Node数组+链表(或红黑树)的数据结构来实现,并发控制使用Synchronized和CAS来实现;

部分核心源码分析:

构造器:

public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//三个参数分别是:集合容量,负载因子,并发级别
//集合容量即预计的集合容量
//负载因子是指集合的密度,或者说是有效存儲占比
//并发级别即预估的并发修改集合的线程个数
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel; //构造集合时,容量默认大于等于并发级别
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
//计算长度时要带上避免哈希冲突的链地址法产生的额外的存储空间再加1 int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
//限制集合的最大长度 this.sizeCtl = cap;
//sizeControl:扩容用的控制变量
}

putVal()方法:put()底层调用了putVal()用于插入键值对

final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
     //返回(h ^ (h >>> 16)) & HASH_BITS
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
       //遍历整个table数组,tab[i]即链表或红黑树的首节点
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
       //如果整个table为空,就初始化这个table;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        //table不为空并且长度大于0,并且桶为空
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
                  //CompareAndSwap(CAS),如果tab[i]为空就将新的元素替换进去
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
          //如果tab[i]的hash为MOVED,代表需要转移节点
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
            //在每个tab上加私有锁,粒度更细
if (tabAt(tab, i) == f) {
if (fh >= 0) {
                 //找到hash大于0的tab[i]节点
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;
                      //判断hash和key是否相等,相等就保存该点的值
if (!onlyIfAbsent)
e.val = value;
                      //将指定值value保存到节点上
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
                      //判断是否为bin的尾节点
pred.next = new Node<K,V>(hash, key,
value, null);
break;
                      //若是,就生成一个节点放在尾部,并跳出循环
}
}
}
else if (f instanceof TreeBin) {
               //如果tab[i]是TreeBin类型,说明这个Bin是红黑树结构,执行红黑树的插入操作
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);
            //当tab[i]下的元素大于等于预设阈值8时,就将整个bin转变为红黑树存儲
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

replaceNode方法:被remove()底层调用,用于删除节点

final V replaceNode(Object key, V value, Object cv) {
// 计算key的hash值
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
          
// table为空或表长度为0或key对应的bin为空或长度为0,直接跳出循环
break;
else if ((fh = f.hash) == MOVED) // bin中第一个结点的hash值为MOVED就转移节点
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) { // bin中首节点没有变化,结点hash值大于0
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { // 结点的hash值与指定的hash值相等,并且key也相等
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) { // cv为空或者与结点value相等或者不为空并且相等
// 保存该结点的val值
oldVal = ev;
if (value != null) // value为null
// 设置结点value值
e.val = value;
else if (pred != null) // 前驱不为空
// 前驱的后继为e的后继,即删除了e结点
pred.next = e.next;
else
// 设置table表中下标为index的值为e.next
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) { // 节点为红黑树结点类型
validated = true;
// 类型转化
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) { // 根节点不为空并且存在与指定hash和key相等的结点
// 保存p结点的value
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) { // cv为空或者与结点value相等或者不为空并且相等
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p)) // 移除p结点
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}

get()方法:获取对应键的值

public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
       //如果table不是空,且table的长度>0,且bin不为空
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
          //若表中元素的hash和入参的hash相等,且两者的键一致,就返回该节点的值
return e.val;
}
else if (eh < 0)
          //若表中节点的hash小于0,就在tab[i]的bin里找相同键的节点,若找到了就返回该节点的值,若没找到,就返回空
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))))
          //若表中节点的hash大于等于0,就遍历其所有后继节点并判断是否与存在相同的hash和键,有就返回节点的值
return e.val;
}
}
return null;
}

treeifyBin()方法:用于将链表转化成红黑树的数据结构,考虑到若一个散列桶(bin)中的节点太多,链表存儲的话查找节点的效率就会变成线性,所以在节点数大于一个阈值之后,会将之转化为红黑树,提升查找效率为O(logn)

private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
          //表不为空的条件下,表长小于阈值8,就对表进行扩容
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
          //表不为空的条件下,bin中存在节点,并且节点的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) {
                 //遍历bin中的所有节点
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;
                   //更新临时节点变量tl
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
              //设置table表中的下标为hd
}
}
}
}
}

size()方法:可以看到size()方法内并没有任何的同步机制,故size()所获得的值可能已经是过时的,因此只能仅仅作为一个估计值,尽管在并发环境下用处不大,但仍然可以作为我们权衡利弊的因素

public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}

小结:

  JDK1.8的ConcurrentHashMap相比之前版本的ConcurrentHashMap有很了大的改进与不同,从源码中可以发现,每个同步代码块都是作为每个散列桶(bin)的私有锁来实现线程同步的,故每个bin之间的操作都是可以异步完成的,至于多线程的执行效率方面,就取决于散列桶的数量和散列函数的合理性(节点分配的均匀程度)了.

ConcurrentHashMap带来的结果是,在并发访问的环境下实现更高的系统吞吐量,在单线程环境下损失非常小的性能.并且,它的迭代器并不会抛出ConcurrentModifi-cationException,因此不需要再迭代的时候对容器加锁.

                        ----Java并发编程实战   

参考资料:

Java并发编程实战

https://www.cnblogs.com/lujiango/p/7580558.html

https://www.cnblogs.com/everSeeker/p/5601861.html

https://www.cnblogs.com/leesf456/p/5453341.html

Java并发(四):并发集合ConcurrentHashMap的源码分析的更多相关文章

  1. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  2. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  3. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  4. 【死磕 Java 集合】— ConcurrentSkipListMap源码分析

    转自:http://cmsblogs.com/?p=4773 [隐藏目录] 前情提要 简介 存储结构 源码分析 主要内部类 构造方法 添加元素 添加元素举例 删除元素 删除元素举例 查找元素 查找元素 ...

  5. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  6. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  7. 死磕 java集合之ArrayDeque源码分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...

  8. 死磕 java集合之LinkedList源码分析

    问题 (1)LinkedList只是一个List吗? (2)LinkedList还有其它什么特性吗? (3)LinkedList为啥经常拿出来跟ArrayList比较? (4)我为什么把LinkedL ...

  9. Java8集合框架——LinkedList源码分析

    java.util.LinkedList 本文的主要目录结构: 一.LinkedList的特点及与ArrayList的比较 二.LinkedList的内部实现 三.LinkedList添加元素 四.L ...

随机推荐

  1. 安装 ambaria

    hadoop安装 wget http://public-repo-1.hortonworks.com/ambari/centos6/1.x/updates/1.2.4.9/ambari.repo cp ...

  2. NodeJS”热部署“代码,实现动态调试(hotnode,可以实现热更新)

    NodeJS”热部署“代码,实现动态调试   开发中遇到的问题 如果你有 PHP 开发经验,会习惯在修改 PHP 脚本后直接刷新浏览器以观察结果,而你在开发 Node.js 实现的 HTTP 应用时会 ...

  3. C#使用Json.NET解析Json

    本文转载自 http://xiaosheng.me/2016/10/01/article25/ 最近在 C# 项目中需要使用到 Json 格式的数据,我简单上网搜索了一下,基本上有两种操作 Json ...

  4. HierarchyId通过父节点创建一个新的子节点

    --HierarchyId通过父节点创建一个新的子节点 CREATE TABLE #temp( node HierarchyID ); insert into #temp select '/' uni ...

  5. C++基础之C++编译调试

     C++程序的实现(预处理,编译,连接)Linux平台编译gcc和g++都是GNU的编译器.1.对于.c后缀的文件,gcc把它当做是C程序:g++当做是C++程序:2.对于.cpp后缀的文件,gcc和 ...

  6. JAVA对象创建的过程

    Java中一个实例对象被创建的过程 一.类的加载过程 首先,Jvm在执行时,遇到一个新的类时,会到内存中的方法区去找class的信息,如果找到就直接拿来用,如果没有找到,就会去将类文件加载到方法区.在 ...

  7. matlab求定积分和不定积分

    matlab求定积分与不定积分 创建于2018-03-21 22:42 求定积分与不定积分是一件比较繁琐的事,但是我们可以借助matlab,下面与大家分享解决方法 材料/工具 matlab 求不定积分 ...

  8. weex前端式写法解决方案---eros

    前言 如果想用前端的方式写一个app怎么办呢? 如果你用的是 React,那么它已经有了一个比较完善的体系跟社区.如果你用的是Vue又不想花费太多时间去重新学习React,那么目前比较靠谱的方案就是w ...

  9. c语言数组相关的计算

    1.数组的创建:元素类型 数组名 [常量或者常量表达式] 如:int arr1[10];注:即使是被const修饰的变量也不能作为[]中的内容,它本质上依然属于变量,只是具有常量属性2.数组的初始化: ...

  10. cf822D(质因子)

    题目链接: http://codeforces.com/problemset/problem/822/D 题意: 输入 t, l, r 求 t0·f(l) + t1·f(l + 1) + ... +  ...