IT评测·应用市场-qidao123.com

标题: Java面试题:HashMap为什么线程不安全、ConcurrentHashMap原理(JDK1.7与JD [打印本页]

作者: 反转基因福娃    时间: 2024-9-1 16:27
标题: Java面试题:HashMap为什么线程不安全、ConcurrentHashMap原理(JDK1.7与JD
还记得 HashMap的实现原理、jdk1.7与jdk1.8的HashMap有什么区别吗?如果忘记可以到这里重新温习: Java面试题:ArrayList底层实现原理、HashMap的实现原理、HashMap的jdk1.7和jdk1.8有什么区别
1.HashMap 为什么线程不安全

1.1 概述——HashMap线程不安全的体现、原因、改善

HashMap是线程不安全的,它黑白同步的数据结构。主要体现在:

HashMap线程不安全原因(具体原因见1.2、1.3):

为了在多线程环境下使用安全的HashMap,可以接纳以下步伐:
1.2 jdk1.7中的线程不安全——死循环、数据丢失

jdk1.7 HashMap的transfer函数如下:
  1. void transfer(Entry[] newTable, boolean rehash) {
  2.     int newCapacity = newTable.length;
  3.     for (Entry<K,V> e : table) {
  4.         while(null != e) {
  5.             Entry<K,V> next = e.next;
  6.             if (rehash) {
  7.                 e.hash = null == e.key ? 0 : hash(e.key);
  8.             }
  9.             int i = indexFor(e.hash, newCapacity);
  10.             e.next = newTable[i];
  11.             newTable[i] = e;
  12.             e = next;
  13.         }
  14.     }
  15. }
复制代码
在对table进行扩容到newTable后,需要将原来数据转移到newTable中,留意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的次序会翻转,这里也是形成死循环的关键点。下面进行详细分析。
条件条件:此处假设

未resize前的数据结构如下:

如果在单线程中,末了的结果如下:

在多线程环境下,假设有两个线程A和B都在进行put操纵。线程A在实行到transfer函数中第11行代码 newTable = e 处挂起,此时线程A中运行结果如下,e=3、next=7、e.next=null:

线程A挂起后,此时线程B正常实行,并完成resize操纵,结果如下:

重点来了,根据Java内存模式可知,线程B实行完数据迁徙后,此时主内存中newTable和table都是最新的,也就是说:7.next=3、3.next=null。
此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,代码实行过程如下:
  1. newTable[3]=e ----> newTable[3]=3
  2. e=next ----> e=7
复制代码
此时结果如下:

继承循环:
  1. e=7
  2. next=e.next ----> next=3【从主存中取值】
  3. e.next=newTable[3] ----> e.next=3【从主存中取值】
  4. newTable[3]=e ----> newTable[3]=7
  5. e=next ----> e=3
复制代码
结果如下:

再次进行循环:
  1. e=3
  2. next=e.next ----> next=null
  3. e.next=newTable[3] ----> e.next=7 即:3.next=7
  4. newTable[3]=e ----> newTable[3]=3
  5. e=next ----> e=null
复制代码
留意此次循环:e.next=7,而在前次循环中7.next=3,出现环形链表,并且此时e=null循环结束。结果如下

上面说了此时e.next=null即next=null,当实行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操纵完成,很明显当线程A实行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操纵时会出现死循环。
并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。
参考回复:
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁徙的过程中,有大概导致死循环。
比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二参与
线程二:也读取hashmap,直接进行扩容。因为是头插法,链表的次序会进行颠倒过来。比如原来的次序是AB,扩容后的次序是BA,线程二实行结束。
线程一:继承实行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。当然,JDK 8 将扩容算法做了调解,不再将元素参加链表头(而是保持与扩容前一样的次序),尾插法,就制止了jdk7中死循环的问题。
1.3 jdk1.8中的线程不安全——数据覆盖

在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再接纳头插法方式,而是直接插入链表尾部 即尾插法,保持了链表元素的次序,解决了扩容造成的死循环、数据丢失问题。如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁徙。
但在多线程的环境下仍然不安全,会发生数据覆盖问题。
  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2.                boolean evict) {
  3.     Node<K,V>[] tab; Node<K,V> p; int n, i;
  4.     if ((tab = table) == null || (n = tab.length) == 0)
  5.         n = (tab = resize()).length;
  6.     if ((p = tab[i = (n - 1) & hash]) == null)  //如果没有hash碰撞则直接插入元素
  7.         tab[i] = newNode(hash, key, value, null);
  8.     else {
  9.         Node<K,V> e; K k;
  10.         if (p.hash == hash &&
  11.             ((k = p.key) == key || (key != null && key.equals(k))))
  12.             e = p;
  13.         else if (p instanceof TreeNode)
  14.             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  15.         else {
  16.             for (int binCount = 0; ; ++binCount) {
  17.                 if ((e = p.next) == null) {
  18.                     p.next = newNode(hash, key, value, null);
  19.                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  20.                         treeifyBin(tab, hash);
  21.                     break;
  22.                 }
  23.                 if (e.hash == hash &&
  24.                     ((k = e.key) == key || (key != null && key.equals(k))))
  25.                     break;
  26.                 p = e;
  27.             }
  28.         }
  29.         if (e != null) { // existing mapping for key
  30.             V oldValue = e.value;
  31.             if (!onlyIfAbsent || oldValue == null)
  32.                 e.value = value;
  33.             afterNodeAccess(e);
  34.             return oldValue;
  35.         }
  36.     }
  37.     ++modCount;
  38.     if (++size > threshold)
  39.         resize();
  40.     afterNodeInsertion(evict);
  41.     return null;
  42. }
复制代码
根据putVal源码,留意第6行代码,如果没有hash碰撞则会直接插入元素。假设两个线程A、B都在进行put操纵,并且hash函数盘算出的插入下标是相同的,当线程A实行完第6行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入;然后线程A得到时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
除此之前,还有代码的第38行处++size,假设线程A、B同时进行put操纵,当前HashMap的zise大小为10,当线程A实行到第38行代码时,从主内存中得到size的值为10后准备进行+1操纵,但是由于时间片耗尽只好让出CPU;线程B拿到CPU还是从主内存中拿到size的值10进行+1操纵,完成了put操纵并将size=11写回主内存,然后线程A再次拿到CPU并继承实行(此时size的值仍为10),当实行完put操纵后,还是将size=11写回内存,此时,线程A、B都实行了一次put操纵,但是size的值只增长了1,所有说还是由于数据覆盖又导致了线程不安全。
1.4 如何在多线程环境下使用安全的HashMap

为了在多线程环境下使用安全的HashMap,可以接纳以下步伐:
2.ConcurrentHashMap原理、分段锁、局部锁、线程安全

HashMap 在多线程环境下操纵不安全,ConcurrentHashMap是HashMap的线程安全版本。
JDK1.8中 ConcurrentHashMap内部是使用 数组 + 链表 + 红黑树 的结构来存储元素。相比于同样线程安全的HashTable来说,服从等各方面都有极大地进步。
ConcurrentHashMap 的上风在于分身性能和线程安全,一个线程进行写操纵时,它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。
ConcurrentHashMap底层数据结构

加锁的方式




JDK1.8中的ConcurrentHashMap比JDK1.7中的ConcurrentHashMap幸亏那里?

JDK1.8为什么使用内置锁synchronized来取代重入锁ReentrantLock,有以下几点:

2.1 ConcurrentHashMap概述

java.util.concurrent.ConcurrentHashMap属于JUC包下的一个聚集类,可以实现线程安全。
ConcurrentHashMap和HashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地点,因此时间复杂度是O(1)。查询非常快。同时,ConcurrentHashMap是线程安全的HashMap。专门用于多线程环境。
JDK1.7
ConcurrentHashMap 接纳分段锁策略,由多个 Segment 组合而成,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操纵的时候给 Segment 赋予了一个对象锁(Put 操纵时,锁的是某个 Segment,其他线程对其他 Segment 的读写操纵均不影响),从而保证多线程环境下并发操纵安全。
不同Segment的并发写入(可以并发实行);同一Segment的一写一读(可以并发实行,table有volatile关键字修饰,保证每次获取值都是最新的);同一Segment的并发写入,会阻塞
ConcurrentHashMap 中每个Segment各自持有一把锁。在保证线程安全的同时低落了锁的粒度,让并发操纵服从更高。
JDK1.8
相比于 JDK1.7 中的 ConcurrentHashMap,JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,接纳 CAS + synchronized 来保证并发安全;数据结构跟jdk1.8中HashMap一样,数组+链表改为 数组+链表+红黑树,当冲突链表长度大于8时,会将链表转变成红黑树结构。
ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 服从又提拔了 N 倍!
2.2 ConcurrentHashMap源码 jdk1.8

组成
  1. //ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
  2. //而HashMap没有使用volatile,  transient Node<K,V>[] table;
  3. transient volatile Node<K,V>[] table;
复制代码
put方法 put()方法没有效synchronized修饰
  1. public V put(K key, V value) {
  2.     return putVal(key, value, false);
  3. }
  4. final V putVal(K key, V value, boolean onlyIfAbsent) {
  5.     // key和value都不能为null
  6.     if (key == null || value == null) throw new NullPointerException();
  7.     int hash = spread(key.hashCode());
  8.     int binCount = 0;
  9.     for (Node<K,V>[] tab = table;;) {  //死循环,可视为乐观锁
  10.         Node<K,V> f; int n, i, fh;
  11.         if (tab == null || (n = tab.length) == 0)
  12.             // 如果tab未初始化或者个数为0,则初始化node数组
  13.             tab = initTable();
  14.         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  15.             if (casTabAt(tab, i, null,
  16.                          new Node<K,V>(hash, key, value, null)))
  17.                 // 如果使用CAS插入元素时,发现已经有元素了,则进入下一次循环,重新操作
  18.                 // 如果使用CAS插入元素成功,则break跳出循环,流程结束
  19.                 break;                   // no lock when adding to empty bin
  20.         }
  21.         else if ((fh = f.hash) == MOVED)
  22.             // 如果要插入的元素所在的tab的第一个元素的hash是MOVED,则当前线程帮忙一起迁移元素
  23.             tab = helpTransfer(tab, f);
  24.         else {   //发生hash冲突
  25.             // 如果这个tab不为空且不在迁移元素,则锁住这个tab(分段锁)
  26.             // 并查找要插入的元素是否在这个tab中
  27.             // 存在,则替换值(onlyIfAbsent=false)
  28.             // 不存在,则插入到链表结尾或插入树中
  29.             V oldVal = null;
  30.             synchronized (f) {
  31.                 // 再次检测第一个元素是否有变化,如果有变化则进入下一次循环,从头来过
  32.                 if (tabAt(tab, i) == f) {
  33.                     // 如果第一个元素的hash值大于等于0(说明不是在迁移,也不是树)
  34.                     // 那就是tab中的元素使用的是链表方式存储
  35.                     if (fh >= 0) {
  36.                         // tab中元素个数赋值为1
  37.                         binCount = 1;
  38.                         // 遍历整个tab,每次结束binCount加1
  39.                         for (Node<K,V> e = f;; ++binCount) {
  40.                             K ek;
  41.                             if (e.hash == hash &&
  42.                                 ((ek = e.key) == key ||
  43.                                  (ek != null && key.equals(ek)))) {
  44.                                 // 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false),并退出循环
  45.                                 oldVal = e.val;
  46.                                 if (!onlyIfAbsent)
  47.                                     e.val = value;
  48.                                 break;
  49.                             }
  50.                             Node<K,V> pred = e;
  51.                             if ((e = e.next) == null) {
  52.                                 // 如果到链表尾部还没有找到元素,就把它插入到链表结尾并退出循环
  53.                                 pred.next = new Node<K,V>(hash, key,
  54.                                                           value, null);
  55.                                 break;
  56.                             }
  57.                         }
  58.                     }
  59.                     else if (f instanceof TreeBin) {
  60.                         // 如果第一个元素是树节点
  61.                         Node<K,V> p;
  62.                         // tab中元素个数赋值为2
  63.                         binCount = 2;
  64.                         // 调用红黑树的插入方法插入元素,如果成功插入则返回null,否则返回寻找到的节点
  65.                         if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
  66.                                                        value)) != null) {
  67.                             // 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false),并退出循环
  68.                             oldVal = p.val;
  69.                             if (!onlyIfAbsent)
  70.                                 p.val = value;
  71.                         }
  72.                     }
  73.                 }
  74.             }
  75.             // 如果binCount不为0,说明成功插入了元素或者寻找到了元素
  76.             if (binCount != 0) {
  77.                 // 如果链表元素个数达到了8,则尝试树化
  78.                 // 因为上面把元素插入到树中时,binCount只赋值了2,并没有计算整个树中元素的个数,所以不会重复树化
  79.                 if (binCount >= TREEIFY_THRESHOLD)
  80.                     treeifyBin(tab, i);
  81.                 // 如果要插入的元素已经存在,则返回旧值
  82.                 if (oldVal != null)
  83.                     return oldVal;
  84.                 // 退出外层大循环,流程结束
  85.                 break;
  86.             }
  87.         }
  88.     }
  89.     // 成功插入元素,元素个数加1(是否要扩容在这个里面)
  90.     addCount(1L, binCount);
  91.     // 成功插入元素返回null
  92.     return null;
  93. }
