HashMap 在多线程的执行过程当中,出现线程安全的问题,如何解决?
1、使用 HashTable
几乎在每一个方法中添加了 synchornized 关键字,是一种性能低下的线程安全集合类;
为什么 HashTable 不允许使用 null 作为 key? 但是HashMap 可以的?
主要是因为为了避免程序执行的二义性:
1、不清楚是原来的 key 不存在
2、还是存储的 key 就是 null;
HashMap ConcurrentHashMap 都是存在二义性的,但是 HashMap 通过结合 contains(key) 的使用可以消除这种二义性;
但是 ConcurrentHashMap ,在多线程的访问之下,是没有办法判断到底是本身 key 不存在还是原来的 key 就是 null;
因为 ConcurrentHashMap 使用的是 fail – safe 机制,获取到的数据不是最新的,所以不能使用 contains(key) 进行例如 HashMap 的判断;
ConcurrentHashMap的key value不能为null,map可以?
2、使用 synchronizedMap
synchronizedMap 如何实现线程安全的?
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;
}
在 synchornizedMap 中的源代码中存在 Map m 变量,mutex 互斥锁,当使用无参数构造的时候,互斥锁就是传入进来的 Map m 对象;
使用有参数构造函数的时候,使用传递进来的对象作为互斥锁;
有了互斥锁之后,在底层源代码中,对相关的方法代码使用 synchornized 进行加锁控制线程安全的问题;
所谓的互斥锁 mutex ,就是同一时间中,只能有一个线程可以访问这个mutex 对象,其他对象也想要访问 mutex 的时候,在外面等待当前线程完成操作之后释放了锁,才能继续访问;
// 截取的部分代码,大量的使用 synchornized 进行加锁,控制并发访问;防止出现线程安全问题;
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);}
}
public void clear() {
synchronized (mutex) {m.clear();}
}
3、使用效率高得 ConcurrentHashMap
详细的原理在下文中进行相关的描述;
JDK 1.7 HashMap 在多线程下面形成的死链问题
参考下面的博客,写的十分的清楚
为什么HashMap 1.7 会产生死循环?
小结一下:主要的原因就是两个线程同时想要对 HashMap 1.7 进行扩容,但是扩容的操作没有同步;
有一个线程已经完成了扩容,哈希桶上面的元素的引用已经变了;
但是,另外一个线程同样还是按照原来的引用进行扩容,造成了死链的形成;
扩容本来是遍历所有元素,放置到新的哈希表中,但是遍历不下去了,因为形成了死链。一直在死链上面循环,得不到放置到新的哈希表里面的元素,一直僵持;
ConcurrentHashMap 1.7 的实现原理
ConcurrentHashMap 1.8 的实现原理
Fail – Fast Fail – Safe 机制
Fail – Fast
Fail - Fast机制就是指的使用迭代器遍历集合的时候,如果集合的结构发生了变化,会出现异常; 结构出现的异常是:增加或者删除元素的时候,集合的结构就发生了变化;
-
fail-fast 机制是Java集合(Collection)中的一种错误机制。 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加、删除),则会抛出ConcurrentModificationException(并发修改异常)。
-
所以,在多线程环境下,是很容易抛出ConcurrentModificationException的,比如线程1正在对集合进行遍历,此时线程2对集合进行修改(增加、删除)。但是,单线程就不会抛出吗?很明显,单线程也会有类似的情况,比如main线程在遍历时对集合进行修改(增加、删除、修改),那么main线程就会抛出ConcurrentModification Exception异常
-
阿里的技术手册中禁止在迭代器中使用 ArrayList 的 add() 方法
ArrayList 在遍历的时候,ArrayList 的结构是不允许改变的,否则会出现ConcurrentModificationException,底层就是 迭代器的 expectedModCount 与 ArrayList 的 modCount 数值不一致,所以会报错,所以在使用迭代器进行遍历的时候,禁止使用 add() 方法;
或者说在迭代器中禁止使用所有有修改 modCount 的方法,因为会出现ConcurrentModificationException;
但是有的方法中没有修改 modCount 就是可以放置在迭代器中使用的;
Fail – Safe
采用安全失败机制的集合容器,使用迭代器遍历的是原来集合的拷贝版本,不在原来的集合中进行遍历;
迭代器在遍历的时候访问的是拷贝的集合,所以在遍历的过程中对于集合结构的修改不能被迭代器检测到,所以就触发不了 ConcurrentModificationException 异常;
优点:
避免了ConcurrentModificationException异常;
缺点:
1、拷贝的时候产生了大量的对象,对于系统资源的开销是比较大的;
2、在使用迭代器遍历集合的时候,没有办法保证得到的数据是最新的,因为可能在集合拷贝之后,原始的集合进行了数据的更新;
参考博客