• ConcurrentHashMap解析


        ConcurrentHashMap是hashMap在多线程安全下的实现,当hashMap要作为类成员变量时都可考虑使用concurrentHashMap来替代HahsMap.

    HashMap有哪些操作线程不安全

    1. 如果使用的是无参构造器,那么在put时会对数组进行初始化,这个过程是线程不安全的

    2. java1.7之前resize扩容时多线程会导致元素丢失或死环,1.8之后虽然不存在这个问题,但多线程下每个线程都是重复工作

    3. tab[i]=new Node()操作在多线程下可能会导致元素覆盖

    4. size++操作是非线程安全的,导致size不准确

    ConcurrenHashMap是如何解决这些问题的(java1.8版本)

    put过程源码分析

    1. final V putVal(K key, V value, boolean onlyIfAbsent) {
    2. if (key == null || value == null) throw new NullPointerException();
    3. 获取hash值,这里跟hashmap不同的是多了个HASH_BITS,可能是为了更加散列减少hash冲突
    4. int hash = spread(key.hashCode());
    5. int binCount = 0;
    6. for (Node<K,V>[] tab = table;;) {
    7. Node<K,V> f; int n, i, fh;
    8. if (tab == null || (n = tab.length) == 0)
    9. 如果使用的无参构造器,那么这里就要先初始化数组,方法里面使用了compareAndSwapInt保证只有一个线程能进行数组初始化,且初始化容量默认是16,其他线程再次进入for循环
    10. tab = initTable();
    11. 当hash值对应的数组下标位置没有元素,也就是没有hash冲突时,cas保证同时只有一个线程能tab[i]操作成功,这就避免了可能的元素覆盖,操作成功的线程直接break退出,操作失败的线程再次进入for循环
    12. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    13. if (casTabAt(tab, i, null,
    14. new Node<K,V>(hash, key, value, null)))
    15. break; // no lock when adding to empty bin
    16. }
    17. else if ((fh = f.hash) == MOVED)
    18. tab = helpTransfer(tab, f);
    19. else {
    20. V oldVal = null;
    21. 当tab[i]元素不为null,说明有hash冲突,就用synchronized锁住tab[i]上的元素
    22. synchronized (f) {
    23. 双重判断,跟单例原理差不多,怕上锁前已被其他线程修改
    24. if (tabAt(tab, i) == f) {
    25. if (fh >= 0) {
    26. binCount = 1;
    27. 遍历hash冲突的链表,并用binCount记录元素个数
    28. for (Node<K,V> e = f;; ++binCount) {
    29. K ek;
    30. 判断key是不是一个,两个条件:hash相等且(key地址/值相等或key重写的equals相等),判断成立的话用新值覆盖老值并break
    31. if (e.hash == hash &&
    32. ((ek = e.key) == key ||
    33. (ek != null && key.equals(ek)))) {
    34. oldVal = e.val;
    35. if (!onlyIfAbsent)
    36. e.val = value;
    37. break;
    38. }
    39. Node<K,V> pred = e;
    40. 遍历到最后一个元素并把新元素挂在最后面
    41. if ((e = e.next) == null) {
    42. pred.next = new Node<K,V>(hash, key,
    43. value, null);
    44. break;
    45. }
    46. }
    47. }
    48. 如果是红黑树,则放入树中
    49. else if (f instanceof TreeBin) {
    50. Node<K,V> p;
    51. binCount = 2;
    52. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
    53. value)) != null) {
    54. oldVal = p.val;
    55. if (!onlyIfAbsent)
    56. p.val = value;
    57. }
    58. }
    59. }
    60. }
    61. if (binCount != 0) {
    62. 如果链表个数超过了8个则转换为红黑树
    63. if (binCount >= TREEIFY_THRESHOLD)
    64. treeifyBin(tab, i);
    65. if (oldVal != null)
    66. return oldVal;
    67. break;
    68. }
    69. }
    70. }
    71. 这个实际就是size++操作和扩容判断,size++使用的是分段计数方式,对于每个分段使用cas进行+1,扩容过程跟put过程差不多,整个过程跟hashMap扩容原理一样,使用了尾插法,也使用synchronized锁,在tab[i]赋值时也使用了cas
    72. addCount(1L, binCount);
    73. return null;
    74. }

    总结(如何解决了HashMap线程不安全问题):

    1. 初始化数组时使用cas保证只有一个线程能成功并去做初始化工作,其他线程则继续for循环

    2. 在没有hash冲突时tab[i]使用cas进行赋值,保证同时只有一个线程赋值成功,其他线程则在下次for循环时进入hash冲突的代码段

    3. 当有hash冲突时使用synchronized锁住tab[i]位置的元素,而且进行了双检查,保证上锁后元素没有变化,然后在锁里进行链表或红黑树的维护,保证了hash冲突时元素的插入也是线程安全的

    4. 在size++时使用了分段计数方式,线程对单独的cell进行cas操作,这样减小了锁冲突的可能性,在保证线程安全的情况下提高了size++的性能,但在size()方法时需要把base和cell的数都加起来,是一种典型的空间换时间的操作

    5. 在扩容时使用的技巧跟put过程差不多,通过cas和synchronized保证线程安全,跟hashmap一样是尾插法和2倍容量

    公众号同名,欢迎关注

  • 相关阅读:
    R语言两个矩阵(两组)数据的相关性分析
    虹科 | 解决方案 | 汽车示波器 远程诊断方案
    关于华为OD,你应该知道的
    反射和工厂设计模式(JAVA基础九)
    多肽TAT接枝/功能肽RGDC修饰荧光碳量子点/碳量子点修饰多肽LyP-1的制备研究
    linux最小化安装后的初始化
    Django笔记二十四之数据库函数之比较和转换函数
    【CentOS】忘记root密码
    python sorted 自带函数模块 排序方法使用
    js 连接快手打印组件并实现打印
  • 原文地址:https://blog.csdn.net/shidebin/article/details/126817912