复制代码
put操纵总结:
   做插入操纵时,首先进入乐观锁,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器;如果已经初始化,则判断该hash位置的节点是否为空,
如果为空,则通过CAS操纵进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则资助其扩容。如果没有扩容,则进行末了一步,先加锁,然后找到hash值相同的那个节点(hash冲突),
循环判断这个节点上的链表,决定做覆盖操纵还是插入操纵。
循环结束,插入完毕。
  
get方法
  1. public V get(Object key) {
  2.     Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  3.     // 计算hash
  4.     int h = spread(key.hashCode());
  5.     // 判断数组是否为空,通过key定位到数组下标是否为空
  6.     if ((tab = table) != null && (n = tab.length) > 0 &&
  7.         (e = tabAt(tab, (n - 1) & h)) != null) {
  8.         // 如果第一个元素就是要找的元素,直接返回
  9.         if ((eh = e.hash) == h) {
  10.             if ((ek = e.key) == key || (ek != null && key.equals(ek)))
  11.                 return e.val;
  12.         }
  13.         else if (eh < 0)
  14.             // hash小于0,说明是树或者正在扩容
  15.             // 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式
  16.             return (p = e.find(h, key)) != null ? p.val : null;
  17.         // 遍历整个链表寻找元素
  18.         while ((e = e.next) != null) {
  19.             if (e.hash == h &&
  20.                 ((ek = e.key) == key || (ek != null && key.equals(ek))))
  21.                 return e.val;
  22.         }
  23.     }
  24.     return null;
  25. }
