来,用hashmap的结构图开头如何?
在jdk1.8中如果链表长度达到8就会改为红黑树+数组的结构
下面进入正题:
ConcurrentHashMap、HashMap、HashTable的区别:
1、HashMap:线程不安全,底层结构(jdk1.7:数组+链表;jdk1.8:数组+链表,数组+红黑树)
2、HashTable:线程安全(依赖synchronized关键字对整个结构加锁),底层结构与HashMap相同,Key和Vlaue值都不可以为null
3、ConcurrentHashMap:线程安全(jdk1.7:Segment分段锁;jdk1.8:采用volatile关键字定义链表头部,采用final、CAS等方式进行无锁操作),底层结构与HashMap类似,只不过jdk1、7中有Segment分段结构
恭喜你中奖啦,因为我还准备往下说:
一、HashMap
-
HashMap线程不安全的原因
在多线程下,使用HashMap进行put操作可能会引发死循环,导致CPU利用率接近100% -
final HashMap<String,String> map = new HashMap<String,String>(2); Thread t = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < 10000; i++){ new Thread(new Runnable(){ @Override public void run() { map.put(UUID.randomUUID().toString(),""); } },"ftf"+i).start(); } } },"ftf"); t.start(); t.join();
HashMap在并发执行put操作时会引发死循环,是因为多线程同时进行put操作,当需要扩容的时候会新生成一个数组,然后将原来数组中链表中的节点重新进行hash计算,然后连接,就有可能导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环Entry。
-
HashMap结构
先不多说放一个图,你还不明白,请打我- 图中左边蓝色部分是table数组部分,右侧是真正的数据链部分
- 在jdk1.7前所有的数据链就想上半部分黄色链表部分一样
- 为应对数据增对链表查询速度问题,jdk1.8之后采用“数组+链表”和“数组+红黑树”并用的方式,而他们的节点就是链表长度大于8
-
Hash速度为什么那么快?
它为什么这么快呢?这得从他的插入说起- 一个原本就有几个数据的数组table(keyvalue数据),接下来插入key9的数据点
- 首先获得key9的hash值,然后进行除余,得到余数之后选择对应数组下标的链表
- 利用头插法进行插入节点
- 这样的话整个插入操作的时间复杂度仅为O(1),似不似很强
说完插入啦,看一下查询到底如何?
根据上述的插入的过程已经看到插入前要先查询到节点所在链表在数组中的位置(时间复杂度O(1))
找到了链表头节点,下面对链表进行遍历,我们知道链表的查询时间复杂度为O(n),不过这里要说,这里的n可不是hashmap数据的个数,是某一个链表上的节点个数,如果总的数据很多,数组的长度随之扩容,那么这样的n也就不够塞牙缝的啦
jdk8中链表超过8就编程了红黑树,我们知道其查询时间复杂度为O(N),这里的N是红黑树的高度,就算有几百个数据也不过几层它的查询速度比链表很快(这个也是不用说,如果不快那它不是白改啦)
总的来说hashMap的查询的时间复杂度为O(1)
关于删除和修改
删除和修改无非就是先找到这个数据节点,然后把它修改一点或者删啦,
修改的话没啥说的,所以修改的时间负责度也是O(1)
但是删除就不一定啦
- 当为链表时直接删除即可,
- 如果是红黑树的话
- 不需要调整则直接删除
- 需要调整的话就需要进行旋转或者标色操作,当然设计红黑树的目的就是减少平衡树频繁的旋转修改操作对性能造成影响,所以红黑树的调整次数会大幅度减少
- 这里想说HashMap在用于读多写少的情况下非常得心应手,几个亿的数据量也不过是小儿科,不过需要注意的是:如此大的数据量理应在程序启动时全部添加完毕,后续多用于读操作,他的优势就明显的很啦
二、ConcurrentHashMap
-
线程安全原理不同版本(jdk1.7、jdk1.8)比较
-
jdk1.7中采用Segment分段锁保证线程安全
-
首先将数据分为一段一段的存储,然后每一段数据配备一把锁,当一个线程占用锁访问其中一段数据的时候,其它段的数据也能被其他线程访问。
-
Segment是一种可重入锁
-
一个ConcurrentHashMap中包含一个Segment数组,Segment的结构和HashMap类似,是一种数组+链表结构,一个Segment里包含一个HashEntry数组,每个HashMap是一个链表结构的元素
-
每一个Segment守护这一个HashEntry数组里的元素,这个数组中的任何一个元素需要修改时,首先获得他对应的Segment锁,获取锁啦,后面就不用说啦,就算是村长来也得等
-
-
jdk1.8中采用volatile 、final关键字及CAS操作保证原子性操作
-
我们知道1.8的时候ConcurrentHashMap的结构和HashMap是差不多的,数组维护了一个Node<K,V>数组,数组中就是链表结构的头部
-
无论是数组还是数组中的每一个键值对都用volatile(Java 内存模型中的可见性、原子性和有序性)、final关键字进行修饰,保证数据操作原子性从而保证线程安全
-
-
-
底层存储结构不同版本(jdk1.7、jdk1.8)比较
看了上面两个图片,我又想到一个事儿:- 他们两个的查询仿佛有点差别
- 在jdk1.7中类似于进行了两次分组,第一次进行hash值除余分配Segment分组位置,然后再进行一次除余计算在Segment中Node<K,V>数组中的位置(需要两次hash)
- 在jdk1.8中,呀,这不就跟HashMap一样了吗(需要一次hash)
没啦!回家吧 ┗( ▔, ▔ )