ConcurrentHashMap

 

ConcurrentHashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

1.7中的数据结构

uploading.4e448015.gif正在上传…重新上传取消

是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。

1. ConcurrentHashMap并发度高的原因么?

(1)原理上来说,ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。(2)每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。(3)就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

2. ConcurrentHashMap如何做到put线程安全?

  1.         final V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  2.           // 将当前 Segment 中的 table 通过 key  hashcode 定位到 HashEntry  
  3.             HashEntry<K,V> node = tryLock() ? null :  
  4.                 scanAndLockForPut(key, hash, value);  
  5.             V oldValue;  
  6.             try {  
  7.                 HashEntry<K,V>[] tab = table;  
  8.                 int index = (tab.length - 1) & hash;  
  9.                 HashEntry<K,V> first = entryAt(tab, index);  
  10.                 for (HashEntry<K,V> e = first;;) {  
  11.                     if (e != null) {  
  12.                         K k;  
  13.  // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value  
  14.                         if ((k = e.key) == key ||  
  15.                             (e.hash == hash && key.equals(k))) {  
  16.                             oldValue = e.value;  
  17.                             if (!onlyIfAbsent) {  
  18.                                 e.value = value;  
  19.                                 ++modCount;  
  20.                             }  
  21.                             break;  
  22.                         }  
  23.                         e = e.next;  
  24.                     }  
  25.                     else {  
  26.                  // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。  
  27.                         if (node != null)  
  28.                             node.setNext(first);  
  29.                         else  
  30.                             node = new HashEntry<K,V>(hash, key, value, first);  
  31.                         int c = count + 1;  
  32.                         if (c > threshold && tab.length < MAXIMUM_CAPACITY)  
  33.                             rehash(node);  
  34.                         else  
  35.                             setEntryAt(tab, index, node);  
  36.                         ++modCount;  
  37.                         count = c;  
  38.                         oldValue = null;  
  39.                         break;  
  40.                     }  
  41.                 }  
  42.             } finally {  
  43.                //释放锁  
  44.                 unlock();  
  45.             }  
  46.             return oldValue;  
  47.         }  

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

2. 那他get的逻辑呢?

(1)get 逻辑比较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

(2)由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

(3)ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

3.你有没有发现1.7虽然可以支持每个Segment并发访问,但是还是存在一些问题?

因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样问题,所以他在jdk1.8完全优化了。

4. jdk1.8他的数据结构

(1)其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

(2)跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。

5. 他值的存取操作么?以及是怎么保证线程安全的?

ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:

(1)根据 key 计算出 hashcode 。

(2)判断是否需要进行初始化。

(3)即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

(4)如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

(5)如果都不满足,则利用 synchronized 锁写入数据。

(6)如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

uploading.4e448015.gif正在上传…重新上传取消

6.你在上面提到CAS是什么?自旋又是什么?

(1)CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。

(2)CAS 操作的流程如下图所示,线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

 

这是一种乐观策略,认为并发操作并不总会发生。uploading.4e448015.gif正在上传…重新上传取消

7.CAS就一定能保证数据没被别的线程修改过么?

并不是的,比如很经典的ABA问题,CAS就无法判断了。

8. 什么是ABA?

就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。

  1. 那怎么解决ABA问题?

用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。时间戳

10. CAS性能很高,但是我知道synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?

(1)synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。

(2)针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。

11. 那我们回归正题,ConcurrentHashMap的get操作又是怎么样子的呢?

  1. 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  2. 如果是红黑树那就按照树的方式获取值。
  3. 就不满足那就按照链表的方式遍历获取值

uploading.4e448015.gif正在上传…重新上传取消

TODO:文中的快速失败(fail—fast)问到,那对应的安全失败(fail—safe)也是有可能知道的

  1. 谈谈你理解的 Hashtable,讲讲其中的 get put 过程。ConcurrentHashMap同问。
  2. 1.8 做了什么优化?
  3. 线程安全怎么做的?
  4. 不安全会导致哪些问题?
  5. 如何解决?有没有线程安全的并发容器?
  6. ConcurrentHashMap 是如何实现的?
  7. ConcurrentHashMap并发度为啥好这么多?
  8. 1.7、1.8 实现有何不同?为什么这么做?
  9. CAS是啥?
  10. ABA是啥?场景有哪些,怎么解决?
  11. synchronized底层原理是啥?
  12. synchronized锁升级策略
  13. 快速失败(fail—fast)是啥,应用场景有哪些?安全失败(fail—safe)同问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值