复制代码
步调如下:

ConcurrentHashMap的get()方法没有加synchronized锁,为什么可以不加锁?因为table有volatile关键字修饰,保证每次获取值都是最新的。
【Hashtable的get(Object key)方法加了synchronized锁,性能较差】
remove方法
  1. public V remove(Object key) {
  2.     // 调用替换节点方法
  3.     return replaceNode(key, null, null);
  4. }
  5. final V replaceNode(Object key, V value, Object cv) {
  6.     // 计算hash
  7.     int hash = spread(key.hashCode());
  8.     // 循环遍历数组
  9.     for (Node<K,V>[] tab = table;;) {
  10.         Node<K,V> f; int n, i, fh;
  11.         //校验参数
  12.         if (tab == null || (n = tab.length) == 0 ||
  13.                 (f = tabAt(tab, i = (n - 1) & hash)) == null)
  14.             break;
  15.         else if ((fh = f.hash) == MOVED)
  16.             // 如果正在扩容中,协助扩容
  17.             tab = helpTransfer(tab, f);
  18.         else {
  19.             V oldVal = null;
  20.             // 标记是否处理过
  21.             boolean validated = false;
  22.             //用 synchronized 同步锁,保证并发时元素移除安全
  23.             synchronized (f) {
  24.                 // 再次验证当前tab元素是否被修改过
  25.                 if (tabAt(tab, i) == f) {
  26.                     if (fh >= 0) {
  27.                         // fh>=0表示是链表节点
  28.                         validated = true;
  29.                         // 遍历链表寻找目标节点
  30.                         for (Node<K,V> e = f, pred = null;;) {
  31.                             K ek;
  32.                             if (e.hash == hash &&
  33.                                     ((ek = e.key) == key ||
  34.                                             (ek != null && key.equals(ek)))) {
  35.                                 V ev = e.val;
  36.                                 if (cv == null || cv == ev ||
  37.                                         (ev != null && cv.equals(ev))) {
  38.                                     oldVal = ev;
  39.                                     if (value != null)
  40.                                         e.val = value;
  41.                                     else if (pred != null)
  42.                                         pred.next = e.next;
  43.                                     else
  44.                                         setTabAt(tab, i, e.next);
  45.                                 }
  46.                                 break;
  47.                             }
  48.                             pred = e;
  49.                             // 遍历到链表尾部还没找到元素,跳出循环
  50.                             if ((e = e.next) == null)
  51.                                 break;
  52.                         }
  53.                     }
  54.                     else if (f instanceof TreeBin) {
  55.                         // 如果是树节点
  56.                         validated = true;
  57.                         TreeBin<K,V> t = (TreeBin<K,V>)f;
  58.                         TreeNode<K,V> r, p;
  59.                         // 遍历树找到了目标节点
  60.                         if ((r = t.root) != null &&
  61.                                 (p = r.findTreeNode(hash, key, null)) != null) {
  62.                             V pv = p.val;
  63.                             if (cv == null || cv == pv ||
  64.                                     (pv != null && cv.equals(pv))) {
  65.                                 oldVal = pv;
  66.                                 if (value != null)
  67.                                     p.val = value;
  68.                                 else if (t.removeTreeNode(p))
  69.                                     setTabAt(tab, i, untreeify(t.first));
  70.                             }
  71.                         }
  72.                     }
  73.                 }
  74.             }
  75.             // 如果处理过,不管有没有找到元素都返回
  76.             if (validated) {
  77.                 // 如果找到了元素,返回其旧值
  78.                 if (oldVal != null) {
  79.                     // 如果要替换的值为空,元素个数减1
  80.                     if (value == null)
  81.                         addCount(-1L, -1);
  82.                     return oldVal;
  83.                 }
  84.                 break;
  85.             }
  86.         }
  87.     }
  88.     // 没找到元素返回空
  89.     return null;
  90. }
