提到多线程肯定想到数据的线程安全问题如何解决,util包中的Hashtable,Vector都是线程安全的,最初的时候也都会选择这几种数据存储方式,在前几年面试的时候也经常会被问到Hashtable与HashMap,Vector和ArrayList的区别。简单看一下Hashtable,Vector线程安全的实现方式,这两种都是直接对方法加synchronized,直接上代码,看一下Hashtable。
public synchronized V get(Object key) { //方法内部代码不再贴了,与这次学的东西无关 }
public synchronized V put(K key, V value) {}
Vector的方式也是这样。个别没有加上synchronized关键字的public方法,也是调用了对象内部的同步方法实现了同步或者用Collections.synchronized方式加上同步,使该方法实现同步。除了这样实现线程安全,还可以直接使用Collections.SynchronizedCollection对其他非线程安全的数据结构加锁实现线程安全。
不管是对方法加synchronized,还是使用Collections.SynchronizedCollection实现同步,对对象的修改或者查询时,锁住的都是整个对象。 上面的实现方式确实解决了大部分线程安全问题,但随之而来的确实性能的下降。举个例子:用一个Hashtable存储了用户信息,每当一个用户有修改甚至查询请求时,整个Hashtable就要被锁住,也就是说,在这个玩家信息数据处理期间,所有用户的相关请求线程全部要等待,用户量级小的时候也许感觉不出来卡顿,如果放在当前互联网环境下,那将是灾难性的。
进入正题,Java1.5之后加入了java.util.concurrent 包,这个包包含有一系列能够让 Java 的并发编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去动手实现能够解决以上问题的工具包。这次主要学习一下ConcurrentHashMap的实现原理。
ConcurrentHashMap定义了Segment内部类,看一下代码:
//Segment继承了ReentrantLock重入锁(这个概念这次先不看)
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//HashEntry与HashMap中类似,可以理解为一个单向链表元素,作为存放相同hash值不同key的键值对
//这样一个Segment就相当于一个HashMap
transient volatile HashEntry<K,V>[] table;
V put(K key, int hash, V value, boolean onlyIfAbsent) {
//在对Segment进行操作时,对当前对象加锁
lock();
try {
//数据操作
} finally {
unlock();
}
}
}
ConcurrentHashMap通过数组形式存放多个Segment,用key的hash值做一次再hash当做下标识别当前键值对存放在哪个segment里。
final Segment<K,V>[] segments;
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
//用key的hashCode再做一次hash
int hash = hash(key.hashCode());
return segmentFor(hash).put(key, hash, value, false);
}
在对segment元素进行操作时加锁,这样当其它人线程操作当前ConcurrentHashMap对象时,只要key的hash值与加锁的值不同,就可以直接操作其它Segment元素。
示意图:
以上原理皆为JDK1.6版本源码,JDK1.8对ConcurrentHashMap做了大量修改,不仅去除了Segment概念,直接采用HashEntry保存数据,将锁也直接加载HashEntry,大大减少了数据冲突引起的延迟,还引入了红黑树算法,使hash之后在数组的分布更加均匀。对JDK1.8的源码学习之后补上。