• 翻了ConcurrentHashMap1.7 和1.8的源码,我总结了它们的主要区别。


    ConcurrentHashMap

    思考:HashTable是线程安全的,为什么不推荐使用?

    HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。

    1 ConcurrentHashMap 1.7

    在JDK1.7中ConcurrentHashMap采用了数组+分段锁的方式实现

    Segment(分段锁)-减少锁的粒度

    ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

    1.存储结构

    Java 7 版本 ConcurrentHashMap 的存储结构如图:

    ConcurrnetHashMap 由很多个 Segment 组合,而每一个 Segment 是一个类似于 HashMap 的结构,所以每一个 HashMap 的内部可以进行扩容。但是 Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,所以可以认为 ConcurrentHashMap 默认支持最多 16 个线程并发。

    2. 初始化

    通过 ConcurrentHashMap 的无参构造探寻 ConcurrentHashMap 的初始化流程。

    1. /**
    2. * Creates a new, empty map with a default initial capacity (16),
    3. * load factor (0.75) and concurrencyLevel (16).
    4. */
    5. public ConcurrentHashMap() {
    6. this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    7. }
    8. 复制代码

    无参构造中调用了有参构造,传入了三个参数的默认值,他们的值是。

    1. /**
    2. * 默认初始化容量,这个容量指的是Segment 的大小
    3. */
    4. static final int DEFAULT_INITIAL_CAPACITY = 16;
    5. /**
    6. * 默认负载因子
    7. */
    8. static final float DEFAULT_LOAD_FACTOR = 0.75f;
    9. /**
    10. * 默认并发级别,并发级别指的是Segment桶的个数,默认是16个并发大小
    11. */
    12. static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    13. Segment下面entryset数组的大小是用DEFAULT_INITIAL_CAPACITY/DEFAULT_CONCURRENCY_LEVEL求出来的。
    14. 复制代码

    接着看下这个有参构造函数的内部实现逻辑。

    1. @SuppressWarnings("unchecked")
    2. public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
    3. // 参数校验
    4. if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
    5. throw new IllegalArgumentException();
    6. // 校验并发级别大小,大于 1<<16,重置为 65536
    7. if (concurrencyLevel > MAX_SEGMENTS)
    8. concurrencyLevel = MAX_SEGMENTS;
    9. // Find power-of-two sizes best matching arguments
    10. // 2的多少次方
    11. int sshift = 0;//控制segment数组的大小
    12. int ssize = 1;
    13. // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值
    14. while (ssize < concurrencyLevel) {
    15. ++sshift;//代表ssize左移的次数
    16. ssize <<= 1;
    17. }
    18. // 记录段偏移量
    19. this.segmentShift = 32 - sshift;
    20. // 记录段掩码
    21. this.segmentMask = ssize - 1;
    22. // 设置容量 判断初始容量是否超过允许的最大容量
    23. if (initialCapacity > MAXIMUM_CAPACITY)
    24. initialCapacity = MAXIMUM_CAPACITY;
    25. // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量
    26. //求entrySet数组的大小,这个地方需要保证entrySet数组的大小至少可以存储下initialCapacity的容量,假设initialCapacity为33,ssize为16,那么c=2,所以if语句是true,那么c=3,MIN_SEGMENT_TABLE_CAPACITY初始值是2,所以if语句成立,那么cap=4,所以每一个segment的容量初始为4,segment为1616*4>33成立,entrySet数组的大小也需要是2的幂次方
    27. int c = initialCapacity / ssize;
    28. if (c * ssize < initialCapacity)
    29. ++c;
    30. int cap = MIN_SEGMENT_TABLE_CAPACITY;
    31. //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数
    32. while (cap < c)
    33. cap <<= 1;
    34. // create segments and segments[0]
    35. // 创建 Segment 数组,设置 segments[0]
    36. Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
    37. (HashEntry<K,V>[])new HashEntry[cap]);
    38. Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    39. UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    40. this.segments = ss;
    41. }
    42. 复制代码

    总结一下在 Java 7 中 ConcurrnetHashMap 的初始化逻辑。

    1. 必要参数校验。
    2. 校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无参构造默认值是 16.
    3. 寻找并发级别 concurrencyLevel 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16
    4. 记录 segmentShift 偏移量,这个值为【容量 = 2 的N次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
    5. 记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15.
    6. 初始化 segments[0]默认大小为 2负载因子 0.75扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。
    1. 计算segment数组容量的大小。
    2. 计算entrySet数组的大小。
    3. 初始化segment数组,其中生成一个s0对象放在数组的第0个位置
    4. 为什么首先需要一个s0存储到数组的第一个位置?

    因为初始化数组完成后数组元素都还是null值,以后每一次添加一个元素的话,需要封装为entrySet对象,还需要对entrySet数组的大小重新计算,如果把第一次的计算结果全部存储到S0中,那么以后的话只需要直接拿来使用即可,不需要重新计算。虽然Segment对象不同,但是对象中属性内容其实是一样的。

    1. Segment数组的长度第一次已经确定,以后不会在改变,扩容是局部扩容,只对setrySet数组的容量进行扩容。

    3. put

    接着上面的初始化参数继续查看

  • 相关阅读:
    laravel session 生命周期顶级边路
    【经验分享】运用云服务器实现挂机手机网课的操作,部分手机软件适用
    Vue3 实现一个无缝滚动组件(支持鼠标手动滚动)
    一起学数据结构(6)——栈和队列
    rpc error: code = Unimplemented desc =
    Ubuntu下vscode dotNet downloading的问题(Cmake代码高亮)
    spring boot 定时任务@Scheduled(cron = ““)不可用时并且注入失败时——笔记
    CFD瞬态计算的一些注意事项
    自动化测试在 Kubernetes Operator 开发中的应用:以 OpenTelemetry 为例
    Nginx 修改server_name后无法访问
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/127105720