复制代码
步调如下:

2.3 ConcurrentHashMap结构 jdk1.7–>jdk1.8

jdk1.7下的ConcurrentHashMap

它由多个 Segment 组合而成。Segment 本身就相当于一个 HashMap 对象。
同 HashMap 一样,Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。
单一的 Segment 结构如下:

像这样的 Segment 对象,在 ConcurrentHashMap 聚集中有多少个呢?有 2 的 N 次方个,共同保存在一个名为 segments 的数组当中。
因此整个ConcurrentHashMap的结构如下:

可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有多少个子哈希表。
它的核心属性
  1. final Segment<K,V>[] segments;
  2. transient Set<K> keySet;
  3. transient Set<Map.Entry<K,V>> entrySet;
复制代码
其中,Segment是它的一个内部类,主要组成如下:
  1. static final class Segment<K,V> extends ReentrantLock implements Serializable {
  2.         private static final long serialVersionUID = 2249069246763182397L;
  3.        
  4.         // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
  5.         transient volatile HashEntry<K,V>[] table;
  6.        
  7.         transient int count;
  8.         transient int modCount;
  9.         transient int threshold;
  10.         final float loadFactor;
  11.         // ...
  12. }
复制代码
HashEntry也是一个内部类,主要组成如下:
  1. static class HashEntry<K,V> {
  2.     final int hash;
  3.     final K key;
  4.     volatile V value;
  5.     volatile HashEntry<K,V> next;
  6.     HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
  7.         this.hash = hash;
  8.         this.key = key;
  9.         this.value = value;
  10.         this.next = next;
  11.     }
  12.     //...
  13. }
