ConcurrentHashMap是JUC包中提供的一个并发集合类,是线程安全且高效的HashMap(该文章有对HashMap的详细介绍,结构方面二者相似)实现。
之所以引入ConcurrentHashMap,是因为
- HashMap线程不安全:在多线程环境下,使用HashMap的put操作会引起死循环,原因是多线程会导致HashMap的Entry链表形成环形数据结构,导致Entry的next节点永远不为空,就会产生死循环获取Entry。(HashMap的key,value均可为null,其他两个不行。)
- HashTable效率低下:HashTable容器使用sychronized来保证线程安全,采取锁住整个表结构来达到同步目的,在线程竞争激烈的情况下,当一个线程访问HashTable的同步方法,其他线程也访问同步方法时,会进入阻塞或轮询状态;如线程1使用put方法时,其他线程既不能使用put方法,也不能使用get方法,效率非常低下。
-
ConcurrentHashMap的锁分段技术可提升并发访问效率:首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
以下介绍JDK8中的ConcurrentHashMap
结构:
和 1.8 HashMap 结构类似,当链表节点数超过指定阈值的话,也是会转换成红黑树的,大体结构也是一样的。即先是链表,后为红黑树
put:
- 判断Node[]数组是否初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
- 检查到内部正在扩容,就帮助它一块扩容。
- 如果f!=null(链表/红黑树的头元素),则使用synchronized锁住f元素。如果是Node(链表结构)则执行链表的添加操作;如果是TreeNode(树型结构)则执行树添加操作。
- 判断链表长度已经达到临界值(默认值),当节点超过这个值就需要把链表转换为树结构。
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
扩容:
通过判断该节点的hash值是不是等于-1(MOVED),代码为(fh = f.hash) == MOVED,说明 Map 正在扩容。那么就帮助 Map 进行扩容。以加快速度。这里重点是多线程并发扩容,可以大大加快扩容速度。
关键方法:
ConcurrentHashMap支持Java8中的lambda表达式,对代码进行了简化。
computeIfAbsent:如果key不存在,则调用后面的函数式接口计算,把计算后的val作为值。
public class ConcurrentHashMapDemo { private static final ConcurrentHashMap<String, Integer> USER_ACCESS_COUNT = new ConcurrentHashMap<>(); public static void main(String[] args) { //如果key不存在,则调用后面的函数式接口计算,把计算后的val作为值 USER_ACCESS_COUNT.computeIfAbsent("cc", k -> 1); System.out.println(USER_ACCESS_COUNT.get("cc")); } }
computeIfPresent:如果key存在则修改并返回value,如果不存在则返回null。
public class ConcurrentHashMapDemo { private static final ConcurrentHashMap<String, Integer> USER_ACCESS_COUNT = new ConcurrentHashMap<>(); public static void main(String[] args) { //如果key存在则修改,如果不存在则返回null System.out.println(USER_ACCESS_COUNT.computeIfPresent("cc", (k, v) -> v + 1)); } }
merge(合并数据)
如果指定的键尚未与(非空)值相关联,则将其与给定值相关联。
-
key - 与指定值关联的键
-
value - 如果不存在则使用的值
-
remappingFunction - 重新计算值(如果存在)的函数
public class ConcurrentHashMapDemo { private static final ConcurrentHashMap<String, Integer> USER_ACCESS_COUNT = new ConcurrentHashMap<>(); //merge public static void main(String[] args) { ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap(); Stream.of(1, 2, 3, 4, 6, 2, 3, 6, 8, 1).forEach(v -> { concurrentHashMap.merge(v, 5, Integer::sum); }); System.out.println(concurrentHashMap); } }
//输出结果:{1=10, 2=10, 3=10, 4=5, 6=10, 8=5}
在JDK1.7和JDK1.8中的区别:
在JDK1.8主要设计上的改进有以下几点:
1、不采用segment而采用node,锁住node来实现减小锁粒度。
2、设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
3、使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
4、sizeCtl的不同值来代表不同含义,起到了控制的作用。
采用synchronized而不是ReentrantLock:
1. 减少内存开销
假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
2. 获得JVM的支持
可重入锁毕竟是API这个级别的,后续的性能优化空间很小。
synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。
参考文章:https://blog.csdn.net/mrlin6688/article/details/104646661
https://blog.csdn.net/qq_41432730/article/details/121318157