点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人。

文章不定期同步公众号,还有各种一线大厂面试原题、我的学习系列笔记。

jdk1.7和jdk1.8中ConcurrentHashMap的区别?

底层数据结构的区别
  • jdk1.7中的ConcurrenHashMap的底层结构=Segment数组+HashEntry数组来实现,put过程使用了Synchronized,结构如下:



    由上,jdk1.7中ConcurretHashMap=Segment类数组,每个Segment元素=HashEntry类数组=类似一个hashMap结构,每个HashEntry元素=链表;当多线程并发时,锁住的是单个Segment元素(Segment继承ReentrantLock,此处jdk1.7使用的是ReentrantLock的非公平锁),但即使是单个Segment元素,里面也含有一整个HashEntry数组(类似一个HashMap),所以锁住的是一整个HashEntry数组,故并发度也没有那么高
  • jdk1.8中的ConcurrenHashMap的底层结构=Node数组+链表+红黑树,put过程使用了Synchronized+CAS(调用Unsafe类的cas方法),结构如下:

由上,jdk1.8中ConcurretHashMap类似于HashMap,它们都是数组+链表+红黑树,所有的操作都一样,唯一区别是ConcurretHashMap在具体某个桶的位置插入元素时,该位置的链表或红黑树会被同步访问【Synchronized(某桶的头节点)】,这样粒度比jdk1.7的更小了,锁住的是某个链表(或红黑树)

put方法的区别
  • jdk1.7中的put需定位两次:先定位要插入的元素在segment数组的下标,然后加锁去根据这个下标定位HashEntry数组的下标【key的hash值&HashEntry数组长度】,没有获得锁的线程做一些准备工作:

    (1)提前找好HashEntry中桶的位置;

    (2)遍历该桶有没有相同的key

    进行(1)、(2)的同时不断自旋获取锁,超过64次还没获取到锁就挂起该线程

  • jdk1.8中的put只需定位一次:

-->假如Node型table数组为空则初始化initTable()

-->table初始化完成,定位要插入元素所在的table[i]的位置,进一步判断table[i]为空则执行cas插入;

-->table[i]不为空且table[i].hash=-1,代表ConcurrentHashMap正在扩容,则加入扩容;

-->table[i].hash不为-1则直接插入,若是链表则插入到链表尾,若是红黑树则插入到红黑树

短时间内如何将大量数据高效地插入到ConcucurentHashMap?

以jdk1.8为例,影响concurrentHashMap插入元素的效率主要有两点:插入时频繁地扩容+插入时并发地访问:

(1)解决‘插入时频繁地扩容’:需选择合适的初始化容量和扩容因子

(2)解决‘插入时并发地访问’:插入节点时会对Node链表头节点加锁,然而锁也有'偏向锁...重量级锁',只要控制锁不发生升级,尽量保持在偏向锁状态,这样每个桶就只有一个线程访问,不会发生高并发从而提高插入效率。如何保持每个头结点加锁之后都是'偏向锁'状态呢?利用concurrentHashMap的spread()方法求key的hash值(预处理数据),将存在哈希冲突的key都集中地插入某个桶(可能会有多个桶-多个哈希冲突),因为每个桶都用单线程去put,从而没有其他线程去竞争同一个桶的锁,锁就一直为‘偏向锁’。

