线程安全集合类 ConcurrentHashMap

  • Post author:
  • Post category:其他




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.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、在使用迭代器遍历集合的时候,没有办法保证得到的数据是最新的,因为可能在集合拷贝之后,原始的集合进行了数据的更新;



参考博客


为什么HashMap 1.7 会产生死循环?



版权声明:本文为qq_40417070原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。