一:摘要概述
- Collections并发安全实现
- HashTable并发安全与效率
- ConcurrentHashMap并发安全
- HashMap的key和value为什么允许null但是HashTable和ConcurrentHashMap不允许
二:Collecitons并发安全实现
Collections工具类中提供了函数synchronizedMap
实现线程安全,该函数会返回一个线程安全的类SynchronizedMap
。该类实现线程安全的方式也简单粗暴就是synchronized
关键字,锁的对象就是this这个map对象
// SynchronizedMap是Collections静态内部类
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
// 该方法为SynchronizedMap的put函数,位置Conllctions2587行
// synchronized锁的mutex就是构造函数的this对象
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
三:HashTable并发安全与效率
HashTable的并发安全实现较为简单,查看源码几个关键方法get、put都是直接在方法上使用synchronized
关键字修饰保证并发安全。虽然synchronized
在JDK1.6经过大幅度优化后性能有所提升,但是整个方法上锁的策略还是不可避免的效率低下
四:ConcurrentHashMap并发安全
ConcurrentHashMap是并发安全的一个Map,由此引发下列两个问题:
- ConcurrentHashMap如何保证线程安全
- ConcurrentHashMap函数get、put并发安全
4.1 并发安全保障
ConcurrentHashMap在JDK1.7的时候使用lock锁的方式保证segment段范围内线程安全,这样做锁粒度较大并发效率也并不是很高。JDK1.8时使用CAS+Synchronized
的方式保证线程安全,这时候锁的粒度是一个哈希桶
4.2 Get函数并发安全
查询的线程安全用到的是volatile
关键字保证内存可见性的特点,查看如下Node内部类发现其中的val
和next
都被volatile修饰,因为get函数不涉及到数据修改。当其它线程修改数据以后volatile保证了第一时间主内存与工作内存的同步,并发情况下就是线程安全的,不得不说这是一个经典的volatile用法
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
4.3 Put函数并发安全
volatile不保证原子性,所以在put函数中涉及到元素修改以后使用了CAS+Synchronized保证线程安全
当哈希桶为空时使用CAS乐观锁实现原子数据替换,具体代码如下,源码位置在1019
行
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
当哈希桶不为空时直接使用synchronized
关键字对哈希桶桶上节点加锁,保证链表更新的线程安全,具体代码如下,源码位置在1027
行
// f对象在源码1018行进行赋值,值就是哈希桶位置上的Node节点
synchronized (f) {
...
}
五:Null值问题
HashMap中key和value都可以存储null值,但是HashTable和ConcurrentHashMap都不允许key为null。这个问题主要还是fail-fast
、fail-safe
两种机制的区别,其中HashMap采用fail-fast,剩下两者采用fail-safe
5.1 fail-fast
迭代过程中如果容器内容发生改变则直接抛出ConcurrentModificationException
异常,实现原理就是使用modCount
这个记录Map结构修改次数的属性值与expectedModCount
迭代开始就存储的值进行比较
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
5.2 fail-safe
安全失败机制,这种机制使得本次读取的数据不是最新数据,因为是在原容器的拷贝上进行的迭代。当读取到null值时无法判断是不存在亦或是为空。这时候也无法调用contain(key)进行判断,自然也就不允许插入null值