Java:ConcurrentHashMap类小记-2(JDK7)

对 Java 中的 ConcurrentHashMap类,做一个微不足道的小小小小记,分三篇博客:

结构说明

构造函数

无参构造

  1. // 空参构造
  2. public ConcurrentHashMap() {
  3. // 调用本类的带参构造,都是使用的默认值
  4. // DEFAULT_INITIAL_CAPACITY = 16
  5. // DEFAULT_LOAD_FACTOR = 0.75f
  6. // int DEFAULT_CONCURRENCY_LEVEL = 16
  7. this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
  8. }

带参数构造

  1. // initialCapacity定义ConcurrentHashMap存放元素的容量
  2. // concurrencyLevel定义ConcurrentHashMap中Segment[]的大小
  3. public ConcurrentHashMap(int initialCapacity,
  4. float loadFactor, int concurrencyLevel) {
  5. // 参数校验
  6. if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
  7. throw new IllegalArgumentException();
  8. if (concurrencyLevel > MAX_SEGMENTS)
  9. concurrencyLevel = MAX_SEGMENTS;
  10. // Find power-of-two sizes best matching arguments
  11. int sshift = 0;
  12. int ssize = 1; // 指的是Segment数组的长度
  13. // 计算Segment[]的大小,保证是2的幂次方数
  14. while (ssize < concurrencyLevel) {
  15. ++sshift;
  16. // 左移1位,即*2操作,最终保证:ssize为大于等于concurrencyLevel的2的幂次方数
  17. ssize <<= 1;
  18. }
  19. // 这两个值用于后面计算Segment[]的角标
  20. this.segmentShift = 32 - sshift; // a.假定concurrencyLevel为16,则sshift为4,则segmentShift为28
  21. this.segmentMask = ssize - 1; // b.假定concurrencyLevel为16,则ssize为16,segmentMask为15
  22. if(initialCapacity > MAXIMUM_CAPACITY)
  23. initialCapacity = MAXIMUM_CAPACITY
  24. // 计算每个Segment中存储元素的个数,基于上述假定,16/16=1,即c=1
  25. int c = initialCapacity / ssize;
  26. if (c * ssize < initialCapacity)
  27. ++c;
  28. // 最小Segment中存储元素的个数为2,MIN_SEGMENT_TABLE_CAPACITY=2
  29. int cap = MIN_SEGMENT_TABLE_CAPACITY;
  30. // 矫正每个Segment中存储元素的个数,保证是2的幂次方,最小为2
  31. while (cap < c)
  32. cap <<= 1;
  33. // 创建一个Segment对象,作为其他Segment对象的模板
  34. Segment<K,V> s0 =
  35. // new了一个HashEntry数组,容量即为cap
  36. new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
  37. (HashEntry<K,V>[])new HashEntry[cap]);
  38. // 创建一个Segment数组
  39. Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
  40. // 利用Unsafe类,将创建的Segment对象存入0角标位置
  41. // 通过cas,将创建的Segment对象设置到Segment[]数组中
  42. // 其中SBASE即为数组的基础偏移量,baseOffset
  43. UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
  44. // final Segment<K,V>[] segments;
  45. this.segments = ss;
  46. }

综上:ConcurrentHashMap中保存了一个默认长度为16的Segment[],每个Segment元素中保存了一个默认长度为2的HashEntry[]数组,我们添加的元素,是存入对应的Segment中的HashEntry[]中。所以ConcurrentHashMap中默认元素的长度是32个,而不是16个

内部类

Segment 数组

  1. static final class Segment<K,V> extends ReentrantLock implements Serializable {
  2. // 构造函数
  3. Segment(float 1f, int threshold, HashEntry<K,V>[] tab){
  4. this.loadFactor = 1f;
  5. this.threshold = threshold;
  6. this.table = tab;
  7. }
  8. // ...
  9. }

Segment 是继承自 ReentrantLock 的,它可以实现同步操作,从而保证多线程下的安全。因为每个Segment 之间的锁互不影响,所以我们也将ConcurrentHashMap中的这种锁机制称之为分段锁,这比HashTable的线程安全操作高效的多。

