jdk8
总结
1、ConcurrentHashMap,是Java并发包中自JDK1.5后提供的一个线程平安且高效的HashMap实现,能够用来代替HashTable。间接实现了ConcurrentMap接口,同时继承了AbstractMap抽象类。
2、JDK1.8后,ConcurrentHashMap应用CAS算法联合synchronized同步锁的形式保障线程平安。
3、ConcurrentHashMap的存储构造与HashMap基本一致,应用外部子类Node作为根本单元,存储链表节点数据,应用外部子类TreeNode存储树节点数据。同时减少了几个子类节点对象:ForwardingNode(转发节点)、TreeBin(红黑树根节点)、ReservationNode(保留节点)
UML
性能介绍
根本信息
包门路:java.util.concurrent
**
阐明:ConcurrentHashMap,是Java并发包中自JDK1.5后提供的一个线程平安且高效的HashMap实现,能够用来代替HashTable。间接实现了ConcurrentMap接口,同时继承了AbstractMap抽象类。
它沿用了与它同期间的HashMap版本的思维,(jdk1.8)底层仍然由“数组”+链表+红黑树的形式。然而为了做到并发,又减少了很多辅助的类,例如TreeBin,Traverser等对象外部类。
特点:1、线程平安【JDK1.7之前应用分段锁实现,JDK1.8开始应用CAS算法实现】
2、不反对null的key和value
ConcurrentMap是如何保障线程平安的?
咱们晓得,HashTable通过synchronized同步锁保障线程平安,同一时间只有有一个线程操作某个数据,就会锁定整个哈希表,其余线程就无奈对HashTable进行任何操作,只能期待该线程执行结束或开释锁,那这种形式其实是很不敌对且效率很低的。
分段锁
于是ConcurrentHashMap在JDK1.7应用了“分段锁”这种机制,线程拜访数据时锁定一段范畴的数据,这样在容器内就会存在多个锁定区间的锁(相似数据库的“间隙锁”),每一把锁锁一段数据,这样在多线程拜访时不同段的数据时,就不会存在锁竞争了,这样便能够无效地进步并发效率
在ConcurrentHashMap中,Segment是一个类,实际上每一个segment都是一个HashEntry<K,V>[] table, table中的每一个元素实质上都是一个HashEntry的单向队列。
每一个Segment都领有一个锁,当进行写操作时,只须要锁定一个Segment,而其它Segment中的数据是能够拜访的。实质上Segment类就是一个小的hashmap,外面table数组存储了各个节点的数据
因为Segment继承ReentrantLock,所以在put时通过ReentrantLock的tryLock()办法尝试去获取锁,如果获取胜利就直接插入相应的地位,如果曾经有线程获取该Segment的锁,那以后线程会以自旋的形式(自旋就是一个循环获取锁的过程)持续调用tryLock()办法去获取锁,超过指定次数就挂起,期待唤醒
CAS+synchonized
JDK1.8版本则做了2点批改
1、将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的构造.(与HashMap的变动基本一致)
2、勾销segments字段,间接采纳transient volatile HashEntry<K,V>[] table保留数据,采纳table数组元素作为锁,从而实现了对每一行数据进行加锁,并发管制应用synchronized和CAS来操作
synchronized只锁定以后链表或红黑树的首节点,这样只有哈希不抵触(不操作同一地位元素),就不会产生并发,效率又晋升很多。
CAS(Compare And Swap,比拟替换)算法,它蕴含三个参数 CAS(V, E, N): V 示意要更新的变量, E 示意预期值, N 示意新值。根本思维就是一直地去比拟以后内存中的变量值与你指定的一个变量值(预期值)是否相等,如果相等,则承受你指定的批改的值(新值),否则证实曾经有别的线程批改过该变量的值,回绝你的操作。因为以后线程中的值曾经不是最新的值,你的批改很可能会笼罩掉其余线程批改的后果。这一点与乐观锁,SVN的思维是比拟相似的。
本文基于JDK1.8进行编写
简略应用
package com.java.map;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/** * @description * @date: 2021-05-29 23:07 */public class ConcurrentHashMapCode { public static void main(String[] args) { Map<String,Object> concurrentHashMap = new ConcurrentHashMap<>(); concurrentHashMap.put("one",1); concurrentHashMap.put("two", 2); System.out.println(concurrentHashMap); // 如果传入key对应的value曾经存在,就返回存在的value,不进行替换 concurrentHashMap.putIfAbsent("one", 2); concurrentHashMap.putIfAbsent("three", 3); System.out.println(concurrentHashMap); }}
输入:
{one=1, two=2}{one=1, two=2, three=3}
思考
ConcurrentHashMap与HashMap的比拟
线程安全性
- HashMap不是线程平安的,多线程并发下扩容可能会导致数据笼罩的状况。
- ConcurrentHashMap线程平安,在ConcurrentHashMap中,大量应用了U.compareAndSwapXXX的办法,这个办法是利用一个CAS算法实现无锁化的批改值的操作,他能够大大降低锁代理的性能耗费。同时,在ConcurrentHashMap中还定义了三个原子操作,用于对指定地位的节点进行操作。这三种原子操作被宽泛的应用在ConcurrentHashMap的get和put等办法中。
- 咱们能够发现JDK8中ConcurrentHashMap的实现应用的是锁拆散思维,只是锁住的是一个node,而锁住Node之前的操作是基于在volatile和CAS之上无锁并且线程平安的。
null值
- HashMap容许key和value为空
- ConcurrentHashMap不容许
迭代
- HashMap在用iterator遍历的同时,不容许批改HashMap;
- ConcurrentHashMap容许该行为,并且更新对后续的遍历是可见的;
扩容机制不同
HashMap的扩容机制
HashMap的扩容,个别都蕴含两个步骤:
① table数组的扩容
table数组的扩容,个别就是新建一个2倍大小的槽数组,这个过程通过由一个单线程实现,且不容许呈现并发。
② 数据迁徙
所谓数据迁徙,就是把旧table中的各个槽中的结点重新分配到新table中。比方,单线程状况下,能够遍历原来的table,而后put到新table中。
这一过程通常波及到槽中key的rehash,因为key映射到桶的地位与table的大小无关,新table的大小变了,key映射的地位个别也会变动。
ConcurrentHashMap的扩容机制
ConcurrentHashMap在解决rehash的时候,并不会从新计算每个key的hash值,而是利用了一种很奇妙的办法。
ConcurrentHashMap外部的table数组的大小必须为2的幂次,起因是让key均匀分布,缩小抵触,这只是其中一个起因。
另一个起因就是:当table数组的大小为2的幂次时,通过key.hash & table.length-1
这种形式计算出的索引i
,当table扩容后(2倍),新的索引要么在原来的地位i
,要么是i+n
。
咱们来看个例子:
上图中:
扩容前,table数组大小为16,key1和key2映射到同一个索引5;
扩容后,table数组的大小变成 2*16=32 ,key1的索引不变,key2的索引变成 5+16=21。
而且还有一个特点,扩容后key对应的索引如果产生了变动,那么其变动后的索引最高位肯定是1(见扩容后key2的最高位)。
这种解决形式十分利于扩容时多个线程同时进行的数据迁徙操作,因为旧table的各个桶中的结点迁徙不会相互影响,所以就能够用“分治”的形式,将整个table数组划分为很多局部,每一部分蕴含肯定区间的桶,每个数据迁徙线程解决各自区间中的结点,对多线程同时进行数据迁徙十分无利。
看到这里就点个赞吧分享更多技术文章,去帮忙更多的人,这里有我所有知识库哟~ https://www.yuque.com/yingwenerjie