jdk1.8的put源码

  1. final V putVal(K key, V value, boolean onlyIfAbsent) { //onlyIfAbsent默认传入false
  2. if (key == null || value == null) throw new NullPointerException();
  3. int hash = spread(key.hashCode()); //求key的hash值=预处理hashcode:便于将存在哈希冲突的key集中地插入某个桶
  4. int binCount = 0;
  5. for (Node<K,V>[] tab = table;;) {
  6. Node<K,V> f; int n, i, fh;
  7. if (tab == null || (n = tab.length) == 0) //如果table为空则初始化数组
  8. tab = initTable();
  9. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果table非空则检查插入位置table[i]处的位置是否为空,若是则用cas插入Node节点
  10. if (casTabAt(tab, i, null,
  11. new Node<K,V>(hash, key, value, null)))
  12. break; // no lock when adding to empty bin
  13. }
  14. else if ((fh = f.hash) == MOVED) //如果插入位置table[i]的头结点f的【hash值=fh】为-1则代表concurrentHashMap正在扩容,则加入扩容
  15. tab = helpTransfer(tab, f);
  16. else {
  17. V oldVal = null;
  18. synchronized (f) { //以上情况都不是,则插入到链表或红黑树,该步直接把头结点synchronized加锁,这样锁住的就是单个链表或单个红黑树
  19. if (tabAt(tab, i) == f) {
  20. if (fh >= 0) {//判断该桶位置处是链表还是红黑树:头结点的hash值>=,代表该处是链表
  21. binCount = 1;
  22. for (Node<K,V> e = f;; ++binCount) { //binCount用于计算该链表的结点树,后面会用到判断binCount>8则转为红黑树
  23. K ek;
  24. if (e.hash == hash &&
  25. ((ek = e.key) == key ||
  26. (ek != null && key.equals(ek)))) {//key相同
  27. oldVal = e.val;
  28. if (!onlyIfAbsent) //onlyIfAbsent默认为false
  29. e.val = value;
  30. break;
  31. }
  32. Node<K,V> pred = e;
  33. if ((e = e.next) == null) { //插入到链表尾
  34. pred.next = new Node<K,V>(hash, key,
  35. value, null);
  36. break;
  37. }
  38. }
  39. }
  40. else if (f instanceof TreeBin) {
  41. Node<K,V> p;
  42. binCount = 2;
  43. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
  44. value)) != null) {
  45. oldVal = p.val;
  46. if (!onlyIfAbsent)
  47. p.val = value;
  48. }
  49. }
  50. }
  51. }
  52. if (binCount != 0) {
  53. if (binCount >= TREEIFY_THRESHOLD) //TREEIFY_THRESHOLD默认8,判断链表结点数>=8则转为红黑树
  54. treeifyBin(tab, i);
  55. if (oldVal != null)
  56. return oldVal;
  57. break;
  58. }
  59. }
  60. }
  61. addCount(1L, binCount);
  62. return null;
  63. }
  64. //Node型数组table为空时,初始化
  65. private final Node<K,V>[] initTable() {
  66. Node<K,V>[] tab; int sc;
  67. while ((tab = table) == null || tab.length == 0) {
  68. //sizeCtl默认为0,可有三种取值:-1代表table正被其他线程初始化;0代表table等待初始化;大于0代表table初始化完成
  69. if ((sc = sizeCtl) < 0)
  70. Thread.yield(); // -1代表table正被其他线程初始化,本线程让出时间片进入就绪状态
  71. else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //SIZECTL默认为0:【this在SIZECTL偏移量处的值=默认0】与sc相比,若相等则将this在SIZECTL偏移量处的值置为-1,代表table正在被初始化【疑问:sizeCtl是不同于SIZECTL的,SIZECTL=-1但sizeCtl没有置-1,所以上面Thread.yield()应该永远执行不到?】
  72. try {
  73. if ((tab = table) == null || tab.length == 0) {
  74. int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//sc默认为0,DEFAULT_CAPACITY=默认16
  75. @SuppressWarnings("unchecked")
  76. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
  77. table = tab = nt;
  78. sc = n - (n >>> 2); //n=16,n >>> 2即n/4,所以sc=n-(n/4)=3n/4=0.75n=12
  79. }
  80. } finally {
  81. sizeCtl = sc; //把sizeCtl置为12>0,代表table初始化完成
  82. }
  83. break;
  84. }
  85. }
  86. return tab;
  87. }

OK,如果文章哪里有错误或不足,欢迎各位留言。

创作不易,各位的「三连」是二少创作的最大动力!我们下期见!