HashEntry 数组

  1. // ConcurrentHashMap中真正存储数据的对象
  2. static final class HashEntry<K,V> {
  3. final int hash; // 通过运算,得到的键的hash值
  4. final K key; // 存入的键
  5. volatile V value; // 存入的值
  6. volatile HashEntry<K,V> next; // 记录下一个元素,形成单向链表
  7. HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
  8. this.hash = hash;
  9. this.key = key;
  10. this.value = value;
  11. this.next = next;
  12. }
  13. }

图示Segment数组

put 过程

源码分析

put() 方法:

  1. public V put(K key, V value) {
  2. Segment<K,V> s;
  3. if (value == null)
  4. // ConcurrentHashMap的value不能为null
  5. throw new NullPointerException();
  6. // 基于key,计算hash值,key也不允许为null,hash中会调用key的hashcode方法
  7. int hash = hash(key);
  8. // 因为一个键要计算两个数组的索引,为了避免冲突,这里取hash的高位计算Segment[]的索引,后续进一步说明
  9. int j = (hash >>> segmentShift) & segmentMask;
  10. // a)根据角标位获取segments数组中的segment元素,通过cas方式获取,计算方式如下:
  11. // j<<SSHIFT+SBASE 等价于 j*scale+sbase,这样操作是考虑到了效率问题
  12. if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
  13. (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
  14. // 判断该索引位的Segment对象是否创建,没有就创建,☆后续分析☆
  15. s = ensureSegment(j);
  16. // 调用Segment的put方法实现元素添加,☆后续分析☆
  17. return s.put(key, hash, value, false);
  18. }
  19. // 进一步对索引计算的说明:
  20. (hash >>> segmentShift) & segmentMask;
  21. // 还是按照初始的concurrencyLevel为16为16,则segmentShift为28,segmentMask为15
  22. // 对hash值无符号右移动segmentShift,即取到了hash的高4位
  23. // 将hash值的高4位和segmentMask进行按位 &, 等价于(Segment的数组长度-1)取模,计算出该hash值在Segment数组的角标位

初始对于角标位置的segment元素:ensureSegment()

  1. // 创建对应索引位的Segment对象,并返回
  2. // 在创建segment元素并放入HashEntry数组的过程中并没有加锁,而是通过cas保证了线程安全
  3. private Segment<K,V> ensureSegment(int k) {
  4. final Segment<K,V>[] ss = this.segments;
  5. // 同样获取segments中的偏移量
  6. long u = (k << SSHIFT) + SBASE; // raw offset
  7. Segment<K,V> seg;
  8. // 获取,如果为null,即创建,
  9. // 而获取这个segment元素判空在进入函数之前已经进行了一次,在此处再一次获取则是考虑到了线程安全问题
  10. if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
  11. // 以0角标位的Segment为模板
  12. Segment<K,V> proto = ss[0]; // use segment 0 as prototype
  13. int cap = proto.table.length;
  14. float lf = proto.loadFactor;
  15. int threshold = (int)(cap * lf);
  16. // 创建了HashEntry[]
  17. HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
  18. // 获取,如果为null,即创建,又去了一次角标位置的segment元素,也是考虑了线程安全
  19. if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
  20. == null) { // recheck
  21. // 正在创建对应的segment
  22. Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
  23. // 自旋方式,将创建的Segment对象放到Segment[]中,确保线程安全
  24. while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
  25. == null) {
  26. // 当待放的位置为null时,则放入
  27. // 如果放成功了,则break
  28. if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
  29. break;
  30. }
  31. }
  32. }
  33. // 不为空了则返回
  34. return seg;
  35. }