复制代码
和 HashMap 的 Entry 基本一样,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。
get方法

put方法

高并发线程安全:Put 操纵时,锁的是某个 Segment,其他线程对其他 Segment 的读写操纵均不影响。因此解决了线程安全问题。
jdk1.8下的COncurrentHashMap

虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢。因此JDK8有所改进:
(在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于8时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询服从方面,又大大的进步了不少。)
1)结构改变

首先是结构上的变化,相比于 JDK1.7 中的 ConcurrentHashMap,JDK1.8 中 ConcurrentHashMap 取消了 Segment 分段锁,接纳 CAS + synchronized 来保证并发安全;数据结构跟jdk1.8中HashMap一样,数组+链表改为 数组+链表+红黑树,当冲突链表长度大于8时,会将链表转变成红黑树结构。

ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 服从又提拔了 N 倍!
2)HashEntry 改为 Node

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2.     final int hash;
  3.     final K key;
  4.     volatile V val;
  5.     volatile Node<K,V> next;
  6. }
复制代码
和 JDK7 的 HasEntry 作用相同,对 val 和 next 都用了 volatile 关键字,保证了可见性。
3)Put操纵的变化


4)Get操纵的变化


为什么取消分段锁,分段锁有什么问题

2.4 ConcurrentHashMap总结

