学习了HashMap知识点,了解到HashMap不是线程安全的,因为HashMap在高并发情况下可能会出现环形链表,造成下一次读 *** 作形成死循环。所以,要使用ConcurrentHashMap线程安全的方法。
不多说了直接学习。每天进步一点点,加油。
- 一、ConcurrentHashMap是什么?
- 二、ConcurrentHashMap方法部分源码
- 1.put()方法源码
- 2.get()方法的源码
- 三、可能碰到的一些问题
- 有了解过ConcurrentHashMap吗?它的存储结构是什么样的?
- ConcurrentHashMap1.7和1.8的区别
- 1.8版本ConcurrentHashMap使用什么技术来保证线程安全?
- ConcurrentHashMap默认初始容量是多少?
- ConCurrentHashmap 的key,value是否可以为null。
- ConcurrentHashMap有哪些构造函数?
- ConCurrentHashmap 每次扩容是原来容量的几倍
- ConcurrentHashMap的get方法是否要加锁,为什么?
- java1.8中,ConCurrentHashmap是如何计算它的size大小的?
一、ConcurrentHashMap是什么?
在面试的过程面试问完HashMap还会问安全的方法有哪些?HashMap 是线程不安全的集合类,在并发情况下可能由于线程争用导致程序获取不正确的结果。而ConcurrentHashMap 是对 HashMap 的功能增强,使 HashMap 支持高并发下的读写线程安全。看了ConcurrentHashMap源码发现很多方法和代码跟HashMap相似。
只是思路相同,实现不同而已。
public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { // 不允许插入空值或空键 // 允许value空值会导致get方法返回null时有两种情况: // 1. 找不到对应的key2. 找到了但是value为null; // 当get方法返回null时无法判断是哪种情况,在并发环境下containsKey方法已不再可靠, // 需要返回null来表示查询不到数据。允许key空值需要额外的逻辑处理,占用了数组空间,且并没有多大的实用价值。 // HashMap支持键和值为null,但基于以上原因,ConcurrentHashMap是不支持空键值。 if (key == null || value == null) throw new NullPointerException(); // 高低位异或扰动hashcode,和HashMap类似 int hash = spread(key.hashCode()); // bincount表示链表的节点数 int binCount = 0; // 尝试多种方法循环处理,后续会有很多这种设计 for (Node[] tab = table;;) { Node f; int n, i, fh; // 情况一:如果数组为空则进行初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); // 情况二:目标下标对象为null else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 重点:采用CAS进行插入 if (casTabAt(tab, i, null,new Node (hash, key, value, null))) break; } // 情况三:数组正在扩容,帮忙迁移数据到新的数组 // 同时会新数组,下次循环就是插入到新的数组 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 情况四:直接对节点进行加锁,插入数据 // 下面代码很多,但逻辑和HashMap插入数据大同小异 // 因为已经上锁,不涉及并发安全设计 else { V oldVal = null; // 同步加锁 synchronized (f) { // 重复检查一下刚刚获取的对象有没有发生变化 if (tabAt(tab, i) == f) { // 链表处理情况 if (fh >= 0) { binCount = 1; // 循环链表 for (Node 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; } Node pred = e; // 若未找到则插在链表尾 if ((e = e.next) == null) { pred.next = new Node (hash, key, value, null); break; } } } // 红黑树处理情况 else if (f instanceof TreeBin) { Node p; binCount = 2; if ((p = ((TreeBin )f).putTreeval(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } else if (f instanceof ReservationNode) throw new IllegalStateException("Recursive update"); } } // 判断是否需要转化为红黑树,和返回旧数值 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 总数+1 addCount(1L, binCount); return null; }
从上面的源码可以看出ConcurrentHashMap 不支持使用 null 作为 key 或者 value。
ConcurrentHashMap添加数据时,采取了CAS+synchronize结合策略。首先会判断该节点是否为null,如果为null,尝试使用CAS添加节点;如果添加失败,说明发生了并发冲突,再对节点进行上锁并插入数据。在并发较低的情景下无需加锁,可以显著提高性能。同时只会CAS尝试一次,也不会造成线程长时间等待浪费cpu时间的情况。ConcurrentHashMap的put方法整体流程如下(并不是全部流程):
流程如下:
1.首先会判断数组是否已经初始化,如若未初始化,会先去初始化数组; 2.如果当前要插入的节点为null,尝试使用CAS插入数据; 3.如果不为null,则判断节点hash值是否为-1;-1表示数组正在扩容,会先去协助扩容,再回来继续插入数据。 4.最后会执行上锁,并插入数据,最后判断是否需要返回旧值; 5.如果不是覆盖旧值,需要更新map中的节点数,也就是图中的addCount方法。
注意到源码中有两个关键方法:初始化数组的initTable(),修改map中节点总数的addCount。是put插入的关键方法。
2.get()方法的源码public V get(Object key) { Node[] tab; Node e, p; int n, eh; K ek; int h = spread(key.hashCode()); 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; } } return null; }
通过上面的get()方法源码可以看出,首先要计算 hash 值,然后再根据hash 值找到数组对应位置: (n - 1) & h。最后根据该位置处结点性质进行相应找。如果该位置为 null,那么直接返回 null 。如果该位置处的节点刚好就是需要的值,就返回该节点的值。如果该位置节点的 hash 值小于 0,说明容量不足正在扩容,或者是红黑树进行遍历对比查询数据。
三、可能碰到的一些问题 有了解过ConcurrentHashMap吗?它的存储结构是什么样的?
作为Java开发工程师一定要学习和查看ConcurrentHashMap源码数据结构知识点,
ConcurrentHashMap数据结构跟HashMap一样数组+链表+红黑树实现的。
1.7 版本数组+链表,Segment+HashEntry。锁的粒度基于segment,包含多个HashEntry。锁的粒度比较大。
1.8 版本数组+链表+红黑树(Node+CAS+Synchronized),锁的粒度就是node,大大降低了锁的粒度。
采用了Node+CAS+Synchronized锁的方式来保证线程安全
ConcurrentHashMap默认初始容量是多少?1.8版本中ConcurrentHashMap默认初始容量是16,可以根据业务需要设置容量大小,不设置就是取默认值。
ConCurrentHashmap 的key,value是否可以为null。都不可以为null ,如果Key或者Value为null会报空指针异常。
ConcurrentHashMap有哪些构造函数?有五个构造函数,分别是:无参构造函数,可传入容器大小的构造函数,可传入Map的构造函数,可设置阈值和初始容量的构造函数,可设置初始容量和阈值并发级别等五种,具体源码可查看以下部分
//无参构造函数 public ConcurrentHashMap() { } //可传初始容器大小的构造函数 public ConcurrentHashMap(int initialCapacity) { if (initialCapacity < 0) throw new IllegalArgumentException(); int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); this.sizeCtl = cap; } //可传入map的构造函数 public ConcurrentHashMap(Map extends K, ? extends V> m) { this.sizeCtl = DEFAULT_CAPACITY; putAll(m); } //可设置阈值和初始容量 public ConcurrentHashMap(int initialCapacity, float loadFactor) { this(initialCapacity, loadFactor, 1); } //可设置初始容量和阈值和并发级别 public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (initialCapacity < concurrencyLevel) // Use at least as many bins initialCapacity = concurrencyLevel; // as estimated threads long size = (long)(1.0 + (long)initialCapacity / loadFactor); int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); this.sizeCtl = cap; }ConCurrentHashmap 每次扩容是原来容量的几倍
每次扩容是原来的2倍
在transfer方法里面会创建一个原数组的俩倍的node数组来存放原数据。
不需要,get方法采用了unsafe方法来保证线程安全
java1.8中,ConCurrentHashmap是如何计算它的size大小的?对于size的计算,在put()的时候使用了addCount()方法。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)