最近在学习并发map的源码,如果由错误欢迎指出。这仅供我自己学习记录使用。

首先就先来说一下几个全局变量

    private static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量2的30次方
private static final int DEFAULT_CAPACITY = 16; //默认容量 1<<4 private static final float LOAD_FACTOR = 0.75f; //负载因子
static final int TREEIFY_THRESHOLD = 8; //链表转为红黑树,大于8小于6先对链表数组进行翻倍扩容操作
static final int UNTREEIFY_THRESHOLD = 6; //树转列表
static final int MIN_TREEIFY_CAPACITY = 64; //链表真正转为红黑树
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;//stamp高位标识移动位数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int MOVED = -1; // forwarding nodes 的hash值,如果hash值等于-1代表线程协助扩容
static final int TREEBIN = -2; // roots of trees 的hash值,如果hash等于-2代表,当前桶是红黑树
static final int RESERVED = -3; // transient reservations 的hash值 // usable bits of normal node hash,在hash计算的时候运用到,与HashMap计算出来的hash值进行与操作
static final int HASH_BITS = 0x7fffffff; static final int NCPU = Runtime.getRuntime().availableProcessors(); //可用处理器数量

然后是几个全局属性

transient volatile Node<K,V>[] table;//当前ConcurrentHashmap的Node数组,正在使用的数组

private transient volatile Node<K,V>[] nextTable;//ForwardNode所指向的下一个表,正在扩容的数组(还未使用)

private transient volatile long baseCount;//如果使用CAS计数成功,使用该值进行累加,计数用的

//扩容设置的参数,默认为0,当值=-1的时候,代表当前有线程正在进行扩容操作
//当值等于-n的时候,代表有n个线程一起扩容,其中n-1线程是协助扩容
//当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75
private transient volatile int sizeCtl;//标记状态以及数组阈值 private transient volatile int transferIndex;//数组扩容的时候用到 private transient volatile int cellsBusy; //如果使用CAS计算失败,也就是说当前处于高并发的情况下,那么
//就会使用CounterCell[]数组进行计数,类似jdk1.7分段锁的形式,锁住一个segment
//最后size()方法统计出来的大小是baseCount和counterCells数组的总和
private transient volatile CounterCell[] counterCells;//计数数组。

首先是有参构造,这里如果是

ConcurrentHashMap chm = new ConcurrentHashMap(15);

那么其实容量不是15,而是32;

public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}

从这里可以看出是看tableSizeFor这个方法的,15+7+1=23;

 private static final int tableSizeFor(int c) {
int n = c - 1;//23-1=22 0b10110
n |= n >>> 1;// 10110 | 01011 = 11111,下面都是11111也就是31
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//31+1 = 32
}

所以这就是最后的容量,为32,也就是有参的参数的两倍最近的2的次方数。

接下来就将put方法

final V putVal(K key, V value, boolean onlyIfAbsent) {//onlyIfAbsent跟hashmap一样,就是判断是否要覆盖,默认为false,覆盖。
if (key == null || value == null) throw new NullPointerException();////这句话可以看出,ConcurrentHashMap中不允许存在空值,这个是跟HashMap的区别之一 //通过这个机制,我们可以通过get方法获取一个key,如果抛出异常,说明这个key不存在
int hash = spread(key.hashCode());//这个方法就相当于基于计算hash值。
int binCount = 0;//这个是记录这个桶的元素个数,目的是用它来判断是否需要转换红黑树,
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
        //情况一:如果数组为空或者长度为0,进行初始化工作
if (tab == null || (n = tab.length) == 0)
tab = initTable();
        //情况二:如果获取的位置的节点为空,说明是首节点插入情况,也就是该桶位置没有元素,利用cas将元素添加。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,//cas加自旋(和外侧的for构成自旋循环),保证元素添加安全
new Node<K,V>(hash, key, value, null)))//如果加成功了,那么就break,否则再经过for的死循环进行判断
break; // no lock when adding to empty bin
}
        //情况三:如果hash计算得到的桶的位置元素的hash值为moved,也就是-1,证明正在扩容,那么就协助扩容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
         //hash计算的桶位置元素不为空,且当前没有处于扩容操作,进行元素添加
        //情况四:这个桶有元素,则执行插入操作,有两种可能,一是这个桶对应的链表没有相同的key,那么久再链表尾插入node节点,而是有相同的key,那么久替换其value。
else {
V oldVal = null;
synchronized (f) { //对当前桶进行加锁,保证线程安全,执行元素添加操作 //将桶位置的元素锁住,那么在该桶位中的链表或者红黑树进行添加元素的话,就是安全的,只有这个线程拿住了这个锁
if (tabAt(tab, i) == f) {//因为添加元素之后可能链表已经变成红黑树了,那么这个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) {//尾插法,如果e的下一个不是null,那么循环会让pred变成e,直到最后节点,此时e的下一个为null的话
                            //那么也就是pred下一个为null,那么插入到pred下一个即可。
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) {

if (binCount >= TREEIFY_THRESHOLD)//链表长度大于/等于8,有可能将链表转成红黑树,因为在treeifyBin(tab, i);方法中还有一个判断数组长度是否小于64的判断,如果小于64,就不会
                //树化。只是数组扩容。
treeifyBin(tab, i);
if (oldVal != null) //如果是重复键,直接将旧值返回
return oldVal;
break;
}
}
}
addCount(1L, binCount);//添加的是新元素,维护集合长度,并判断是否要进行扩容操作
return null;
}