底层数据结构

加锁的方式

JDK1.7
ConcurrentHashMap 接纳分段锁策略,由多个 Segment 组合而成,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操纵的时候给 Segment 赋予了一个对象锁(Put 操纵时,锁的是某个 Segment,其他线程对其他 Segment 的读写操纵均不影响),从而保证多线程环境下并发操纵安全。
ConcurrentHashMap 中每个Segment各自持有一把锁。在保证线程安全的同时低落了锁的粒度,让并发操纵服从更高。
JDK1.8
相比于 JDK1.7 中的 ConcurrentHashMap,JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,接纳 CAS + synchronized 来保证并发安全;数据结构跟jdk1.8中HashMap一样,数组+链表改为 数组+链表+红黑树,当冲突链表长度大于8时,会将链表转变成红黑树结构。
ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 服从又提拔了 N 倍。
3.HashMap与ConcurrentHashMap工作原理、区别、总结

3.1 HashMap与ConcurrentHashMap区别


3.2 工作原理

3.2.1 HashMap

HashMap的工作原理、底层数据结构 可以查看 Java面试题:ArrayList底层实现原理、HashMap的实现原理、HashMap的jdk1.7和jdk1.8有什么区别
HashMap的数据结构: 底层使用hash表数据结构,即数组和链表或红黑树。jdk1.7使用的是 数组+链表,jdk1.8 当链表长度大于阈值(默认为8)并且数组长度达到64时 会转换为红黑树
初始容量:HashMap 的初始容量是 0,这是一种懒加载机制,直到第一次 put 操纵才会初始化数组大小,默认大小是 16。
扩容逻辑:
   HashMap 使用的是拉链法来解决散列冲突,扩容并不是必须的,但是不扩容的话会造成拉链的长度越来越长,导致散列表的时间复杂度会倾向于 O(n) 而不是 O(1)。
  HashMap 扩容的触发时机出现在元素个数超过阈值(容量 * loadFactor)的时候时,会将聚集的一维数组扩大一倍,然后重新盘算每个元素的位置。
  

