HashMap与线程安全
一、HashMap 为何是线程不安全的
HashMap是通过散列表来实现存储结构的,具体内容请看我的另一篇博客《HashMap深度解析》,那么HashMap为什么线程不安全呢,主要有两个原因。
首先肯定是多个线程同时去往集合里添加数据,第一个原因:两个线程同时添加相同的key值数据,当两个线程同时遍历完桶内的链表时,发现,没有该key值的数据,这是他们同时创建了一个Entry结点,都添加到了桶内的链表上,这样在该HashMap集合中就出现了两个Key相同的数据。第二个原因:当两个线程同时检测到size/capacity>负载因子时,在扩容的时候可能会在链表上产生死循环(为什么会产生死循环,可以看一些HashMap的死循环相关的博客),也可能会产生存储异常。
二、如何线程安全的使用HashMap
方法一:Hashtable
Hashtable是Java低版本中提出来的,由于其内部的加锁机制,是的其性能较低,目前已经不常用了。所以当一个线程访问Hashtable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以,效率很低。
HashTable源码中是使用synchronized来保证线程安全的,比如下面的get方法和put方法:
public synchronized V get(Object key) {}
public synchronized V put(K key, V value) {}
方法二:SynchronizedMap
调用synchronizedMap()方法后会返回一个SynchronizedMap类的对象,而在SynchronizedMap类中使用了synchronized同步关键字来保证对Map的操作是线程安全的。
源码如下
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
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) {
if (m==null)
throw new NullPointerException();
this.m = 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);}
}
public void clear() {
synchronized(mutex) {m.clear();}
}
private transient Set<K> keySet = null;
private transient Set<Map.Entry<K,V>> entrySet = null;
private transient Collection<V> values = null;
public Set<K> keySet() {
synchronized(mutex) {
if (keySet==null)
keySet = new SynchronizedSet<K>(m.keySet(),mutex);
return keySet;
}
}
public Set<Map.Entry<K,V>> entrySet() {
synchronized(mutex) {
if (entrySet==null)
entrySet = new SynchronizedSet<Map.Entry<K,V>>(m.entrySet(), mutex);
return entrySet;
}
}
public Collection<V> values() {
synchronized(mutex) {
if (values==null)
values = new SynchronizedCollection<V>(m.values(), mutex);
return values;
}
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized(mutex) {return m.equals(o);}
}
public int hashCode() {
synchronized(mutex) {return m.hashCode();}
}
public String toString() {
synchronized(mutex) {return m.toString();}
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized(mutex) {s.defaultWriteObject();}
}
}
方法三:ConcurrentHashMap
ConcurrentHashMap是java.util.concurrent包中的一个类,
首先,我们先来了解一下这个集合的原理。hashtable是做了同步的,但是性能降低了,因为 hashtable每次同步执行的时候都要锁住整个结构。于是ConcurrentHashMap 修改了其锁住整个结构的格式,改为了只锁住HashMap的一个桶,锁的粒度大大减小,如下图:
而且ConcurrentHashMap的读取操作几乎是完全的并发操作。所以ConcurrentHashMap 读操作的加锁加锁粒度变小,个体操作几乎没有锁,所以比起之前的Hashtable大大变快了(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。我认为ConcurrentHashMap是线程安全的集合中最高效的。
而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
在Java 7中使用的是对Segment(Hash表的一个桶)加锁的方式
ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点)。
Segment的源码
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =Runtime.getRuntime().
availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
HashEntry的源码
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
在Java 8中摒弃了Segment的概念,利用CAS算法做了新方式
CAS算法采用“数组+链表+红黑树”的方式实现
————亓慧杰