当 segments 数组中存在了 HashEntry 时,则通过调用 Segment 的 put 方法实现元素添加

  1. final V put(K key, int hash, V value, boolean onlyIfAbsent) {
  2. // tryLock尝试获取锁,获取成功,node为null,代码向下执行
  3. // 如果有其他线程占据锁对象,那么去做别的事情,而不是一直等待,这是为了提升效率,
  4. // 即调用方法scanAndLockForPut☆后续分析☆
  5. HashEntry<K,V> node = tryLock() ? null :
  6. scanAndLockForPut(key, hash, value);
  7. V oldValue;
  8. try {
  9. // 成功进入了,说明获取Segments数组对应的Segment元素成功了,即成功加锁
  10. HashEntry<K,V>[] tab = table;
  11. // 取hash的低位,计算HashEntry[]的索引
  12. int index = (tab.length - 1) & hash;
  13. // 获取HashEntry[]索引位的元素对象,也是通过cas的方式获取
  14. HashEntry<K,V> first = entryAt(tab, index);
  15. for (HashEntry<K,V> e = first;;) { // 死循环
  16. if (e != null) { // 获取的元素对象不为空,发生了hash冲突
  17. K k;
  18. // 如果是重复元素,覆盖原值
  19. if ((k = e.key) == key ||
  20. (e.hash == hash && key.equals(k))) {
  21. oldValue = e.value;
  22. if (!onlyIfAbsent) {
  23. e.value = value;
  24. ++modCount;
  25. }
  26. break; // 替换成功之后break
  27. }
  28. // 如果不是重复元素,获取链表的下一个元素,继续循环遍历链表
  29. e = e.next;
  30. }
  31. else { // 如果获取到的元素为空
  32. // 若当前添加的键值对的HashEntry对象已经创建
  33. if (node != null)
  34. node.setNext(first); // 头插法关联即可:即当前节点的下一个节点为当前的头节点
  35. else
  36. // 创建当前添加的键值对的HashEntry对象
  37. node = new HashEntry<K,V>(hash, key, value, first);
  38. // 添加的元素数量递增
  39. int c = count + 1;
  40. // 判断是否需要扩容
  41. if (c > threshold && tab.length < MAXIMUM_CAPACITY)
  42. // 需要扩容,则进行扩容操作,☆后续分析☆
  43. rehash(node);
  44. else
  45. // 不需要扩容
  46. // 将当前添加的元素对象,存入数组角标位,完成头插法添加元素
  47. setEntryAt(tab, index, node);
  48. ++modCount;
  49. count = c;
  50. oldValue = null;
  51. break;
  52. }
  53. }
  54. } finally {
  55. //释放锁
  56. unlock();
  57. }
  58. return oldValue;
  59. }

在 put 方法中,若没有拿获取到锁的情况下,则调用方法scanAndLockForPut(),去完成HashEntry对象的创建,提升效率

  1. private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
  2. // 获取头部元素,获取是可以获取的
  3. HashEntry<K,V> first = entryForHash(this, hash);
  4. HashEntry<K,V> e = first;
  5. HashEntry<K,V> node = null
  6. int retries = -1; // negative while locating node
  7. while (!tryLock()) {
  8. // 获取锁失败,只有当获取锁成功后才会跳出循环,一种自旋操作
  9. HashEntry<K,V> f; // to recheck first below
  10. // 刚刚开始肯定是进入这里
  11. if (retries < 0) {
  12. // 没有下一个节点,并且也不是重复元素,直接创建HashEntry对象,不再遍历
  13. if (e == null) {
  14. if (node == null) // speculatively create node
  15. // 创建了HashEntry对象
  16. node = new HashEntry<K,V>(hash, key, value, null);
  17. retries = 0;
  18. }
  19. else if (key.equals(e.key))
  20. // 是一个重复元素,不需要创建HashEntry对象,不再遍历
  21. retries = 0;
  22. else
  23. // 继续遍历下一个节点
  24. e = e.next;
  25. }
  26. else if (++retries > MAX_SCAN_RETRIES) {
  27. // 如果尝试获取锁的次数过多,直接阻塞
  28. // MAX_SCAN_RETRIES会根据可用cpu核数来确定
  29. lock(); // 当自旋的次数大于MAX_SCAN_RETRIES,则不再自旋了,直接上锁(阻塞锁),直到获取到锁之后再break
  30. break;
  31. }
  32. else if ((retries & 1) == 0 &&
  33. (f = entryForHash(this, hash)) != first) {
  34. // 如果期间有别的线程获取锁,导致HashEntry结构发生变化,则重新遍历
  35. e = first = f; // re-traverse if entry changed
  36. retries = -1;
  37. }
  38. }
  39. // 成功获取到锁了,则退出该函数,继续回到put方法中
  40. return node;
  41. }

