ConcurrentHashMap&HashMap&TableMap

深藏阁楼爱情的钟 2023-02-09 11:56 14阅读 0赞

首先HashMap的初始容量设置为16,其实必须是2的幂。
问一个问题:

  1. Hashmap中的链表大小超过八个时会自动转化为红黑树,
  2. 当删除小于六时重新变为链表,为啥呢?

在源码中,有一个叫做_Factor,默认是0.75,根据泊松分布,也即是说单个hash桶内元素个数为8的概率小于百万分之一。所以以7为转折点,大于等于8的时候才进行转换为树,小于等于6的时候就化为链表。

在多线程情况下,为保证线程安全,怎样使用HashMap?

  • 使用Collections.synchronizedMap(Map)创建线程安全的map集合;
  • Hashtable
  • ConcurrentHashMap
    但是在一般情况下,处于线程并发度原因,只使用ConcurrentHashMap。

1 HashTable与HashMap区别:

  • 在多线程情况下使用HashTable,对get方法进行synchoried枷锁,保证安全机制。
  • HashMap允许键值为空:
    HashTable不允许键值为空,一下源码

    //HashMap 看出当键值为空,则返回第一个桶进行插值
    static final int hash(Object key) {

    1. int h;
    2. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

    HashTable
    采用安全失败机制:即当前获取的数据可能不是最新的数据,如果为空值,则无法判断是为空还是不存在。
    //看下面抛出异常信息。
    public V put(K key, V value) {

    1. Segment<K,V> s;
    2. if (value == null)
    3. throw new NullPointerException();//这就是为啥他不可以put null值的原因
    4. int hash = hash(key);
    5. int j = (hash >>> segmentShift) & segmentMask;
    6. if ((s = (Segment<K,V>)UNSAFE.getObject
    7. (segments, (j << SSHIFT) + SBASE)) == null)
    8. s = ensureSegment(j);
    9. return s.put(key, hash, value, false);

    }

HashMap线程不安全,为什么使用使用CourrentHashMap?1.7版本

HashMap的底层参考我的另一篇文章,桶原则:https://blog.csdn.net/huifeidezhu521/article/details/105451427

在ConcrrentHashMap中的数组采用Segment 数组,理论上讲采用分段锁机制,继承ReentrantLock,就是访问当前Segment,并不会影响其他的。

put操作,做到线程安全
1.7版本

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

1.8版本:
put1.8版本

用语言描述就是:

  1. 根据 key 计算出 hashcode
  2. 判断是否需要进行初始化。
  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

get操作
需要通过key值Hsah之后定位到Segement,再一次Hash定位到具体元素。每次获取都是最新值,并不需要加锁。

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

但是在1.8版本中采用CAS + synchronized 来保证并发安全性。同时引入红黑树。

ConcurrentHashMap的get操作

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

1.8版本

小结:1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率,甚至取消了 ReentrantLock 改为了 synchronized。

///站在巨人的肩膀上,也是第一次写这么深奥的东西,谅解。

发表评论

表情:
评论列表 (有 0 条评论,14人围观)

还没有评论,来说两句吧...

相关阅读