深入分析ConcurrentHashMap

再多线程的情况下,如果使用HashMap,就会导致死循环,导致cpu利用率接近100%,所以如果是并发的情况不要使用HashMap
导致死循环主要是这段代码,当在多线程的情况由于没有同步导致,着段代码在扩容的时候会执行

    do {
    Entry<K,V> next = e.next; //假设线程一执行到这里就被调度挂起了,当再次获得执行的时候,
    数据结构已经改变了,而线程却不知道
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
    } while (e != null);

线程安全的HashTable 容器

HashTable安全是安全,但是代价太大,该容器采用synchronized来保证线程安全,在多线程同时对它进行操作,竞争激烈的情况下效率非常的底下。因为当一个线程访问的时候,其他线程在轮询或阻塞的状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

并发集合容器ConcurrentHashMap

ConcurrentHashMap 里面的table 是实现 Map.Entry接口的一个数组,在对ConcurrentHashMap,进行put操作的时候,如果里面没有
键值对,则进行添加。

  else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                 if (casTabAt(tab, i, null,
                              new Node<K,V>(hash, key, value, null)))
                     break;                   // no lock when adding to empty bin
             }

这里的casTabAt,以及tabAt本质使用的是sun.misc.Unsafe这里类里面的方法,该方法是在不加锁的情况下通过CAS实现线程安全
如果里面有存在想通过的key,那么就对其进行更新,在更新的代码块中,使用synchronized 来进行同步

        synchronized (f) {
                            if (tabAt(tab, i) == f) {
                                if (fh >= 0) {
                                    binCount = 1;
                                    for (Node<K,V> e = f;; ++binCount) {
                                        K ek;
                                        if (e.hash == hash &&
                                            ((ek = e.key) == key ||
                                             (ek != null && key.equals(ek)))) {
                                            oldVal = e.val;
                                            if (!onlyIfAbsent)
                                                e.val = value;
                                            break;
                                        }
                                      .....
                                    }
                                }
                                ....
                            }
                        }

刚才我们说HashMap 在重新分配空间的时候会导致死循环,那么ConcurrentHashMap是如何解决这个问题的了?结果跟我们上面分析的put
操作如出一辙

     else if ((f = tabAt(tab, i)) == null)
                    advance = casTabAt(tab, i, null, fwd);
                else if ((fh = f.hash) == MOVED)
                    advance = true; // already processed
                else {
                    synchronized (f) {
                        ...

                    }

使用了CAS,synchronized进行同步,这样就保证了线程的安全

说完put,那么get 操作又是如何保证同步的了?

  if ((tab = table) != null && (n = tab.length) > 0 &&
              (e = tabAt(tab, (n - 1) & h)) != null) {
              if ((eh = e.hash) == h) {
                  if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                      return e.val;
              }
              else if (eh < 0)
                  return (p = e.find(h, key)) != null ? p.val : null;
              while ((e = e.next) != null) {
                  if (e.hash == h &&
                      ((ek = e.key) == key || (ek != null && key.equals(ek))))
                      return e.val;
              }
          }

get的实现相对于来说简单了,先经过一次再哈希,然后使用这个哈希值通过哈希运算定位到table,从代码上面看到就是使用一个 tabAt操作而并没有使用synchronized来对代码块进行同步,是因为在存储
value 的时候使用了volatile

        static class Node<K,V> implements Map.Entry<K,V> {
                              final int hash;
                              final K key;
                              volatile V val;
                              ....
                            }

定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值)之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

参考资料:

1.JDK1.8源代码。

2.《Java并发编程实践》

3.聊聊并发

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMap在扩容时采用了分段加锁的策略,也就是将整个Map分成多个段(Segment),每个段都有自己的锁。每个段维护着一个哈希表,包含多个链表或红黑树。当一个线程需要进行插入、删除或者查找操作时,只需要锁住当前需要操作的段,而不是整个HashMap。这样可以大大提高并发性能。 在JDK 1.8版本中,ConcurrentHashMap使用了synchronized、volatile和CAS(Compare And Swap)的组合来实现对锁的降低粒度。具体来说,每个Segment中的操作使用了synchronized关键字来保证原子性,volatile关键字用于保证可见性,而CAS用于实现乐观锁机制,减少锁的争用。这样一来,多个线程可以同时对不同的段进行操作,提高了并发性能。 至于为什么选择将initialCapacity设置为2的n次幂,这是为了提高HashMap的性能。当initialCapacity的容量是2的n次幂时,可以通过位运算来计算key的hash值对应的桶的索引,这样可以加快定位的速度。而且,当需要进行扩容时,也可以更高效地进行重新分配。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [JavaSE基础面试题.docx](https://download.csdn.net/download/BruceLiu_code/85610232)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [又一篇深入分析ConcurrentHashMap的文章](https://blog.csdn.net/caihaijiang/article/details/8219826)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [说说ConcurrentHashMap原理与实现](https://blog.csdn.net/jjclove/article/details/124334337)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值