留意:链表的长度大于8 且 数组长度大于64转换为红黑树
3.2.2 ConcurrentHashMap

JDK1.7
   ConcurrentHashMap 接纳分段锁策略,由多个 Segment 组合而成,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操纵的时候给 Segment 赋予了一个对象锁(Put 操纵时,锁的是某个 Segment,其他线程对其他 Segment 的读写操纵均不影响),从而保证多线程环境下并发操纵安全。
  ConcurrentHashMap 中每个Segment各自持有一把锁。在保证线程安全的同时低落了锁的粒度,让并发操纵服从更高。
  JDK1.8
   相比于 JDK1.7 中的 ConcurrentHashMap,JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,接纳 CAS + synchronized 来保证并发安全;数据结构跟jdk1.8中HashMap一样,数组+链表改为 数组+链表+红黑树,当冲突链表长度大于8时,会将链表转变成红黑树结构。
  ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 服从又提拔了 N 倍。
  细节如下:
底层数据结构

加锁的方式




4.HashMap与Hashtable的区别

Hashtable和HashMap都是 基于hash表实现的K-V结构的聚集,Hashtable是jdk1.0引入的一个线程安全的聚集类,内部使用数组+链表的形式来实现
从功能特性的角度来说
1、Hashtable是线程安全的(HashTable 对每个方法都增长了 synchronized),而HashMap不是
2、HashMap的性能要比Hashtable更好,因为Hashtable接纳了全局同步锁来保证安全性,对性能影响较大
从内部实现的角度来说
1)Hashtable使用数组加链表,HashMap JDK1.7数组+链表、JDK1.8 数组+链表+红黑树
2)HashMap初始容量是16,Hashtable初始容量是11
3)HashMap可以使用null作为key;而Hashtable不允许 null 作为 Key,会抛出NullPointerException异常
他们两个的key的散列算法不同:Hashtable直接是使用key的hashcode对数组长度取模;而HashMap对key的hashcode做了二次散列,从而制止key的分布不均匀影响到查询性能
5.HashMap、Hashtable、ConcurrentHashMap区别

HashMap、Hashtable、ConcurrentHashMap都是 基于hash表实现的K-V结构的聚集,在线程安全、底层数据结构方面有所区别

6.为什么 HashMap 接纳拉链法而不是开放地点法?

Java 给予 HashMap 的定位是一个相对通用的散列表容器,它应该在面对各种输入的时候都体现稳固。而开辟地点法相对来说容易出现数据堆积,在数据量较大时大概出现连续冲突的环境,性能不敷稳固。
我们可以举个反例,在 Java 原生的数据结构中,也存在使用开放地点法的散列表 —— 就是 ThreadlLocal。因为项目中不会大量使用 ThreadLocal 线程局部存储,所以它是一个小规模数据场景,这里使用开辟地点法是没问题的。
7.Map总结

实现类数据结构是否线程安全key是否可为null是否有序HashMap哈希表结构,jdk1.7 数组+链表,jdk1.8 数组+链表+红黑树否是否ConcurrentHashMap哈希表结构,jdk1.7 数组+链表,jdk1.8 数组+链表+红黑树是否否Hashtable哈希表结构,数组+链表是否否LinkedHashMap继承自HashMap,数组+链表+红黑树+双重链接列表否是是TreeMap红黑树否否是 参考 黑马程序员相干视频与条记、HashMap源码分析 —— 一篇文章搞定HashMap面试、HashMap为什么线程不安全、一文彻底弄懂ConcurrentHashMap,轻松应对面试官!、深入浅出ConcurrentHashMap详解、HashMap与ConcurrentHashMap工作原理、区别和总结

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4