为什么HashMap是线程不安全的?
1、讲讲一下HashMap的存储结构
transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
可以看到HashMap内部存储使用了一个node数组(默认大小是16),而Node类包含一个Node类型的next变量,指向下一个node相当于一个链表,所有根据Hash值计算的key一样的的数据会存储到一个bucket(桶)中(一个链表中),如下图
注意:java8中如果hash值相同的key的数量大于容量(默认8)时,会使用平衡树代替链表。
2、讲讲HashMap的扩容机制
HashMap内部的node数组默认是16,假设有100万个元素,每个桶(node)中平均存储62500个元素,那么Map的remove,get,put等方法的效率也会下降,为了解决这个问题,hashMap提供了自动扩容机制,当bucket个数超过容量(默认16)*loadfactor(默认0.75)之后,会把数组大小扩展为2*16=32个,并重新计算每个元素在在新数组中的位置。
图来源http://coding-geek.com/how-does-a-hashmap-work-in-java/
从图中可以看出,在已知key的情况下,没扩容之前获取EntryE需要遍历5次,扩容之后获取EntryE需要2次。
3、讲讲为什么线程不安全
两个方法,两个并发线程同时put元素,如果根据key值计算的hash值一样,那么put的数据会被覆盖;如果两个线程同时检测到当前数组大小大于容量*loadactory之后,会同时扩容并重新计算元素的新位置,并更新数组,这样最后就会出现有一个线程的数据会丢失。HashMap在线程并发时会容易死循环,死循环不是发生在put时而发生在扩容时。
4、讲讲如何安全的使用HashMap
//Hashtable Map<String, String> hashtable = new Hashtable<>(); //synchronizedMap Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>()); //ConcurrentHashMap Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
HashTable 中使用synchronized来保证线程安全,显然效率很低,不能
public synchronized V get(Object key) { // 省略实现 } public synchronized V put(K key, V value) { // 省略实现 }
ConcurrentHashMap(简称CHM)
CHM是在JDK1.5之后引入的,用来替代HashTable,是concurrent包的重要成员,在JDK1.5之前如果要保证线程安全的使用map,只能选择HashTable或者SynchronizedMap,但是在操作map时,HashTable和SynchronizedMap会锁住整个map,引入了ConcurrentHashMap之后,在操作map时,只锁住了部分,允许多线程的并发读操作,同时通过同步锁保证了,数据的完整性。
CHM允许多线程的读操作,同时根据默认并发级别(可以在构造函数中传入,默认16)个数,允许16个线程同时进入操作map,而SynchronizedMap和HashTable只允许一个线程进入操作map,但是如果是更新操作,比如remove,put,clear可以并发执行,所以不能保证最后返回的结果是最新的。如果只有很少的线程操作map,建议将并发级别设置少一些。
有时候我们在往map中put元素时,我们需要预判断一下是否存在该元素,再执行put操作,同时为了并发操作,我们可能会加锁避免并发get和put。如下:
synchronized(map){ if (map.get(key) == null){ return map.put(key, value); } else{ return map.get(key); } }
但是我们可以使用chm的putIfAbsent(key,value)方法可以保证数据的原子性,以及避免线程的竞争。
什么时候使用CHM,建议读的线程数量大于写的线程数量时使用,如果写的数量大于读的数量,比如达到了并发级别,会锁住整个map,读操作要等写操作完成才能执行,所以CHM适用于做缓存,在容器启动时可以被多个线程请求访问。
SynchronizedMap
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map final Object mutex; // Object on which to synchronize SynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m); mutex = this; } SynchronizedMap(Map<K,V> m, Object mutex) { this.m = m; this.mutex = mutex; } public int size() { synchronized (mutex) {return m.size();} } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty();} } public boolean containsKey(Object key) { synchronized (mutex) {return m.containsKey(key);} } public boolean containsValue(Object value) { synchronized (mutex) {return m.containsValue(value);} } public V get(Object key) { synchronized (mutex) {return m.get(key);} } public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);} } public V remove(Object key) { synchronized (mutex) {return m.remove(key);} } public void putAll(Map<? extends K, ? extends V> map) { synchronized (mutex) {m.putAll(map);} }
SynchronizedMap中的方法也加了synChronized关键字来保证线程对map的操作是线程安全的。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/14838.html