总结:并发map,jdk1.8的情况下,底层跟hashmap一样,也是数组加链表加红黑树。

ConcurrentHashMap源码解读一的更多相关文章

  1. HashTable、HashMap与ConCurrentHashMap源码解读

    HashMap 的数据结构 ​ hashMap 初始的数据结构如下图所示,内部维护一个数组,然后数组上维护一个单链表,有个形象的比喻就是想挂钩一样,数组脚标一样的,一个一个的节点往下挂. ​ 我们可以 ...

  2. ConcurrentHashMap源码解读二

    接下来就讲解put里面的三个方法,分别是 1.数组初始化方法initTable() 2.线程协助扩容方法helpTransfer() 3.计数方法addCount() 首先是数组初始化,再将源码之前, ...

  3. 深入理解JAVA集合系列二:ConcurrentHashMap源码解读

    HashMap和Hashtable的区别 在正式开始这篇文章的主题之前,我们先来比较下HashMap和Hashtable之间的差异点: 1.Hashtable是线程安全的,它对外提供的所有方法都是都使 ...

  4. JUC回顾之-ConcurrentHashMap源码解读及原理理解

    ConcurrentHashMap结构图如下: ConcurrentHashMap实现类图如下: segment的结构图如下: package concurrentMy.juc_collections ...

  5. ConcurrentHashMap源码解读三

    今天首先讲解helpTransfer方法 final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) ...

  6. JDK容器类Map源码解读

    java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...

  7. Spring源码-循环依赖源码解读

    Spring源码-循环依赖源码解读 笔者最近无论是看书还是从网上找资料,都没发现对Spring源码是怎么解决循环依赖这一问题的详解,大家都是解释了Spring解决循环依赖的想法(有的解释也不准确,在& ...

  8. 并发-ConcurrentHashMap源码分析

    ConcurrentHashMap 参考: http://www.cnblogs.com/chengxiao/p/6842045.html https://my.oschina.net/hosee/b ...

  9. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

随机推荐

  1. python 常用库收集

    读者您好.今天我将介绍20个属于我常用工具的Python库,我相信你看完之后也会觉得离不开它们.他们是: Requests.Kenneth Reitz写的最富盛名的http库.每个Python程序员都 ...

  2. Jmeter(三十八) - 从入门到精通进阶篇 - 命令行运行JMeter详解(详解教程)

    1.简介 前边一篇文章介绍了如何生成测试报告,细心地小伙伴或者同学们可以看到宏哥启动Jmeter生成测试报告不是在gui页面操作的,而是在gui页面设置好保存以后,用命令行来生成测试报告的.这一篇宏哥 ...

  3. 归并排序(JAVA语言)

    public class merge { public static void main(String[] args) { // TODO Auto-generated method stub int ...

  4. Activity类组成分析(一)Instrumentation

    目录 前言 解剖 继承关系 重要成员 Instrumentation 总结 前言 要了解清楚StartActivity的过程,Activity对象实例的构造过程是重要组成部分:而要弄清楚Activit ...

  5. vue+element+oss实现前端分片上传和断点续传

    纯前端实现: 切片上传 断点续传 .断点续传需要在切上上传的基础上实现 前端之前上传OSS,无需后端提供接口.先上完整代码,直接复制,将new OSS里的参数修改成自己公司OSS相关信息后可用,如遇问 ...

  6. Android 之 SimpleAdapter 学习笔记

    •SimpleAdapter简介 simpleAdapter 的扩展性最好,可以定义各种各样的布局出来: 可以放上ImageView(图片),还可以放上Button(按钮),CheckBox(复选框) ...

  7. (十六)Struts2的标签库

    一.简介 Struts2的标签库使用OGNL为基础,大大简化了数据的输出,也提供了大量标签来生成页面效果,功能非常强大. 在早期的web应用开发中,jsp页面主要使用jsp脚本来控制输出.jsp页面嵌 ...

  8. OAuth2 Token 一定要放在请求头中吗?

    Token 一定要放在请求头中吗? 答案肯定是否定的,本文将从源码的角度来分享一下 spring security oauth2 的解析过程,及其扩展点的应用场景. Token 解析过程说明 当我们使 ...

  9. IDEA main 函数的快捷键

    1.main()快捷键:psvm 当输入ps的时候编辑器就会有如下提示,可见比eclipse要很方便 2.System.out.println()快捷键:sout

  10. 介绍一款能取代 Scrapy 的 Python 爬虫框架 - feapder

    1. 前言 大家好,我是安果! 众所周知,Python 最流行的爬虫框架是 Scrapy,它主要用于爬取网站结构性数据 今天推荐一款更加简单.轻量级,且功能强大的爬虫框架:feapder 项目地址: ...