本文章源码来自 Java 8,重点是put和get方法及涉及到相关方法
// map容器的最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的初始容量
private static final int DEFAULT_CAPACITY = 16;
// toArry 方法转化的最大容量,超过报oom异常
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 加载因子,常说的阀值不过现在基本没用了都是用 n-(n >> 2) 表示
private static final float LOAD_FACTOR = 0.75f;
// 转化为红黑树的节点数,在concurrentHashMap里面容量必须大于64才能进行红黑树转化
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转化为链表的节点数
static final int UNTREEIFY_THRESHOLD = 6;
// 上面说的转化树所需最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 扩容时最小转移容量
private static final int MIN_TRANSFER_STRIDE = 16;
// resize 校验码
private static int RESIZE_STAMP_BITS = 16;
/**
* 主要用于表初始化和扩容时的控制,各种数值有不同的含义
* -1: 表正在初始化或者扩容
* 其他负数: 绝对值减一,就是正在扩容的线程数
* 正数: 表示下次扩容时的阈值,超过该值后进行扩容
*/
private transient volatile int sizeCtl;
Node:table数组中的存储元素,即一个Node对象就代表一个键值对(key,value)存储在table中
TreeNode:看名字就是知道,这里是红黑的节点,但是并不是直接通过TreeNode组成树,而是包装成TreeBins然后组装成树
TreeBins:用于封装维护TreeNode,是红黑树的真正存放节点
ForwardingNode:仅仅用于扩容时的临时节点
// 无参构造器,用默认的值进行初始化
public ConcurrentHashMap() {
}
// 指定初始表长的构造器,tableSizeFor 方法总是返回2的幂次方,将在后面分析这个算法
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(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
// concurrencyLevel主要是为了兼容1.7及之前版本,它并不是实际的并发级别
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;
}
简单来说 这个算法的目标是得到 2的n次方 的一个初始容量,运算过程是通过或运算让每一位的数值都变成1然后通过加1的方式变成2的幂次方
private static final int tableSizeFor(int c) {
// 这里的目的是为了不让 c 翻倍,假设c=8(刚好为2的N次幂的时候不减的下面算法会让数值翻倍)
// c 二进制表现为 1000,减一就变成了111,后面n+1的时候会变回1000
// 如果1000经过运算会变成1111,加一就会变成1000 0,这样会让最后的数值翻倍
int n = c - 1;
// 为了方便我们假设减一后的值为1000 0001 那么下面的运算会变成
// 1000 0001
// 0100 0000 =1100 0001
n |= n >>> 1;
// 1100 0001
// 0011 0000 =1111 0001
n |= n >>> 2;
// 1111 0001
// 0000 1111 =1111 1111
n |= n >>> 4;
// 因为举例的数字没那么大,下面就省略了,效果是一样的就是让所有位数都变成1
n |= n >>> 8;
n |= n >>> 16;
// 最后返回的就是 1111 1111 + 1 = 1 0000 0000 正好是2的九次方
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
主要做的事情:
casTabAt、tabAt、setTabAt 均为CAS操作不在这里细讲,其他方法将在后面分析
public V put(K key, V value) { return putVal(key, value, false); }
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 验证 key value 不为空
if (key == null || value == null) throw new NullPointerException();
// 两次hash spread方法本身也是一次hash
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh;
// 判断容器本身有无初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
/* 1. i 赋值为 (n-1)& hash 可以理解为 hash % n
2. tabAt 寻找tab[i]
3. 复制给 f
4. 判断这个节点是否为空
*/
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 为空说明没有碰撞采用cas保存元素
if (casTabAt(tab, i, null,
new Node(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 e = f;; ++binCount) {
K ek;
// hash 相等 key相等 直接替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 否则插入到链表末尾
Node pred = e;
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
// 如果是黑红树插入树节点
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)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;
}
private final Node[] initTable() {
Node[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) // 判断当前状态,如果正在扩容,让出资源
Thread.yield(); // lost initialization race; just spin
// 否则通过CAS方式将SIZECTL设为-1也就是扩容状态
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node,?>[n];
table = tab = nt;
sc = n - (n >>> 2);// 阀值 0.75
}
} finally {
sizeCtl = sc; // 设置sizeCtl 为当前阀值
}
break;
}
}
return tab;
}
请允许我偷个懒,这里写的很不错
https://www.jianshu.com/p/487d00afe6ca
主要做的事情:
1、根据key调用spread计算hash值;并根据计算出来的hash值计算出该key在table出现的位置i.
2、检查table是否为空;如果为空,返回null,否则进行3
3、检查table[i]处桶位不为空;如果为空,则返回null,否则进行4
4、先检查table[i]的头结点的key是否满足条件,是则返回头结点的value;否则分别根据树、链表查询。
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
int h = spread(key.hashCode());//两次hash计算出hash值
if ((tab = table) != null && (n = tab.length) > 0 &&//table不能为null,是吧
(e = tabAt(tab, (n - 1) & h)) != null) {//table[i]不能为空,是吧
if ((eh = e.hash) == h) { //检查头结点
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) //table[i]为一颗树
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;
}
本文主要是对put 和 get 相关方法进行分析,目的是理解一点点大神的思路和实现方式,能学会一点点东西,有兴趣的小伙伴可以自行查看此容器内的其他方法。