那么要如何保证HashMap的线程安全呢? 方法有不少,好比使用Hashtable或者Collections.synchronizedMap,可是这两位选手都有一个共同的问题:性能。由于不论是读仍是写操做,他们都会给整个集合上锁,致使同一时间的其余操做被阻塞。数据库
虽然Hashtable和Collections.synchronizedMap解决了HashMap的线程不安全的问题,可是带来了运行效率不佳的问题。 数组
基于以上所述,兼顾了线程安全和运行效率的ConcurrentHashMap就出现了。安全
在了解了HashMap以后,接下来就开始了解一下ConcurrentHashMap。
ConcurrentHashMap与HashMap相比,最关键的是要理解一个概念:segment。
Segment其实就是一个Hashmap 。Segment也包含一个HashEntry数组,数组中的每个HashEntry既是一个键值对,也是一个链表的头节点。
Segment对象在ConcurrentHashMap集合中有2的N次方个,共同保存在一个名为segments的数组当中。(类比HashMap来理解Segment就好)markdown
所以ConcurrentHashMap的结构为:
换言之,ConcurrentHashMap是一个双层哈希表。在一个总的哈希表下面,有若干个子哈希表。(这样的双层结构,相似于数据库水平拆分来理解)多线程
ConcurrentHashMap如此的设计,优点主要在于:
每一个segment的读写是高度自治的,segment之间互不影响。这称之为“锁分段技术”;并发
看一下并发状况下的ConcurrentHashMap:
情景一:不一样segment的并发写入性能
不一样的Segment是能够并发执行put操做的.net
情景二:同一segment的并发写入
由于segment的写入是上锁的,所以对 同一segment的并发写入会被阻塞;线程
情景三:同一segment的一写一读
同一segment的写和读是能够并发执行的;设计
看到此处,就已经知道了ConcurrentHashMap的并发状况,有兴趣的话能够继续看下ConcurrentHashMap的具体读写过程。
Get方法:
1.为输入的Key作Hash运算,获得hash值。
2.经过hash值,定位到对应的Segment对象
3.再次经过hash值,定位到Segment当中数组的具体位置。
Put方法:
1.为输入的Key作Hash运算,获得hash值。
2.经过hash值,定位到对应的Segment对象
3.获取可重入锁
4.再次经过hash值,定位到Segment当中数组的具体位置。
5.插入或覆盖HashEntry对象。
6.释放锁。
看到此处,对于ConcurrentHashMap的Get和Put的过程(读写过程)就有了一个完整的了解了。
基于上述,会有一个问题:
每个segment各自持有锁,那么在调用size()方法的时候(size()在实际开发大量使用),怎么保持一致性呢?
详细描述一下上面问题的情景:
Size方法的目的是统计ConcurrentHashMap的总元素数量, 确定要把每一个segment内部的元素数量都加起来。
那么假设一种状况,在统计segment元素数量的过程当中,在统计结束前,已统计过的segment插入了新的元素,size()返回的数量就会出现不一致的问题。
为解决这个问题,ConcurrentHashMap的Size()方法是经过一个嵌套循环解决的,大致过程以下:
1.遍历全部的Segment。
2.把Segment的元素数量累加起来。
3.把Segment的修改次数累加起来。
4.判断全部Segment的总修改次数是否大于上一次的总修改次数。若是大于,说明统计过程当中有修改,从新统计,尝试次数+1;若是不是。说明没有修改,统计结束。
5.若是尝试次数超过阈值,则对每个Segment加锁,再从新统计。
6.再次判断全部Segment的总修改次数是否大于上一次的总修改次数。因为已经加锁,次数必定和上次相等。
7.释放锁,统计结束。
这种解决办法是否是似曾相识?没错,这种思想和乐观锁悲观锁的思想一模一样(不熟悉乐观锁的道友能够看我转的一篇很是生动的介绍,传送门)
为了避免锁全部segment,首先乐观地假设size过程当中不会有修改。当尝试必定次数,才无奈转悲观,锁住全部segment以保证一致性。
补充:
一、以上都是基于Java1.7的ConcurrentHashMap原理和代码;
二、ConcurrentHashMap在对Key求Hash值的时候进行了两次Hash,目的是为了实现Segment均匀分布。
说了那么多,针对Map子类的安全性能够总结以下几点: