本书是基于JDK7创作的,所以下面说的性质都是JDK7特性的总结

  • 为什么要使用ConcurrentHashMap?
  • JDK7中ConcurrentHashMap的结构
  • JDK7中ConcurrentHashMap的初始化过程
  • key-value进入后,如何判断属于那个segment呢?
  • get操作
  • put操作
  • size操作

为什么要使用ConcurrentHashMap

  1. HashMap不是线程安全的,在并发环境下,put操作会发生死循环
  2. Hashtable是线程安全的,但是每个操作都是synchronized修饰,太慢
  3. ConcurrentHashMap也用锁,但是不锁全部的Map,使用 分段锁。 ```java //jdk7 死循环代码 public static void main(String[] args) throws InterruptedException {
    1. final Map<String,String> map = new HashMap<>(2);
    2. Thread thread = new Thread(new Runnable() {
    3. @Override
    4. public void run() {
    5. for (int i = 0; i < 10000; i++) {
    6. new Thread(new Runnable() {
    7. @Override
    8. public void run() {
    9. map.put(UUID.randomUUID().toString(), "");
    10. // System.out.println("hello");
    11. }
    12. }, "gx" + i).start();
    13. }
    14. }
    15. }, "gx");
    16. thread.start();
    17. thread.join();
  1. }
  1. <a name="ImVSn"></a>
  2. # JDK7中ConcurrentHashMap结构
  3. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1609516/1642593712504-c93fd9fb-8772-457d-8bca-505bd592da88.png#clientId=u610d18a7-77e4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=514&id=uf3d59647&margin=%5Bobject%20Object%5D&name=image.png&originHeight=463&originWidth=914&originalType=binary&ratio=1&rotation=0&showTitle=false&size=198934&status=done&style=none&taskId=u8fdde053-23ed-4dbe-a92d-5a0d5069c68&title=&width=1015.555582458591)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/1609516/1642593759528-641c9513-79d4-413b-a9a1-865cd951bd26.png#clientId=u610d18a7-77e4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=830&id=u3a00271d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=747&originWidth=893&originalType=binary&ratio=1&rotation=0&showTitle=false&size=274716&status=done&style=none&taskId=u0633d8b7-5bda-4702-b8fb-68727fa4fb4&title=&width=992.2222485071354)
  4. - 一个ConcurrentHashMap里包含一个Segment数组
  5. - 每个Segment基础ReentrantLock
  6. - 每个Segment的结构又和一个HashMap结构类似,数组链表
  7. - HashEntry数组,每个数组的元素是一个链表。
  8. <a name="QURFK"></a>
  9. # JD7中初始化过程
  10. <a name="l5rlr"></a>
  11. ## 重要参数
  12. - initialCapacity:初始化容量,默认16
  13. - loadFactor:加载因子,默认 0.75
  14. - concurrencyLevel:指定Segement数组长度,默认16
  15. 用上面三个参数来初始化下面参数
  16. - segment数组:
  17. - segmentShift:段偏移量
  18. - segmentMask:段掩码
  19. - segment中的HashEntry数组
  20. <a name="Uq8Ng"></a>
  21. ## 初始化segments数组和掩码和偏移量
  22. ```java
  23. //三个参数都要大于0
  24. if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
  25. throw new IllegalArgumentException();
  26. if (concurrencyLevel > MAX_SEGMENTS)
  27. concurrencyLevel = MAX_SEGMENTS;
  28. // Find power-of-two sizes best matching arguments
  29. int sshift = 0;
  30. //指定segment数组的长度,必须2^N
  31. int ssize = 1;
  32. while (ssize < concurrencyLevel) {
  33. //偏移次数
  34. ++sshift;
  35. ssize <<= 1;
  36. }
  37. //偏移量 据左边
  38. this.segmentShift = 32 - sshift;
  39. this.segmentMask = ssize - 1;
  • concurrenyLevel用来计算segments数组的长度
  • 最后以ssize为准,是2^N
  • sshift计算的是1左移次数,也就是ssize的二进制值,然后用32-sshift得到segmentShift
  • segmentMask等于ssize-1

    初始化每个segment

    ```java if (initialCapacity > MAXIMUM_CAPACITY)
    1. initialCapacity = MAXIMUM_CAPACITY;
    //初始容量 / segment数组大小 , 每个segment中应该有多少个元素 int c = initialCapacity / ssize; //不能整除要+1, 可多不可少 if (c * ssize < initialCapacity)
    1. ++c;
  1. int cap = MIN_SEGMENT_TABLE_CAPACITY;
  2. while (cap < c)
  3. cap <<= 1;
  4. // create segments and segments[0]
  5. Segment<K,V> s0 =
  6. new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
  7. (HashEntry<K,V>[])new HashEntry[cap]);
  8. Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
  9. UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
  10. this.segments = ss;
  1. - cap:是segmentHashEntry数组的长度,最小值为2
  2. - cap*loadFacotr:是每个segment的阈值。
  3. <a name="LWkBY"></a>
  4. # 定位Segment
  5. 一个key-value对进入后,如何判断其放在哪个segment中。<br />是通过Wang/Jenkins hash的变种算法对元素的hashCode进行再散列
  6. ```java
  7. private int hash(Object k) {
  8. int h = hashSeed;
  9. if ((0 != h) && (k instanceof String)) {
  10. return sun.misc.Hashing.stringHash32((String) k);
  11. }
  12. h ^= k.hashCode();
  13. // Spread bits to regularize both segment and index locations,
  14. // using variant of single-word Wang/Jenkins hash.
  15. h += (h << 15) ^ 0xffffcd7d;
  16. h ^= (h >>> 10);
  17. h += (h << 3);
  18. h ^= (h >>> 6);
  19. h += (h << 2) + (h << 14);
  20. return h ^ (h >>> 16);
  21. }

通过上面的方法计算了散列值后,通过下面一段代码找到具体的index

  1. int j = (hash >>> segmentShift) & segmentMask;
  2. if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
  3. (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
  4. s = ensureSegment(j);
  • hash后的结果int,无符号右移后&掩码的作用是让长度落在数组范围

get操作

get操作流程简单经过一次再散列,定位到segment,再调用segment的get获取值
get操作整体若读取的值是空,才会加锁重读。
原因:get方法将要使用的共享变量定义成了 volatile。
在segment中的散列是通过hashCode直接&

put操作

put操作方法必须加锁

  • 定位Segment
  • 在Segment中执行插入操作

    • 是否需要扩容,插入前扩容。
      • 创建一个原数组两倍的数组。然后旧数组元素再散列到新数组
    • 定位元素位置,放置

      size操作

    • 统计每个segment中的元素个数,然后求和

    • 防止count过程中新增元素,所以会统计两次
    • 两次结果不相同的话,才加锁统计
    • modCount是判断是否在统计期间有修改操作