这篇(可能不止这一篇)主要记录一下自己学习ConcurrentHashMap的情况.
主要分析一下put()(调用了putVal())与get()
0.一些常见的东西
这里说一说类中随处可见的几个东西
0.1 tabAt((Node<K,V>[] tab, int i)
见名知意,返回tab中索引为i的内容,volatile.
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
0.2 casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v)
首先看一下这个方法中调用的方法.看了一下别人的解释,大概意思是:针对Object对象进行CAS操作.对于o,原子性的设置其偏移地址为offset的值为x,当且仅当偏移offset处的内容为expected才进行如上操作.更新成功返回true,否则返回false.是一个本地方法.
然后回来理解一下这个方法,将tab中i位置的元素替换为v,仅当i位置元素为c时才进行.
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
public final native boolean compareAndSwapObject(Object o,long offset,
Object expected, Object x);
0.3 static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
tab中i位置的元素设置为v,volatile.
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
那这个常量U又是个啥?
我们翻到源码差不多最后的位置,可以看到一些声明,其中有一个是
private static final sun.misc.Unsafe U
Unsafe是sun.misc下的一个类,此部分的源码未开源.此类中绝大多数方法为native方法.
主要进行一些非常底层的操作,比如直接移动内存指针,听起来就很危险,建议不要使用.但是由于是相当底层的操作,所以效率肯定是相当高的.
0.4 几个常量
static final int MOVED = -1; //FowardingNode的哈希值
static final int TREEBIN = -2; //TreeBin的哈希值
static final int RESERVED = -3; //ReservationNode的哈希值
以上三个类均是Node的子类.
1.存数据
我们存储数据时调用的是put(),put中则是()调用了putVal(),所以这里就看一下putVal().
1.1 putVal(K key, V value, boolean onlyIfAbsent)
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不能存储null
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//计算一下传入key的哈希值 ===================(1)
int binCount = 0;
//开始遍历底层数组
for (Node<K,V>[] tab = table;;) {//tab赋值为底层数组table,相当于做了一个镜像
Node<K,V> f; int n, i, fh;
//table(存储数据的数组)为空就初始化table,见名知意
if (tab == null || (n = tab.length) == 0)
tab = initTable();====================================================(2)
//返回node中索引为(tab的length-1与key的hash进行与运算)的元素,赋值给f
//相当于计算了f在tab中应该处于哪个位置
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//该位置为空的时候直接创建Node对象并插入i位置即可.
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//确实进行修改了就中断循环
break; // no lock when adding to empty bin
//向空位置加入元素不需要加锁
}
//fh赋值为f的哈希值,MOVED的值为-1.
//大部分情况下哈希值不会为负值.
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//剩下的情况就是这个位置有元素了.
else {
//声明一个类型为V的oldVal变量
V oldVal = null;
//用上面获取到的f作为锁,尺度相对Hashtable中(全部锁定)小得多了.
//这里相当于只锁住了底层数组当前位置下的元素,而其他元素并未被锁定
//所以其他位置的元素还可以同步进行putVal操作.
synchronized (f) {
//i位置的元素为f(原本在该位置的元素)
if (tabAt(tab, i) == f) {
//fh,即i位置元素的哈希值大于0
if (fh >= 0) {
//记录循环次数
binCount = 1;
//这里将f赋值给新声明的Node.完成一次循环binCount就+1
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果e的key与传入的key完全相同
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//e的val就用前面声明的oldVal变量储存起来
oldVal = e.val;
if (!onlyIfAbsent)
//e的val替换为传入的value
e.val = value;
break;//中断循环
}
//新声明Node,赋值为e
Node<K,V> pred = e;
//如果e的下一个为空
//即该节点已经是最后一个节点
if ((e = e.next) == null) {
//下一个元素就是传入的key,value,该元素的next为空.
pred.next = new Node<K,V>(hash, key,
value, null);
break;//中断循环
}
}
}
else if (f instanceof TreeBin) { //如果f是树
//以下是树插入元素的过程
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) {
//TREEIFY_THRESHOLD=8
//如果循环次数已经达到8,就意味着该位置有8个元素,就应该将当前节点转换为树.
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
//返回被替换掉的值
if (oldVal != null)
return oldVal;
break;
}
}
}
//当前ConcurrentHashMap的元素数量+1
addCount(1L, binCount);
return null;
}
然后看看标出来的两个方法spread()和initTable()
1.2 spread(int h)
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
逻辑很简单,就是进行重哈希.用来降低哈希冲突的可能性.
1.3 initTable()
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 尚未被初始化的时候才进行初始化操作.
while ((tab = table) == null || tab.length == 0) {
// 参照源码的注释,这个sizeCtl的意义在于判断是否有其他线程在进行初始化操作.
// 小于0的时候就证明有其他线程在进行操作,所以调用Thread.yield()让出时间片.
// 大于0的时候不进行任何操作.
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 初始化的竞赛输了,原地踏步吧~
// 这个compareAndSwapInt()与上面putVal()中casTabAt()
// 调用的方法compareAndSwapObject目的相同.
// 运行到这里,sc一定是大于等于0的.
// 此时修改为-1,表示这个线程正在进行初始化.
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//再次判断是否已经被初始化
if ((tab = table) == null || tab.length == 0) {
//sc大于0的话n就赋值为sc,否则赋值为DEFAULT_CAPACITY(16)
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//创建新的Node数组,数组容量为n,此时才进行真正的初始化.
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
//tab变成了刚刚创建的Node数组,table变成了tab
table = tab = nt;
// n - (n>>>2) = n - 1/4n = 3/4n = 0.75n
//0.75这个数字是不是很熟悉?没错,就是负载因子.
//大量使用位运算,有效提升了运算速度.
sc = n - (n >>> 2);
}
} finally {
// 如果数组已经初始化,那么sc一定大于0
//
sizeCtl = sc;
}
break;
}
}
//初始化完成
return tab;
}
方法的作用是在底层数组table未初始化的时候对其进行初始化.但是这个初始化显然不是简简单单的new一个数组.
2.获取数据
分析完了难度较高的存入数据,下面来看看相对简单的获取数据方法get(Object key).
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算一下key的哈希
int h = spread(key.hashCode());
//底层数组不为空且key计算出的哈希所在位置不为空才进行接下来的操作
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
//ek与key完全相同则直接返回e中的val
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//上面是直接就可以找到的情况,即key恰好与其对应位置上的第一个元素的key完全相同.
// 即key与其对应位置首节点的key恰好匹配.
//eh小于0代表这是Node的子类,直接调用子类中重写的find()寻找.
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//这时候就从首节点的next开始依次寻找,直到找到.
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}