ConcurrentHashMap底层实现原理((JDK1.71.8))

更新时间:2023-05-12 08:47:22 阅读: 评论:0

ConcurrentHashMap底层实现原理((JDK1.71.8))
⽬录
前⾔
我们都知道HashMap在多线程情况下,在put的时候,插⼊的元素超过了容量(由负载因⼦决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进⾏put操作,如果hash 值相同,可能出现同时在同⼀数组下⽤链表表⽰,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
我们来了解另⼀个键值存储集合HashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争⼀把锁,在多线程的环境下,它是安全的,但是⽆疑是效率低下的。
其实HashTable有很多的优化空间,锁住整个table这么粗暴的⽅法可以变相的柔和点,⽐如在多线程的环境下,对不同的数据集进⾏操作时其实根本就不需要去竞争⼀个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利⽤多个锁来控制多个⼩的table
ConcurrentHashMap (JDK1.7的实现)
在JDK1.7版本中,ConcurrentHashMap的数据结构是由⼀个Segment数组和多个HashEntry组成,如下图所⽰:
Segment数组的意义就是将⼀个⼤的table分割成多个⼩的table来进⾏加锁,也就是上⾯的提到的锁分离技术,⽽每⼀个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构⼀样
初始化
ConcurrentHashMap的初始化是会通过位与运算来初始化Segment的⼤⼩,⽤ssize来表⽰,如下所⽰
int size =1;
while(size < concurrencyLevel) {
++a;
size <<=1;
}
如上所⽰,因为ssize⽤位于运算来计算( ssize <<=1 ),所以Segment的⼤⼩取值都是以2的N次⽅,⽆关concurrencyLevel的取值,当然concurrencyLevel最⼤只能⽤16位的⼆进制来表⽰,即65536,换句话说,Segment的⼤⼩最多65536个,没有指定concurrencyLevel元素初始化,Segment的⼤⼩ssize默认为16
每⼀个Segment元素下的HashEntry的初始化也是按照位于运算来计算,⽤cap来表⽰,如下所⽰
int cap =1;
while(cap < c)
cap <<=1;
如上所⽰,HashEntry⼤⼩的计算也是2的N次⽅(cap <<=1), cap的初始值为1,所以HashEntry最⼩的容量为2
put操作
对于ConcurrentHashMap的数据插⼊,这⾥要进⾏两次Hash去定位数据的存储位置
static class  Segment<K,V> extends  ReentrantLock implements  Serializable {
}
从上Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执⾏put操作时,会进⾏第⼀次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进⾏赋值,然后进⾏第⼆次hash操作,找到相应的HashEntry的位置,这⾥会利⽤继承过来的锁的特性,在将数据插⼊指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的
tryLock()⽅法尝试去获取锁,如果获取成功就直接插⼊相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以⾃旋的⽅式去继续的调⽤tryLock()⽅法去获取锁,超过指定次数就挂起,等待唤醒
get操作
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第⼀次需要经过⼀次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进⾏对⽐,成功就返回,不成功就返回null
size操作
计算ConcurrentHashMap的元素⼤⼩是⼀个有趣的问题,因为他是并发操作的,就是在你计算size的时候,他还在并发的插⼊数据,可能会导致你计算出来的size和你实际的size有相差(在你return size的时候,插⼊了多个数据),要解决这个问题,JDK1.7版本⽤两种⽅案
for(;;) {
if(retries++ == RETRIES_BEFORE_LOCK) {
for(int j  =  0    ; j < gments.length; ++j) ensureSegment(j).lock();  // force creation
}
sum = 0L;
size =  0    ;
overflow =  fal    ;
for  (    int  j =  0    ; j < gments.length; ++j) {
Segment<K,V> g = gmentAt(gments, j);
if  (g !=  null    ) { sum += g.modCount;  int  c = g.count;  if  (c <  0  || (size += c) <  0    )
overflow =  true    ;
} }
if  (sum == last)  break    ;
last = sum; } }
finally  {
if  (retries > RETRIES_BEFORE_LOCK) {
for  (    int  j =  0    ; j < gments.length; ++j)
gmentAt(gments, j).unlock();
}
}
1. 第⼀种⽅案他会使⽤不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,⽐较前后两次计算的结果,结果⼀致就
认为当前没有元素加⼊,计算的结果是准确的
2. 第⼆种⽅案是如果第⼀种⽅案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回ConcurrentHashMap (JDK1.8的实现)
JDK1.8的实现已经摒弃了Segment的概念,⽽是直接⽤Node数组+链表+红⿊树的数据结构来实现,
并发控制使⽤Synchronized和CAS 来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
说明:ConcurrentHashMap的数据结构(数组+链表+红⿊树),桶中的结构可能是链表,也可能是红⿊树,红⿊树是为了提⾼查找效率。
在深⼊JDK1.8的put和get实现之前要知道⼀些常量设计和数据结构,这些是构成ConcurrentHashMap实现结构的基础,下⾯看⼀下基本属性:
// node数组最⼤容量:2^30=1073741824
private  static  final  int  MAXIMUM_CAPACITY =  1  <<  30    ;
// 默认初始值,必须是2的幕数
private  static  final  int  DEFAULT_CAPACITY =  16    ;
//数组可能最⼤值,需要与toArray()相关⽅法关联
static  final  int  MAX_ARRAY_SIZE = Integer.MAX_VALUE -  8    ;
/
/并发级别,遗留下来的,为兼容以前的版本
private  static  final  int  DEFAULT_CONCURRENCY_LEVEL =  16    ;
// 负载因⼦
private  static  final  float  LOAD_FACTOR =  0    .75f;
// 链表转红⿊树阀值,> 8 链表转换为红⿊树
static  final  int  TREEIFY_THRESHOLD =  8    ;
//树转链表阀值,⼩于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
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    ;
// 2^15-1,help resize的最⼤线程数
private  static  final  int  MAX_RESIZERS = (    1  << (    32  - RESIZE_STAMP_BITS)) -  1    ;
// 32-16=16,sizeCtl中记录size⼤⼩的偏移量
// 32-16=16,sizeCtl中记录size⼤⼩的偏移量
private  static  final  int  RESIZE_STAMP_SHIFT =  32  - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static  final  int  MOVED    = -    1    ;
// 树根节点的hash值
static  final  int  TREEBIN  = -    2    ;
// RervationNode的hash值
static  final  int  RESERVED  = -    3    ;
/
/ 可⽤处理器数量
static  final  int  NCPU = Runtime().availableProcessors();
//存放node的数组
transient  volatile  Node<K,V>[] table;
/*控制标识符,⽤来控制table的初始化和扩容的操作,不同的值有不同的含义
*当为负数时:-    1    代表正在初始化,-N代表有N-    1    个线程正在进⾏扩容
*当为    0    时:代表当时的table还没有被初始化
*当为正数时:表⽰初始化或者下⼀次进⾏扩容的⼤⼩
*/
private  transient  volatile  int  sizeCtl;
基本属性定义了ConcurrentHashMap的⼀些边界以及操作时的⼀些控制,下⾯看⼀些内部的⼀些结构组成,这些是整个ConcurrentHashMap整个数据结构的核⼼
Node
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,⽤于存储数据,源代码如下
static  class  Node<K,V>  implements  Map.Entry<K,V> {
//链表的数据结构
final  int  hash;
final  K key;
//val和next都会在扩容时发⽣变化,所以加上volatile来保持可见性和禁⽌重排序
volatile  V val;
volatile  Node<K,V> next;
Node(    int  hash, K key, V val, Node<K,V> next) {
this    .hash = hash;
this    .key = key;
this    .val = val;
this    .next = next;

本文发布于:2023-05-12 08:47:22,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/887197.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:操作   链表   计算   数据   数组   数据结构   实现   指定
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图