作为并发版本的HashMap
,它的很多特性都是和HashMap
一样的, 那么在这里我主要还是从源码的层面解析一下,然后看看和HashMap
有哪些不同的地方,同时怎么保证线程安全的。
ConcurrentHashMap解析
我在写的时候发现里面新增了很多属性,而且这些属性很晦涩,所以为了便于理解,我不会先解释属性的作用,还是从方法入手,然后逐步的扩展解释各种对比HashMap
新增的属性的作用。
ConcurrentHashMap的构造方法解析
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity = (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//这里传入参数和HashMap不同,HashMap是initialCapacity
//主要就是考虑并发情况下table扩容的效率问题,比HashMap的初始容量要大
//就是初始容量+初始容量向右移动一位+1 = 初始容量*1.5+1
//空间更大,会降低一些扩容的几率,提高性能
//tableSizeFor方法和Hash Map一样,都是传入参数最近的比它大的2的幂次倍
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
重头戏:putVal方法
这里面将会涉及到并发情况下的扩容处理,维持数据一致性问题,怕说起来没完,具体的扩容方法不再这里展示,还是2倍,但是实现细节都是针对多线程的情况,加了sychronized
,我这里只循着主线看。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//这个计算Hash的方式和HashMap是不一样的,(h ^ (h >>> 16)) & HASH_BITS;
//HASH_BITS这个值是0x7fffffff,转为二进制就是
//2^31-1,二进制就是首位0,其他的都是1。所以这样的计算结果就会发现
//如果原hashcode是负数,那么就会将最高位设置成0,也就是正数,然后
//其他位保持不变,负数在和长度-1进行&运算的时候会和正数有差异
//会导致向左移动,在并发的环境下更容易造成hash冲突。所以加了这样一个变化
int hash = spread(key.hashCode());
int binCount = 0;
//循环table
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//如果table为空,就初始化,然后直接转移到下面看初始化的方法------------------1,做个标记一会回来
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//长度-1 &hash计算索引地址然后调用tabAt方法一行代码,信息量很大
//Unsafe包的getObjectVolatile方法,防止指令重排弄乱结果
//(Node)U.getObjectVolatile(tab, ((long)i k = ConcurrentHashMap.class;
// SIZECTL = U.objectFieldOffset
// (k.getDeclaredField("sizeCtl"));
//所以到这里我们可以猜测一下,sizeCtl 是干啥的,首先它是volatile的,那么它一定是独立线程之外的东西,去记载某些东西
//我们知道多线程中有一个ctl,来记录线程池状态的,那么这个sizeCtl是不是也是和记录状态相关,和size挂上,那么是不是和扩容状态相关?
//我们往下找推测(虽然源码给注释了,但还是想自己推测一下作用,记忆深刻)
//compareAndSwapInt这个方法的第二个参数是内存偏移量,而这个常量确实是sizeCtl字段的内存偏移量
//所以这个方法就是去SIZECTL的内存偏移量找数据,然后和sc对比,
//如果相等 ,然后就将SIZECTL偏移量位置的数据,也就是sizeCtl设置为-1,返回true,
//否则不动,返回false,就是CAS操作,所以sizeCtl =-1的时候说明table正在初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//如果table为空的时候
if ((tab = table) == null || tab.length == 0) {
//如果sc>0,sc给n,否则n设置为默认容量16
//而从上面的构造方法中我们发现sc经过计算是>0的,而有一种情况,就是调用ConcurrentHashMap的无参构造方法
//这个时候就是0了,然后就取默认容量了,然后创建个这个长度的数组,给table,table就初始化好了
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node[n];
table = tab = nt;
//然后sc设置为数组长度的四分之三,也就是0.75。嗯?变成了扩容的阈值了
sc = n - (n >>> 2);
}
} finally {
//这个时候将sizeCtl赋值为sc,这个方法是初始化table,如果刚开始都是0的时候sizeCtl就是扩容阈值了,同时SIZECTL = -1
//如果不是的话,sizeCtl,就取sc的值
sizeCtl = sc;
}
//回去接着看putVal
break;
}
}
return tab;
}
这个初始化的方法中sizeCtl
我们可以看出,如果它>0的时候就表示已经初始化过了,等于阈值。如果=0的时候那么就是还没有初始化,等于-1的时候就是正在初始化。这一下出来三种状态的表示了,还有一状态在putVal
注释中写了。
小总结:: 虽然我没有一行一行代码的进行分析,只分析了一些我们没有在HashMap
中见到的代码,从这些代码中我们可以发现ConcurrentHashMap
能保证并发下的数据一致性就是依赖于锁,也就是sychronized
,而且细化到了节点,摒弃了1.7中分段锁的概念,很大程度的提升了性能,节省了锁的开销,同时用大量的CAS操作来保证数据的一致性,而扩容的算法,计算index
等都是相同的算法。比如里面有个非常好的思路,如果你查找数据时候发现正在扩容,那么就帮助扩容,自己将自己需要的数据移动到新的数组中,然后返回。get
方法我就不列出来了,因为很多代码都和putVal
重复,毕竟更新也许需要先查找的,大家可以自己看一看。对于并发容器最核心的是对多线层的理解,后面我会出文章仔细分析Java的多线程和相关的池化技术。
LinkedHashMap解析
在这里我可以简单的说一下,不管是ConcurrentHashMap
还是LinkedHashMap
其实底层的思想都是HashMap
,弄懂HashMap
,那么这两个容器对我们来说都是进行扩展,只不过在展现形式上不同。大家可以看我的这篇文章# HashMap很细的分析--令人发指,比如ConcurrentHashMap
完善了多线程环境下的数据一致性问题,而LinkedHashMap
也有它的优势,比如它可以按照数据插入的顺序进行访问,支持按照元素的访问顺序进行排序(可以作为LRU容器)等。那么我也是和上面一样,从源码的角度分析一下LinkedHashMap
。
LinkedHashMap的结构
相对于HashMap
,它的结构是一个双向链表,我们上面的特性说过了,它支持按照插入顺序进行遍历,但是呢HashMap
本身是不支持的,所以就需要扩展存储数据的节点,而它的双向链表就体现出来了,看下面它的结构,它继承了Node
,然后定义了before
和after
,这样就能使链表具备双向的能力了。
static class Entry extends HashMap.Node {
Entry before, after;
Entry(int hash, K key, V value, Node next) {
super(hash, key, value, next);
}
}
LinkedHashMap get方法解析
相对于ConcurrentHashMap
,LinkedHashMap
真的是非常简单的,连Put
方法都是使用父类HashMap
的,那么我们看一下get
方法的源码
public V get(Object key) {
Node e;
//直接调用了HashMap的getNode方法,脸都不要了
if ((e = getNode(hash(key), key)) == null)
return null;
//这里我们猜都能猜出来是干啥的,accessOrder就是访问顺序的意思,也就是如果你的LinedHash
//是支持访问顺序排序的话,那么这个参数就是true,那我们也就知道,这个方法就是维护访问的
//作用就是将访问的元素移动到链表末尾
//因为是尾插法,按照插入顺序访问就直接从尾开始,这就会有疑问,每个桶都有链表
//咋能知道拿个链表的尾部是真正最后插入的数据节点呢?看下面的方法
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
LinkedHashMap
重写了newNode
的方法,在linkNodeLast
方法为Entry
指定了before
和after
Node newNode(int hash, K key, V value, Node e) {
LinkedHashMap.Entry p =
new LinkedHashMap.Entry(hash, key, value, e);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry p) {
//因为插入的都在链表的尾部
LinkedHashMap.Entry last = tail;
tail = p;
//如果尾部位空,说明是个新节点,before和after都没有,毕竟就自己一个节点
if (last == null)
head = p;
else {
//否则尾部的after就是新节点,新节点的before就是尾部,双向了吧
p.before = last;
last.after = p;
}
}
LinkedHashMap对于插入和删除节点时的维护解析
如果看过我写HashMap
解析的时候应该看到我在put
方法中有一个方法的注释写的是HashMap
是空实现,LinkedHashMap
会使用,这个方法就是afterNodeInsertion
,插入节点的后处理方法,这个就是个钩子方法,也叫做模版方法。
afterNodeInsertion方法解析
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry first;
//removeEldestEntry如果自己实现LRU的时候需要重写,就是逐出数据的条件
if (evict && (first = head) != null && removeEldestEntry(first)) {
//移除头节点
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
afterNodeRemoval方法解析
这个也是模版方法,在HashMap
移除节点的时候调用,看注释吧
void afterNodeRemoval(Node e) { // unlink
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after;
//把p的before和after都设置null,断开联系
p.before = p.after = null;
//下面就是干一件事,删除节点之后得前后连起来。
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}