代码演示

这里“通话”和“重地”的哈希值是一样的,那么他们添加时,会存入同一个Segment对象,必然会存在锁竞争

  1. public static void main(String[] args) throws Exception {
  2. final ConcurrentHashMap chm = new ConcurrentHashMap();
  3. new Thread(){
  4. @Override
  5. public void run() {
  6. chm.put("通话","11");
  7. System.out.println("-----------");
  8. }
  9. }.start();
  10. //让第一个线程先启动,进入put方法
  11. Thread.sleep(1000);
  12. new Thread(){
  13. @Override
  14. public void run() {
  15. chm.put("重地","22");
  16. System.out.println("===========");
  17. }
  18. }.start();
  19. }

注意:在Debug时,在断点处需要选择线程断点,且加上响应的条件,这里就不再演示了

扩容 rehash

源码分析

  1. // 这个rehash操作都是在获取锁的情况下进行的,因此是线程安全的
  2. private void rehash(HashEntry<K,V> node) {
  3. // 获取原数组和原容量
  4. HashEntry<K,V>[] oldTable = table;
  5. int oldCapacity = oldTable.length;
  6. // 两倍容量
  7. int newCapacity = oldCapacity << 1;
  8. // 计算新的阈值
  9. threshold = (int)(newCapacity * loadFactor);
  10. // 基于新容量,创建HashEntry数组
  11. HashEntry<K,V>[] newTable =
  12. (HashEntry<K,V>[]) new HashEntry[newCapacity];
  13. // 计算新的mask,这个mask当然是用于定位Segment的
  14. int sizeMask = newCapacity - 1;
  15. // 实现数据迁移,遍历oldtable
  16. for (int i = 0; i < oldCapacity ; i++) {
  17. // 获取该位置的首节点
  18. HashEntry<K,V> e = oldTable[i];
  19. // 该位置有节点,则需要进行迁移操作
  20. if (e != null) {
  21. HashEntry<K,V> next = e.next;
  22. // idx表示的是新的位置
  23. int idx = e.hash & sizeMask;
  24. if (next == null) // Single node on list
  25. // 原位置只有一个元素,直接放到新数组即可
  26. newTable[idx] = e;
  27. else { // Reuse consecutive sequence at same slot
  28. // 这里的迁移方式做了一定的优化操作和hashmap迁移相比的话
  29. // === 图1 ===
  30. HashEntry<K,V> lastRun = e;
  31. int lastIdx = idx;
  32. for (HashEntry<K,V> last = next;
  33. last != null;
  34. last = last.next) {
  35. int k = last.hash & sizeMask;
  36. if (k != lastIdx) {
  37. lastIdx = k;
  38. lastRun = last;
  39. }
  40. }
  41. // === 图1 ===
  42. // === 图2 ===
  43. newTable[lastIdx] = lastRun;
  44. // === 图2 ===
  45. // Clone remaining nodes
  46. // === 图3 ===
  47. for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
  48. V v = p.value;
  49. int h = p.hash;
  50. int k = h & sizeMask;
  51. HashEntry<K,V> n = newTable[k];
  52. // 这里旧的HashEntry不会放到新数组
  53. // 而是基于原来的数据创建了一个新的 HashEntry 对象,放入新数组
  54. newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
  55. }
  56. // === 图3 ===
  57. }
  58. }
  59. }
  60. //采用头插法,将新元素加入到数组中
  61. int nodeIndex = node.hash & sizeMask; // add the new node
  62. node.setNext(newTable[nodeIndex]);
  63. newTable[nodeIndex] = node;
  64. table = newTable;
  65. }