集合篇-ConcurrentHashMap的更多相关文章

  1. JUC源码分析-集合篇(一)ConcurrentHashMap

    JUC源码分析-集合篇(一)ConcurrentHashMap 1. 概述 <HashMap 源码详细分析(JDK1.8)>:https://segmentfault.com/a/1190 ...

  2. JUC源码分析-集合篇:并发类容器介绍

    JUC源码分析-集合篇:并发类容器介绍 同步类容器是 线程安全 的,如 Vector.HashTable 等容器的同步功能都是由 Collections.synchronizedMap 等工厂方法去创 ...

  3. JUC源码分析-集合篇(四)CopyOnWriteArrayList

    JUC源码分析-集合篇(四)CopyOnWriteArrayList Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略.其基本思路是,从一开始大家都在共享同一个内容,当某个人想 ...

  4. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  5. 【JAVA秒会技术之秒杀面试官】秒杀Java面试官——集合篇(一)

    [JAVA秒会技术之秒杀面试官]秒杀Java面试官——集合篇(一) [JAVA秒会技术之秒杀面试官]JavaEE常见面试题(三) http://blog.csdn.net/qq296398300/ar ...

  6. JUC源码分析-集合篇(十)LinkedTransferQueue

    JUC源码分析-集合篇(十)LinkedTransferQueue LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素 ...

  7. JUC源码分析-集合篇(九)SynchronousQueue

    JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...

  8. JUC源码分析-集合篇(八)DelayQueue

    JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...

  9. JUC源码分析-集合篇(七)PriorityBlockingQueue

    JUC源码分析-集合篇(七)PriorityBlockingQueue PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现. P ...

随机推荐

  1. MindSpore尝鲜之Vmap功能

    技术背景 Vmap是一种在python里面经常提到的向量化运算的功能,比如之前大家常用的就是numba和jax中的向量化运算的接口.虽然numpy中也使用到了向量化的运算,比如计算两个numpy数组的 ...

  2. 请写出你最常见到的5个runtime exception?

    所谓系统异常,就是-..,它们都是RuntimeException的子类,在jdk doc中查RuntimeException类,就可以看到其所有的子类列表,也就是看到了所有的系统异常.我比较有印象的 ...

  3. 服务端处理 Watcher 实现 ?

    1.服务端接收 Watcher 并存储 接收到客户端请求,处理请求判断是否需要注册 Watcher,需要的话将数据节点 的节点路径和 ServerCnxn(ServerCnxn 代表一个客户端和服务端 ...

  4. 如何通过sql语句完成分页?

    oracle select rownum,bookId from [rownum是伪列名,bookId是列名] (select rownum row_id,bookId from xiaoWJ_boo ...

  5. kafka生产者网络层总结

    1 层次结构 负责进行网络IO请求的是NetworkClient,主要层次结构如下 ClusterConnectionStates报存了每个节点的状态,以node为key,以node的状态为value ...

  6. 会话缓存(Session Cache)?

    最常用的一种使用 Redis 的情景是会话缓存(session cache).用 Redis 缓存会 话比其他存储(如 Memcached)的优势在于:Redis 提供持久化.当维护一个不 是严格要求 ...

  7. Djnago中缓存配置(redis配置案例)

    Django中提供了6种缓存方式: 开发调试 内存 文件 数据库 Memcache缓存(python-memcached模块) Memcache缓存(pylibmc模块) 配置文件 # 内存 CACH ...

  8. MyBatis Plus 2.3 个人笔记-03-Active Record

    AR 语法糖  是一种领域模型模式,特点就是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一条记录 实现AR [在代码生成器中可以添加配置] import com.baomidou ...

  9. IdentityServer4系列 | 混合模式

    一.前言 在上一篇关于授权码模式中, 已经介绍了关于授权码的基本内容,认识到这是一个拥有更为安全的机制,但这个仍然存在局限,虽然在文中我们说到通过后端的方式去获取token,这种由web服务器和授权服 ...

  10. Javascript--function的name属性

    1.非标准的name属性 function sayHi(){  console.log("Hi");} console.log(sayHi.name);