ps.本文所有源码都是基于jdk1.6
首先说明一点,ConcurrentHashMap并不是可以完全替换Hashtable的,因为ConcurrentHashMap的get、clear函数是弱一致的(后面会说到),而Hashtable是强一致的。有作者是这么解释的:我们将“一致性强度”和“扩展性”之间的对比交给用户来权衡,所以大多数集合都提供了synchronized和concurrent两个版本。不过真正需要“强一致性”的场景可能非常少,我们大多应用中ConcurrentHashMap是满足的。

ConcurrentHashMap的数据结构

不得不说,ConcurrentHashMap设计的相当巧妙,它有多个段,每个段下面都是一个Hashtable(相似),所以每个段上都有一把锁(分布式锁),各个段之间的锁互不影响,可以实现并发操作。话不多说,上代码。

final Segment<K,V>[] segments;
  • 1

可以看到ConcurrentHashMap实际上就是一个Segment数组,那么Segment是什么呢?

static final class Segment<K,V> extends ReentrantLock implements Serializable {...transient volatile HashEntry<K,V>[] table; //发现Segment实际上是HashEntry数组...
}
  • 1
  • 2
  • 3
  • 4
  • 5

那HashEntry又是什么呢?

static final class HashEntry<K,V> {final K key;final int hash;volatile V value;final HashEntry<K,V> next;...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

HashEntry是一个单链表
所以ConcurrentHashMap的数据结构如下图:

这里每一个segment所指向的数据结构,其实就是一个Hashtable,所以说每一个segment都有一把独立的锁,来保证当访问不同的segment时互不影响。

ConcurrentHashMap的基础方法

基础方法分为这么几种:
1、段内加锁的:put,putIfAbsent,remove,replace等
2、不加锁的:get,containsKey等
3、整个数据结构加锁的:size,containsValue等

构造函数

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();  //参数合法性校验if (concurrencyLevel > MAX_SEGMENTS)concurrencyLevel = MAX_SEGMENTS; //比如我输入的concurrencyLevel=12,那么sshift = 4,ssize =16,所以sshift是意思就是1左移了几次比concurrencyLevel大,ssize就是那个大于等于concurrencyLevel的最小2的幂次方的数int sshift = 0;int ssize = 1;while (ssize < concurrencyLevel) {++sshift;ssize <<= 1;  //ssize = ssize << 1 , ssize = ssize * 2}segmentShift = 32 - sshift;segmentMask = ssize - 1;   //segmentMask的二进制是一个全是1的数 this.segments = Segment.newArray(ssize); //segment个数是ssize,也就是上图黄色方块数,默认16个if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;int c = initialCapacity / ssize;if (c * ssize < initialCapacity)++c;int cap = 1;while (cap < c)cap <<= 1;   for (int i = 0; i < this.segments.length; ++i)this.segments[i] = new Segment<K,V>(cap, loadFactor);//上图浅绿色块的个数是cap,是一个2的幂次方的数,默认是1,也就是每个segment下都构造了cap大小的table数组
}
Segment(int initialCapacity, float lf) {loadFactor = lf;setTable(HashEntry.<K,V>newArray(initialCapacity));//构造了一个initialCapacity大小的table
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

put函数

public V put(K key, V value) {if (value == null)throw new NullPointerException(); //明确指定value不能为nullint hash = hash(key.hashCode());return segmentFor(hash).put(key, hash, value, false); //segmentFor如下,定位segment下标,然后再put
}final Segment<K,V> segmentFor(int hash) {         //寻找segment的下标return segments[(hash >>> segmentShift) & segmentMask];     //前面说了segmentMask是一个2进制全是1的数,那么&segmentMask就保证了下标小于等于segmentMask,与HashMap的寻下标相似。
}//真正的put操作
V put(K key, int hash, V value, boolean onlyIfAbsent) {lock();          //先加锁,可以看到,put操作是在segment里面加锁的,也就是每个segment都可以加一把锁try {int c = count;if (c++ > threshold) // ensure capacityrehash();   //判断容量,如果不够了就扩容HashEntry<K,V>[] tab = table;      //将table赋给一个局部变量tab,这是因为table是volatile变量,读写volatile变量的开销很大,编译器也不能对volatile变量的读写做任何优化,直接多次访问非volatile实例变量没有多大影响,编译器会做相应优化。int index = hash & (tab.length - 1);   //寻找table的下标HashEntry<K,V> first = tab[index];HashEntry<K,V> e = first;while (e != null && (e.hash != hash || !key.equals(e.key)))e = e.next;  //遍历单链表,找到key相同的为止,如果没找到,e指向链表尾V oldValue;if (e != null) { //如果有相同的key,那么直接替换oldValue = e.value;if (!onlyIfAbsent)e.value = value;}else {  //否则在链表表头插入新的结点oldValue = null;++modCount;tab[index] = new HashEntry<K,V>(key, hash, first, value);count = c; // write-volatile}return oldValue;} finally {unlock();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

该方法也是在持有段锁的情况下执行的,首先判断是否需要rehash,需要就先rehash,扩容都是针对单个段的,也就是单个段的数据数量大于设定的量的时候会触发扩容。接着是找是否存在同样一个key的结点,如果存在就直接替换这个结点的值。否则创建一个新的结点并添加到hash链的头部,这时一定要修改modCount和count的值,同样修改count的值一定要放在最后一步。put方法调用了rehash方法,reash方法实现得也很精巧,主要利用了table的大小为2^n,和HashMap的扩容基本一样,这里就不介绍了。

还有一个叫putIfAbsent(K key, V value)的函数,这个函数的实现和put几乎一模一样,作用是,如果map中不存在这个key,那么插入这个数据,如果存在这个key,那么不覆盖原来的value,也就是不插入了。

remove函数

>remove操作也交给了段内的remove,如下:
V remove(Object key, int hash, Object value) {lock();  //段内先获得一把锁try {int c = count - 1;HashEntry<K,V>[] tab = table;int index = hash & (tab.length - 1);HashEntry<K,V> first = tab[index];HashEntry<K,V> e = first;while (e != null && (e.hash != hash || !key.equals(e.key)))e = e.next;V oldValue = null;if (e != null) {  //如果找到该keyV v = e.value;if (value == null || value.equals(v)) {oldValue = v;// All entries following removed node can stay// in list, but all preceding ones need to be// cloned.++modCount;HashEntry<K,V> newFirst = e.next; //newFirst此时为要删除结点的nextfor (HashEntry<K,V> p = first; p != e; p = p.next)newFirst = new HashEntry<K,V>(p.key,p.hash,newFirst, p.value);//从头遍历链表将要删除结点的前面所有结点复制一份插入到newFirst之前,如下图tab[index] = newFirst;count = c; // write-volatile}}return oldValue;} finally {unlock();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

为什么用这么方式删除呢,细心的同学会发现上面定义的HashEntry的key和next都是final类型的,所以不能改变next的指向,所以又复制了一份指向删除的结点的next。

get函数

public V get(Object key) {int hash = hash(key.hashCode());   //双hash,和HashMap一样,也是为了更好的离散化return segmentFor(hash).get(key, hash);  //先寻找segment的下标,然后再get
}final Segment<K,V> segmentFor(int hash) {      //寻找segment的下标return segments[(hash >>> segmentShift) & segmentMask];  //前面说了segmentMask是一个2进制全是1的数,那么&segmentMask就保证了下标小于等于segmentMask,与HashMap的寻下标相似。
}V get(Object key, int hash) {// count是一个volatile变量,count非常巧妙,每次put和remove之后的最后一步都要更新count,就是为了get的时候不让编译器对代码进行重排序,来保证if (count != 0) { HashEntry<K,V> e = getFirst(hash);   //寻找table的下标,也就是链表的表头while (e != null) {if (e.hash == hash && key.equals(e.key)) {V v = e.value;if (v != null)return v;return readValueUnderLock(e); // recheck  加锁读,这个加锁读不用重新计算位置,而是直接拿e的值}e = e.next;}}return null;
}HashEntry<K,V> getFirst(int hash) {HashEntry<K,V>[] tab = table;return tab[hash & (tab.length - 1)];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

读完上面代码我有一个疑问,就是如果找到的key对应的value是null的话,加锁再读一次,既然上面put操作不允许value是null,那读到的value为什么会有null的情况呢?我们分析一下这种情况,就是put操作和get操作同时进行的时候,可以分为两种情况:
1、put的key已经存在,由于value不是final的,可以直接更新,且value是volatile的,所以修改会立马对get线程可见,而不用等到put方法结束。
2、put的key不存在,那么将在链表表头插入一个数据,那么将new HashEntry赋值给tab[index]是否能立刻对执行get的线程可见呢,我们知道每次put完之后都要更新一个count变量(写),而每次get数据的时候,再最一开始都要读一个count变量(读),而且发现这个count是volatile的,而对同一个volatile变量,有volatile写 happens-before volatile读,所以如果写发生在读之前,那么new HashEntry赋值给tab[index]是对get线程可见的,但是如果写没有发生在读之前,就无法保证new HashEntry赋值给tab[index]要先于get函数的getFirst(hash),也就是说,如果某个Segment实例中的put将一个Entry加入到了table中,在未执行count赋值操作之前有另一个线程执行了同一个Segment实例中的get,来获取这个刚加入的Entry中的value,那么是有可能取不到的,这也就是get的弱一致性。但是什么时候会找到key但是读到的value是null呢,仔细看下put操作的语句:tab[index] = new HashEntry(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,举个例子就是这条语句有可能先执行对key赋值,再执行对tab[index]的赋值,最后对value赋值,如果在对tab和key都赋值但是对value还没赋值的情况下的get就是一个空值。
详细可以看看这篇文章:http://ifeve.com/concurrenthashmap-weakly-consistent/
这也就是说无锁的get操作是一个弱一致性的操作。

clear函数

public void clear() {for (int i = 0; i < segments.length; ++i)    //循环clear掉每个段中的内容segments[i].clear();
}
void clear() {if (count != 0) {lock();    //段内加锁try {HashEntry<K,V>[] tab = table;for (int i = 0; i < tab.length ; i++)tab[i] = null;++modCount;count = 0; // write-volatile} finally {unlock();}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

因为没有全局的锁,在清除完一个segments之后,正在清理下一个segments的时候,已经清理segments可能又被加入了数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此,clear方法是弱一致的。

size函数

public int size() {final Segment<K,V>[] segments = this.segments;long sum = 0;long check = 0;int[] mc = new int[segments.length];// Try a few times to get accurate count. On failure due to// continuous async changes in table, resort to locking.for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {check = 0;sum = 0;int mcsum = 0;for (int i = 0; i < segments.length; ++i) {sum += segments[i].count;           //循环相加每个段内数据的个数mcsum += mc[i] = segments[i].modCount;        //循环相加每个段内的modCount}if (mcsum != 0) {                   //如果是0,代表根本没有过数据更改,也就是size是0for (int i = 0; i < segments.length; ++i) {check += segments[i].count;    //再次循环相加每个段内数据的个数,这里为什么会再算一次呢,后面会说if (mc[i] != segments[i].modCount) {    check = -1; // force retry  //如果modCount和之前统计的不一致了,check直接赋值-1,重新再来break;}}}if (check == sum)break;}if (check != sum) { // Resort to locking all segmentssum = 0;for (int i = 0; i < segments.length; ++i)  //循环获取所有segment的锁segments[i].lock();for (int i = 0; i < segments.length; ++i)  //在持有所有段的锁的时候进行count的相加sum += segments[i].count;for (int i = 0; i < segments.length; ++i)  //循环释放所有段的锁segments[i].unlock();}if (sum > Integer.MAX_VALUE)            //returnreturn Integer.MAX_VALUE;elsereturn (int)sum;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

如果我们要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。Segment里的全局变量count是一个volatile变量,那么在多线程场景下,我们是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时可以获取每个Segment的count的最新值,但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了。所以最安全的做法,是在统计size的时候把所有Segment的put,remove和clean方法全部锁住,但是这种做法显然非常低效,因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clear方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。size()的实现还有一点需要注意,必须要先segments[i].count,才能segments[i].modCount,这是因为segment[i].count是对volatile变量的访问,接下来segments[i].modCount才能得到几乎最新的值,这里和get方法的方式是一样的,也是一个volatile写 happens-before volatile读的问题。

上面18行代码,抛出了一个问题,就是为什么会再算一遍,上面说只需要比较modCount不变不就可以了么?但是仔细分析,就会发现,在13行14行代码那里,计算完count之后,计算modCount之前有可能count的值又变了,那么18行的代码主要是解决这个问题。

containsValue函数

containsKey函数比较简单,也是不加锁的简易get,下面说一下containsValue有一个有意思的地方

public boolean containsValue(Object value) {if (value == null)throw new NullPointerException();// See explanation of modCount use abovefinal Segment<K,V>[] segments = this.segments;int[] mc = new int[segments.length];// Try a few times without lockingfor (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {int sum = 0;int mcsum = 0;for (int i = 0; i < segments.length; ++i) {int c = segments[i].count;    //注意这行,声明了一个c变量,并且赋值了,但是下面却完全没有用到。mcsum += mc[i] = segments[i].modCount;if (segments[i].containsValue(value))return true;}boolean cleanSweep = true;    //默认结构没变if (mcsum != 0) {for (int i = 0; i < segments.length; ++i) {int c = segments[i].count;if (mc[i] != segments[i].modCount) {  //如果结构变了,cleanSweep = falsecleanSweep = false;break;}}}if (cleanSweep)    //如果没变,直接返回falsereturn false;}// Resort to locking all segmentsfor (int i = 0; i < segments.length; ++i)segments[i].lock();boolean found = false;try {for (int i = 0; i < segments.length; ++i) {if (segments[i].containsValue(value)) {found = true;break;}}} finally {for (int i = 0; i < segments.length; ++i)segments[i].unlock();}return found;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

注意上面代码15行处,里面有语句int c = segments[i].count; 但是c却从来没有被使用过,即使如此,编译器也不能做优化将这条语句去掉,因为存在对volatile变量count的读取,这条语句存在的唯一目的就是保证segments[i].modCount读取到几乎最新的值,上面有说道这个问题。

本文章参考文章:
1、http://ifeve.com/concurrenthashmap-weakly-consistent/
2、http://www.iteye.com/topic/344876

史上最详细的ConcurrentHashMap详解--源码分析相关推荐

  1. SpringBoot入门详解源码分析

    注:文章内容来自于黑马的虎哥,个人感觉写的挺好的,所以只是做了简单整理,我只是文章的搬运工! # 0.学习目标 - 了解SpringBoot的作用 - 掌握java配置的方式 - 了解SpringBo ...

  2. Oracle 史上最详细的分区表详解

    一.分区表的概念 分区表:当表中的数据量不断增大,查询数据的速度就会变慢,应用程序的性能就会下降,这时就应该考虑对表进行分区.表进行分区后,逻辑上表仍然是一张完整的表,只是将表中的数据在物理上存放到多 ...

  3. 史上最小白之Transformer详解

    1.前言 博客分为上下两篇,您现在阅读的是下篇史上最小白之Transformer详解,在阅读该篇博客之前最好你能够先明白Encoder-Decoder,Attention机制,self-Attenti ...

  4. 史上最简单MySQL教程详解(进阶篇)之存储过程(一)

    史上最简单MySQL教程详解(进阶篇)之存储过程(一) 史上最简单MySQL教程详解(进阶篇)之存储过程(一) 什么是存储过程 存储过程的作用 如何使用存储过程 创建存储过程 DELIMITER改变分 ...

  5. 史上最简单MySQL教程详解(进阶篇)之存储引擎介绍及默认引擎设置

    什么是存储引擎? MySQL存储引擎种类 MyISAM 引擎 InnoDB引擎 存储引擎操作 查看存储引擎 存储引擎的变更 修改默认引擎 什么是存储引擎? 与其他数据库例如Oracle 和SQL Se ...

  6. 史上最简单MySQL教程详解(进阶篇)之索引及失效场合总结

    史上最简单MySQL教程详解(进阶篇)之索引及其失效场合总结 什么是索引及其作用 索引的种类 各存储引擎对于索引的支持 简单介绍索引的实现 索引的设置与分析 普通索引 唯一索引(Unique Inde ...

  7. 史上最简单MySQL教程详解(进阶篇)之视图

    史上最简单MySQL教程详解(进阶篇)之视图 为什么要用视图 视图的本质 视图的作用 如何使用视图 创建视图 修改视图 删除视图 查看视图 使用视图检索 变更视图数据 WITH CHECK OPTIO ...

  8. 史上最小白之BM25详解与实现

    史上最小白之BM25详解与实现 原理 BM25算法是一种计算句子与文档相关性的算法,它的原理十分简单:将输入的句子sentence进行分词,然后分别计算句子中每个词word与文档doc的相关度,然后进 ...

  9. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

最新文章

  1. 8月12笔记-安卓文件扫描
  2. Navicat和DBeaver的查询快捷键
  3. 踩坑记录--VScode编辑不同文件没有及时crtl+s,导致文件修改无效
  4. 基础算法 —— 调度问题 —— 流水调度问题
  5. 利用Kubernetes搭建便携式开发环境之MySQL和Redis
  6. 触发器_触发器第三弹
  7. python从键盘获取列表,python – 从5行中的列表列表中获取所有元素
  8. 新主播如何在直播行业混得好
  9. python调用gpu amd_TensorFlow通过AMD GPU加速(ROCm/Ubuntu 18.04)
  10. 使用DiskGenius拓展系统盘C盘的容量
  11. C语言中mac是什么意思,CMAC是什么意思
  12. face_recognition人脸识别
  13. 【SCIENTIFIC AMERICAN December 2019】【NUCLEAR PHYSICS】Proton Size Puzzle
  14. 来给罗永浩和王自如打个分
  15. 台式计算机用u盘给电脑安装系统,台式机用u盘装系统,教您用u盘装系统给台式机...
  16. PDF编辑方法,怎么把PDF其中一页删除
  17. CVE PoC的精选列表(二)
  18. 元宵快乐!!!! 自己做的贺卡。。 :)
  19. STM32软件模拟IIC---读写驱动AT24Cxx
  20. eCharts好看的 响应式 圆环饼图 及文字 附vue源码代码

热门文章

  1. BUCK降压电路空载问题
  2. 实现isPrime()函数,参数为整数,要有异常处理。如果整数是质数,返回True,否则返回False
  3. LCD制作工艺及相关设备资料配方大全(转)
  4. ImageJ macro 批量vsi转tif
  5. 和AWS云游四海@姓“诸葛”,名“io”
  6. Windows cmd关机重启命令
  7. java设计模式简化版之六大原则
  8. 【Unity3D】Animator动画播放时卡死在第一帧BUG系列
  9. 基于有源钳位三电平的有源电力滤波器(ANPC-APF)MATLAB仿真,包括自建的DSOGI锁相模块和PQ谐波检测模块。 可简单解释。
  10. 基于贝叶斯生存分析的《冰与火之歌》人物死亡率分析