上述说道了,在数据迁移方式中做了一定的优化操作和hashmap迁移相比的话,用个人的话概括而言如下:部分连接在一起的节点数据若重hash后节点idx不变,则直接迁移,而不需要再重新创建节点,即存在一种情况下同时迁移多个节点数据,减少了迁移的次数,能提升迁移的效率,见后续图解部分说明。

图解

图1:

图2:

图3:

优化图解:

集合长度获取

也做了一定的操作保证了 size 是安全的,具体见源码分析

  1. public int size() {
  2. // Try a few times to get accurate count. On failure due to
  3. // continuous async changes in table, resort to locking.
  4. // 拿到Segments数据
  5. final Segment<K,V>[] segments = this.segments;
  6. int size;
  7. boolean overflow; // true if size overflows 32 bits
  8. long sum; // sum of modCounts
  9. long last = 0L; // previous sum
  10. // 重试次数
  11. int retries = -1; // first iteration isn't retry
  12. try {
  13. for (;;) { // 死循环
  14. // 当第4次走到这个地方时,会将整个Segment[]的所有Segment对象锁住,此时结果必然是正确的
  15. // 第4次是因为RETRIES_BEFORE_LOCK=2
  16. if (retries++ == RETRIES_BEFORE_LOCK) {
  17. for (int j = 0; j < segments.length; ++j)
  18. ensureSegment(j).lock(); // force creation
  19. }
  20. sum = 0L;
  21. size = 0;
  22. overflow = false;
  23. for (int j = 0; j < segments.length; ++j) {
  24. Segment<K,V> seg = segmentAt(segments, j);
  25. if (seg != null) {
  26. // 累加所有Segment的操作次数,是为了判断在获取长度时,集合是否发生了变化
  27. sum += seg.modCount;
  28. // segment的长度
  29. int c = seg.count;
  30. // 累加所有segment中的元素个数 size+=c
  31. if (c < 0 || (size += c) < 0)
  32. overflow = true;
  33. }
  34. }
  35. // 当这次累加值和上一次累加值一样,证明没有进行新的增删改操作,返回sum
  36. // 第一次last为0,如果有元素的话,这个for循环最少循环两次的,保证了结果正确
  37. if (sum == last)
  38. break;
  39. // 记录累加的值
  40. last = sum;
  41. }
  42. } finally {
  43. // 如果之前有锁住,解锁
  44. if (retries > RETRIES_BEFORE_LOCK) {
  45. for (int j = 0; j < segments.length; ++j)
  46. segmentAt(segments, j).unlock();
  47. }
  48. }
  49. // 溢出,返回int的最大值,否则返回累加的size
  50. return overflow ? Integer.MAX_VALUE : size;
  51. }

参考:

这部分内容主要参考的是 bilibili UP 主:https://space.bilibili.com/488677881 讲解的 ConcurrentHashMap

其具体的视频地址未给出,而是通过其提供的网盘地址进行下载的

