概述
为了解决HashMap 的线程不安全,有三种解决方法
- 使用Collections.synchronizedMap(map)
- 使用HashTable
- 使用ConcurrentHashMap
考虑到并发度等问题用的最多的是最后一种,它的性能和效率明显高于前面两种。
1、Collections.synchronizedMap
再synchronizedMap内部维护了一个普通话对象map和排斥锁mutex
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;
}
构造器有两个,如果为传入mutex,则将mutex置为this,如果传入了,mutex即为所设置的值。然后创建出synchronizedMap之后对map的操作全是加锁的了。
2、HashTable
HashTable是线程安全的,但是效率却不是那么乐观,其效率底下的原因是其底层对数据操作的时候加上了synchronized加锁
除了HashTable是线程安全的意外,HashTable与HashMap还有以下不同
- 扩容直接不同,HashMap初始容量为16,每次扩容为原来的两倍,而HashTable初始容量为11,每次扩容为2*n+ 1;
- HashMap允许键或值为null,而HashTable不允许键或值为null,因为HashTable底层实fail-safe的机制,会导致读到的数据不是最新的数据,也就是说当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。
- HashMap是fail-fast的,而HashTable是fail-safe的
3、ConcurrentHashMap
ConcurrentHashMap再jdk1.7的实现:
是由Segment数组和HashEntry组成的,是基于数组加链表的,HashEntry使用了volatile修饰它的value和next指针,它的并发度高的原因是其采用分段锁技术,Segment继承自ReentrantLock,每个线程占用一个Segment,不会影响其他的Segment,也就是说它的并发度其实是容量大小。
其put()方法实现线程安全的方式如下:
- ①、首先获取锁,如果获取失败则自旋尝试获取锁
- ②、如果自旋次数达到了MAX_SCAN_RETRIES ,则阻塞获取锁,保证能获取到锁
get操作就比较简单了,先通过hash找到segment,然后再次hash找到指定位置的元素,因为HashEntry的value使用了volatile修饰保证了可见性,所以可以保证每次取数据的时候都是最新的值,get操作很高效,不需要加锁。
问题:ConcurrentHashMap再jdk1.7中存在和HashMap一样的问题,就是遍历链表效率低。
jdk1.8对ConcurrentHashMap的优化
ConcurrentHashMap再jdk1.8种摒弃了分段锁的使用,而是采用CAS + synchronized的方式来保证线程安全,同时把HashEntry改为了Node,但作用不变,Node的value和next指针仍然使用volatile修饰。
ConcurrentHashMap的put操作:
- ①、获取key的hash值
- ②、判断是否需要初始化table
- ③、为当前的key定位出Node位置,如果为空表示当前位置可以写入数据,尝试利用CAS写入,失败则自旋保证成功
- ④、如果当前的位置的hashcode == MOVED(MOVED 为-1),则进行扩容
- ⑤、如果都不满足则利用synchronized写入数据
- ⑥、树形化检测
get操作就比较简单了,直接根据hash值进行红黑树 或者链表的方式获取值即可。注意get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。数组用volatile修饰主要是保证在数组扩容的时候保证可见性。