HashMap
在进入今天的主题之前先来讲讲大家都熟知的HashMap类:
HashMap是集合中的一个重要的类,继承自Map接口,Map和其他集合最大的不同点就在于它是以键值对的形式储存数据,HashMap在开发中一个很致命的问题在于它的线程不安全性,在多线程下进行put()方法时有可能造成链表的闭环,从而形成死循环,然后就需要找寻线程安全的类。
HashTable
接下来看一下HashTable类,它是线程安全的,而它的线程安全操作是在所有涉及到多线程操作的地方都加上了synchronized关键字将整个集合锁住,在保证线程安全的同时这种方法会使得效率非常低下。
那有没有既能保持线程的安全性的同时,又能够使得效率更高呢
ConcurrentHashMap
分段锁机制
ConcurrentHashMap在最开始的时候(jdk1.7之前),为了同时解决安全和效率两大问题,采用了分段锁机制来储存数据,具体操作是:在集合对象中储存一个segment数组,把集合的元素分为16个分段,每个分段上储存一个HashEntry数组及对应的链表,每个HashEntry数组就相当于一个HashMap
当触发锁时每一个segment中都会有一个独立的锁,而不会将整个对象锁住,所以多线程操作时每个segment中的数据不会相互影响,从而保证了效率
当然,这样储存的版本还有着不少的问题,如最多并发只有16个,结构过于臃肿等等,随着版本的迭代,在jdk1.8时更改了ConcurrentHashMap的结构,使之更加完善
jdk1.8的ConcurrentHashMap
CAS原理
jdk1.8版本的ConcurrentHashMap与之前版本相较而言,一个较大的改变就是摈弃了segment数组,利用CAS原理解决并发问题,CAS原理也叫作比较交换原理,其中有三个数:所存的值V,预期的值A,要变换成为的新值B
当A和V相同时,V会修改成B,否则不进行操作。这样就能较好的解决线程安全的问题。
数据结构
在jdk1.8版本之后,ConcurrentHashMap是摒弃了segment数组的数据结构,而是采用了和HashMap差不多的数组+链表+红黑树的结构:
我们先来看一个简单的ConcurrentHashMap的例子:
import java.util.concurrent.ConcurrentHashMap;
public class DemoConcurrentHashMap {
public static void main(String[] args) {
ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap<>();
String key = "aaa";
int value = 1;
map.put(key,value);
System.out.println(key + "--->" + map.get(key));
}
}
运行结果为:
可见,map常用的put()和get()方法在ConcurrentHashMap中实现起来和HashMap中似乎都一样,但是,进入源码中就能发现其中不同所在。
构造方法
首先看一下其构造方法:
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
ConcurrentHashMap共有5个构造方法:
它的无参构造方法非常特别,是一个空实现,也并没有对集合进行初始化操作,那么它的初始化在哪里进行呢:put()方法中。这个奇怪的构造方法也是它和其他集合一大不同点所在。
第二种构造函数传入数组大小,并且将sizeCtl进行赋值,其取值是最小的大于容量的一个2的次幂数
sizeCtl是一个用来控制数组大小的属性,有以下几种情况:
为-1时:代表正在进行初始化操作
为-n时:表示有n-1个线程进行扩容
正数时:若数组未初始化则表示正在进行初始化操作,否则则表示可用容量
其他的构造方法主要传入的参数有Map集合,初始容量,装载因子,线程数等,用法不多
put()方法
构造方法之后就应该进行传值了,和HashMap一样,也是用的put()方法:
源代码如下:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
该代码的逻辑都是if……else非常好理解:
1、如果键或值为null,抛出空指针异常
2、若数组未初始化,则调用initTable()方法进行初始化,这里就解决了构造方法未初始化的问题
3、否则,如果新插入的键没有产生hash冲突的话,便直接进行CAS插入
4、产生了hash冲突的话,如果头结点的hash值为MOVED(-1)的话,则表示应该进行扩容操作
5、产生hash冲突且不用扩容的话,将会对Node数组中该位置进行加锁,防止其他线程的更改,并且根据该位置储存的结构进行插入结点,如果数据结构是链表,则添加至最后一个位置,如果是红黑树,则添加到它应该在的位置
6、到这里,插入操作已经完成,还需判断插入之后,Node数组的该位置是否需要改变数据结构,如果需要则用treeifyBin()方法将链表转换为红黑树
最后记录一下table中元素个数,便完成一次添加元素的操作
get()方法
说完了元素添加之后,我们聊一聊也是常用的用于查找的get()方法
源码如下:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
和HashMap一样,ConcurrentHashMap的查找也需要先得到key的hash值,获取方法是spread()方法:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
该方法将key的hash值高16位与低16位异或后再进行hash,两次hash减少hash冲突
计算出hash值后,定位到table上的对应位置
1、如果key是链表首节点就直接返回所对应的值
2、若Node节点的hash值小于0时,表示正在扩容,则调用正在扩容节点的find
方法查找
3、若都不成立,则搜寻链表
size()方法
看完put/get后,我们看一下另一个常用的方法:size
同样,我们先看它的源码:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
可以看出,就一个重要的sumCount()方法,看一下它的源码:
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
size()方法用一个volatile类型的变量baseCount记录元素的个数,如在前面的put()方法中,有一个addCount()方法,便是用来增加元素的个数而CounterCell数组,作用则是在多并发环境下辅助记录元素个数,在使用size()方法时将baseCount中的元素和CounterCell数组中的每个元素进行累加,结果则为当前的所有元素个数。
虽然jdk1.7的ConcurrentHashMap和HashMap结构上截然不同,但到了1.8版本,可以看到二者在数据结构上是有一定相似的,最大的不同还是在于线程安全方面。