Java:ConcurrentHashMap类小记-2(JDK7)的更多相关文章

  1. Java:ConcurrentHashMap类小记-1(概述)

    Java:ConcurrentHashMap类小记-1(概述) 对 Java 中的 ConcurrentHashMap类,做一个微不足道的小小小小记,分三篇博客: Java:ConcurrentHas ...

  2. Java:ConcurrentHashMap类小记-3(JDK8)

    Java:ConcurrentHashMap类小记-3(JDK8) 结构说明 // 所有数据都存在table中, 只有当第一次插入时才会被加载,扩容时总是以2的倍数进行 transient volat ...

  3. Java:HashMap类小记

    Java:HashMap类小记 对 Java 中的 HashMap类,做一个微不足道的小小小小记 概述 HashMap:存储数据采用的哈希表结构,元素的存取顺序不能保证一致.由于要保证键的唯一.不重复 ...

  4. Java:TreeMap类小记

    Java:TreeMap类小记 对 Java 中的 TreeMap类,做一个微不足道的小小小小记 概述 前言:之前已经小小分析了一波 HashMap类.HashTable类.ConcurrentHas ...

  5. Java:HashTable类小记

    Java:HashTable类小记 对 Java 中的 HashTable类,做一个微不足道的小小小小记 概述 public class Hashtable<K,V> extends Di ...

  6. Java:LinkedHashMap类小记

    Java:LinkedHashMap类小记 对 Java 中的 LinkedHashMap类,做一个微不足道的小小小小记 概述 public class LinkedHashMap<K,V> ...

  7. Java:LinkedList类小记

    Java:LinkedList类小记 对 Java 中的 LinkedList类,做一个微不足道的小小小小记 概述 java.util.LinkedList 集合数据存储的结构是循环双向链表结构.方便 ...

  8. Java:ArrayList类小记

    Java:ArrayList类小记 对 Java 中的 ArrayList类,做一个微不足道的小小小小记 概述 java.util.ArrayList 是大小可变的数组的实现,存储在内的数据称为元素. ...

  9. Java:泛型小记

    Java:泛型小记 对 Java 中的 泛型类,做一个微不足道的小小小小记 泛型实现 概述 开篇: List<String> l1 = new ArrayList<String> ...

随机推荐

  1. noip模拟48

    A. Lighthouse 很明显的容斥题,组合式与上上场 \(t2\) 一模一样 注意判环时长度为 \(n\) 的环是合法的 B. Miner 题意实际上是要求偶拉路 对于一个有多个奇数点的联通块, ...

  2. Linux制作根文件系统笔记

    测试平台 宿主机平台:Ubuntu 12.04.4 LTS 目标机:Easy-ARM IMX283 目标机内核:Linux 2.6.35.3 交叉编译器:arm-linux-gcc 4.4.4 Bus ...

  3. MongoDB(6)- BSON 数据类型

    BSON BSON是一种二进制序列化格式,用于在 MongoDB 中存储文档和进行远程过程调用 跟 JSON 的数据结构很像,但是支持更丰富的数据类型 数据类型 数据类型 序号 别名 备注 Doubl ...

  4. word文档转成图片

    1:先把word文档转成pdf格式  这个是在word中转成pdf格式,保存好 2:再把pdf格式转成图片 在这个链接中打开https://smallpdf.com/cn/pdf-converter, ...

  5. 珠峰2016,第9期 vue.js 笔记部份

    在珠峰参加培训好年了,笔记原是记在本子上,现在也经不需要看了,搬家不想带上书和本了,所以把笔记整理下,存在博客中,也顺便复习一下 安装vue.js 因为方便打包和环境依赖,所以建意npm  init  ...

  6. Java空指针异常:java.lang.NullPointerException解决办法

    问题描述:运行maven项目抛出NullPointerException 空指针异常. 报空指针异常的原因有以下几种: 1字符串变量未初始化    例如:String x=null:对象x为null, ...

  7. 解决sofaboot项目右键入口方法没有run sofa application

    选中入口方法名,右键出现run sofa application

  8. TS基础笔记

    TS优势 更好的错误的提示,开发中及时发现问题:编辑器语法提示更完善:类型声明可以看出数据结构的语义,可读性更好; TS环境搭建 1.安装node;2.npm install typescript@3 ...

  9. 如何获取PHP命令行参数

    使用 PHP 开发的同学多少都会接触过 CLI 命令行.经常会有一些定时任务或者一些脚本直接使用命令行处理会更加的方便,有些时候我们会需要像网页的 GET . POST 一样为这些命令行脚本提供参数. ...

  10. Java面向对象系列(9)- 方法重写

    为什么需要重写? 父类的功能,子类不一定需要,或者不一定满足 场景一 重写都是方法的重写,和属性无关 父类的引用指向了子类 用B类新建了A类的对象,把A赋值给了B,这时候B是A,A又继承了B类,向上转 ...