HashMap线程不安全,怎么解决?
- Collections.synchronizedMap(map)
- Hashtable
-
Collections.synchronizedMap怎么实现线程安全的?
内部维护了一个普通对象Map,和一个排斥所mutex
所有操作map的时候都会加synchronized锁。如果传入mutex参数,则对象排斥锁赋值为当前对象。
HashTable类似,加synchronizedHashtable和HashMap的区别
Hashtable不允许键或值为null, HashMap允许
为什么?
Hashtable使用安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。 如果使用null值,就无法判断对应的key是不存在还是为空,因为你无法再调用一次containsKey(key)来判断key是否存在进行判断,ConcurrentHashMap同理。
实现方式不同
hashtable是继承Dictionary类,HashMap是继承AbstractMap
初始化容量不同
HashMap是16, Hashtable是11, 两者的负载因子默认都是0.75
扩容机制不同
当现有容量大于总容量*负载因子的时候,HashMap是2倍扩容,Hashtable是当前容量翻倍+1
迭代器不同
hashMap的Interator是fast-fail, hashtable的Enumertator不是fast-fail. 所以,当其他线程修改了HashMap的结构,如增删,会报错ConcurrentModificationException, 而Hashtable不会
fail-fast是啥?
快速失败是集合里的一种机制,在用迭代器去遍历一个对象的时候,如果遍历过程中对集合对象的内容做了修改(增加、删除、修改),则会抛出ConcurrentModificationException.
原理是啥?
迭代器在遍历时直接访问集合中的内容,并在遍历过程中使用一个modCount变量。集合在遍历期间如果内容发生变化,就会改变modCount的值。每个迭代器使用hasNext()/next()遍历下一个元素的时候,都会检测modCount是否为expectedmodCount,是的话就返回遍历,否则抛出异常,终止遍历。
ConcurrentHashMap
数据结构是啥?为啥并发度高?
1.7 底层是数组+链表。
1.7 是由Segment数组、HashEntry组成。和HashMap一样,数组+链表。
HashEntry和HashMap差不多,不同点是使用volatile去修饰了它的数据value还有下一个节点next。
并发度高的原因:
分段锁技术,Segment继承于ReentrantLock。每个线程占用锁访问一个Segment的时候,不会影响到其他的Segment. 如果容量大小是16,那它的并发度就是16,可以允许16个线程操作16个Segment而且是线程安全的。
put方法
第一步会尝试获得锁,如果失败说明存在其他线程竞争,自旋
自旋到最大次数则改为阻塞锁获取,保证锁能获得成功
get方法
将key通过hash定位到具体的segment,再通过hash定位到具体的元素。由于HashEntry的value属性是volatile修饰的,保证了内存可见性。整个过程不需要加锁。
1.8 底层是数组+链表+红黑树
抛弃了Segment分段锁,采用CAS+Synchronized来保证并发安全性。
跟HashMap很像,把hashEntry改成了Node,但是作用不变,把值和next都用volatile修饰,保证了可见性,并且引入了红黑树,在链表大于一定值的时候会转换,默认是8.
put方法
- 先根据key计算出hashCode
- 判断是否需要进行初始化
- 即当前的key定位的Node,如果为空,表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功
- 如果当前位置的hashCode == MOVED == -1 ,则需要进行扩容
- 如果都不满足,则利用synchronized锁住数据
如果数量大于TREEIFY_THRESHOLD (默认值8), 则转换为红黑树。
get方法
根据计算出的hashCode寻址
- 如果是红黑树就按照树的方式获取值
- 不满足就按照链表的方式获取值。
为什么1.8会用Synchronized?
synchronized之前都是重量级的锁,1.8升级之后采用了锁升级的方式去做的。针对Synchronized获取锁的方式,JVM采用了锁升级的优化方式,就是先用偏向锁优先同一线程然后再次获得锁,如果失败,就升级为CAS轻量级的锁,如果失败就会短暂的自旋,防止线程被系统挂起。如果以上还是失败,就升级为重量级的锁。
扩容机制
ConcurrentHashMap的扩容机制
1.7 版本
- 1.7的ConcurrentHashMap是基于Segment分段锁实现的
- 每个Segment相当于一个小型的HashMap
- 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
- 先生成新的数组,然后转移原数组到新的数组中
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
- 1.8的ConcurrentHashMap不再基于Segment实现,基于Node
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容,那么该线程一起进行扩容
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加的ConcurrentHashMap中,然后判断是否超过了阈值,超过了则进行扩容
- 支持多线程同时扩容
- 扩容前也是先生成一个数组
- 在转移元素时,先将原数组 分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或者多组的元素转移工作。