HashMap
1.7 数组加链表
底层:数组 定义数组时已经分配好空间了
存放过程:先通过key的hashCode生成一个HashMap生成的HashKey,然后通过HashKey生成数组中的下标。
put方法:取出对象的hashCode,生成HashKey,在数组中找到这个位置链表的头节点的值,新增或更改。返回一个旧值。
初始化是不会构建一个Entry数组的,会在第一次使用put方法时去初始化。
源码属性
//默认的初始化空间
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap当前存放的元素
int size;
// 阈值 = table.length * factor
int threshold;
modCount 修改次数 多线程有关
threshold 阈值 扩容有关
源码方法
1. 初始化
默认的是空构造方法传入的是默认的大小(16)和默认的加载因子(0.75f),这两个数相乘就会得到阈值(threshold),当HashMap中元素得个数超过阈值并且当前操作得数组下标的位置以及存在链表的话,就会触发扩容。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//阈值在一开始是传入的数组长度大小,会在初始化数组时重新赋值。
threshold = initialCapacity;
init();
}
2.put方法
public V put(K key, V value) {
//初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//特判---进行key为null的处理
if (key == null)
return putForNullKey(value);
//算出hash值
int hash = hash(key);
//找到要存放数组的下标
int i = indexFor(hash, table.length);
//先查询存不存在这个key值,存在就会覆盖,不存在就会新增
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//HashMap的处理次数
modCount++;
addEntry(hash, key, value, i);
return null;
}
第一次往HashMap放数据才会进行初始化,在这里就会算出这个HashMap得阈值是多少了,然后进行内部数组的初始化。
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
//计算阈值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
//这个方法影响到的是HashMap的hashSeed,对计算key--value中key的哈希值有着扰乱的作用。
initHashSeedAsNeeded(capacity);
}
在初始化结束后就会存放这个数据到HashMap中了,首先通过特判判断key是不是null,因为在HashMap中,key可以是为null的。
当key为null时,HashMap会将改Key-Value存放在数组下标为0的位置,这是写死的。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
计算hash值
final int hash(Object k) {
//hash种子
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
通过hash与数组长度的与运算算出该存放的下标
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//长度一定是2的幂次方数,然后才会使得到的下标不会越界。
return h & (length-1);
}
添加新值,就会先判断数组是不是需要扩容,需要就会进行扩容,然后重新计算key的hash值。
addEntry(hash, key, value, i);
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
插入新值使用的是头插法
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
3.get方法
get方法比较简单,先是判断key是不是为null,然后从数组中找到该元素放回出去。
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
4.roundUpToPowerOf2 找到最大的2次幂数
这个方法主要是用在初始化数组容量时,找到传入或者默认的大小长度的最大2次幂。
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
而这个方法是根据Integer中的highestOneBit方法来的,将值减一然后翻倍,这样子得到就是该值得最大2次幂数了。
//把最高位后的位数全变成1,然后右移1一位,与运算,就是得到这个数字小于等于2的n次方幂
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
5.resize 扩容
主要是上面讲过的当HashMap中元素得个数超过阈值并且当前操作得数组下标的位置以及存在链表的话,就会触发扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
扩容的大小是原数组大小的两倍,也是2的幂次方数。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer就是将旧数组中的元素迁移到新数组中,在这个过程中,HashMap线程不安全就显现出来了。两个线程在同时操作扩容时会导致数据在迁移的时候,新数组中产生了循环链表。于是,在get方法中,就会死循环了。
rehash这个参数是由initHashSeedAsNeeded方法得来的,这个方法在初始化时也用到过,用于处理hash种子的,这里需要是要看,是不是hash值的偏移不够,于是引入了一个值来扰乱key的hash值,使得hash值更加随机。一般情况下,rehash是false。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//扩容---扩容后的key只有两种可能,一是不变,二是在原始的位置加上table.length的长度
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
6.inflateTable 初始化数组
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
//重新计算阈值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//实例化数组
table = new Entry[capacity];
//初始化Hash种子
initHashSeedAsNeeded(capacity);
}
1.8 数组加链表(在链表的长度大于等于8时转化为红黑树)
红黑树比链表的查询快,比平衡二叉树的插入快,取中值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n7QhAcff-1620831636288)(E:\文档\java\image-20210427203638076.png)]
源码属性
//默认的容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化长度
static final int TREEIFY_THRESHOLD = 8;
//解除树化
static final int UNTREEIFY_THRESHOLD = 6;
//树化时Map容量的最小值
static final int MIN_TREEIFY_CAPACITY = 64;
源码方法
1.初始化
跟1.7的差不多
2.put方法
在这里,跟1.7还是差不多–调用第一遍put才会初始化数组,然后查看该key存放的数组位置是否为空,否则就遍历数组。判断插入链表还是红黑树,或者树化链表。最后看容量是否超过阈值,超过就进行扩容。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//初始化数组,赋值变量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//i = (n - 1) & hash]----计算出存放数组的下标i
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//下面的逻辑就是当Map中存在这个key对应的键值对时赋值给e,方便更新value,插入则是分别在链表或红黑树中进行插入。
else {
Node<K,V> e; K k;
//判断链表|红黑树头结点是否就是需要put的key,赋值给e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//表头是红黑的是实例,在红黑树中找。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//在链表中找
for (int binCount = 0; ; ++binCount) {
//链表找完了还是没有,就操作插入。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//binCount大于等于7时树化,相当于8。因为在前面新增了Key-Value。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//找到了相同的Key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//更新Value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//操作次数+1
++modCount;
//容量大于阈值了,就进行扩容。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.treeifyBin 树化链表
树化链表有两个前提,一是数组不为空,二是数组的长度大于最小树化时的容量。规定了我们数组长度小于64时,是一定不会被树化的。所以上面链表长度大于7时就会树化链表是有歧义的。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 当存放元素的数组不为空或者数组的长度小于64时,采取扩容,而不是直接进行树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//取出链表的头结点
//然后将单项的链表更改为双向链表,每一个TreeNode上都有一个前驱结点(prev)和后继结点(next)
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//在这就会真正进入到树化阶段
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
构造红黑树(有空补充)
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
4.putTreeVal 往红黑树中放值
红黑树的寻找跟平衡二叉树类似,通过大小来逐步分割树的大小,最终找到该值或新增该值。
这里是通过hash值来判断两个数的大小,由于基本数据类型重写了hashCode方法,所以可以大概的认为,hash值就是他们的大小。个人观点
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//取出红黑树头结点
TreeNode<K,V> root = (parent != null) ? root() : this;
//循环
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//循环的头结点比要寻找的结点大 ------ -1,左边。
if ((ph = p.hash) > h)
dir = -1;
//小 ------ 1,右边。
else if (ph < h)
dir = 1;
//Key相同或者相等的话,表示找到旧的Key-Value值,返回出去替换。
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//这一行是在看Key这个元素有没有实现Comparable这个接口,实现了,就会进入到下面的处理,也是为了方便比较值的大小。两个值通过comparable进行比较后的值是0的话,代表着两个key的大小是相同的。
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) {
//进入到仅仅只会搜索一次的步骤中
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
//在左右子树中寻找q并且退出或者继续分叉下去
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//还是找不到旧的key或者 就会根据原始的hashCode来计算了。
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
//p为空时就新增该节点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//先获取xp(新增节点的头结点)的next下一个节点
Node<K,V> xpn = xp.next;
//new一个红黑树的TreeNode对象
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//放在xp的左边或者右边
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//链表的next指针
xp.next = x;
//新节点的父节点和前驱节点
x.parent = x.prev = xp;
//原本xp的next节点的前驱
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//调整红黑树
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
5.get方法
get方法一般都是比较简单的,要不就是从链表要不就是从红黑树中找,逻辑跟1.7的HashMap大体相同。
处理逻辑都在getNode方法中
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断tab数组中存不存在这个值
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//检查头结点是否就是要找的key
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//头结点不是,就先判断是哪种类型的结点
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//链表中找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
6.resize 扩容/初始化
跟1.7的也类似,不过就是扩容的条件少了,直接变成当前元素大于阈值就触发扩容了。这里多了一个操作方法,因为扩容后,元素存放的位置变为了两个。在1.8中,先是遍历整个链表(包括红黑树,因为红黑树也是继承链表的),然后分成两块,一个是原来位置的,一个是扩容后当前位置加上扩容容量的。遍历结束后,直接将两块直接转移过去。
final Node<K,V>[] resize() {
//旧数组
Node<K,V>[] oldTab = table;
//旧容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧阈值
int oldThr = threshold;
//声明新的变量
int newCap, newThr = 0;
if (oldCap > 0) {
//旧容量大于最大容量(1073741824)时,阈值变更为2^31次方,不进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//当旧容量翻倍后还是小于最大容量且默认初始化容量时,新的阈值就是旧阈值的翻倍,新容量也翻倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//初始化
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//容量变更后,实例化数组
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//就的数组不为空,就需要转移元素。
if (oldTab != null) {
//遍历数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//在这个下标中,只有一个元素,直接转移
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//通过红黑树自己来分类。
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//链表分类
else { // preserve order
//不转移元素的头尾
Node<K,V> loHead = null, loTail = null;
//转移元素的头尾
Node<K,V> hiHead = null, hiTail = null;
//遍历
Node<K,V> next;
do {
next = e.next;
//计算出这个元素属于哪一块位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//分别转移,直接赋值
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
红黑树的split分类方法
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//声明变量
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
//记录两个链表的长度
int lc = 0, hc = 0;
//遍历红黑树中隐藏的双向链表
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
//原本位置
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
//新增位置
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//有元素放在初始位置
if (loHead != null) {
//元素的长度小于树化下限6就链表化
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
//存放过去
tab[index] = loHead;
//另一个位置不等于空相当于整颗红黑树搬过去,就不用再次树化了,不然就要重新树化,因为树上的值发生改变了
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
//这里跟上面一样
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
7.tieBreakOrder 根据原始的hashCode来比较hash的大小
通过System中的identityHashCode来找到原始的未修改过得hashCode值。
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
//identityHashCode()原始的hash值
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
ConcurrentHashMap
1.7 数组中的数组加链表
课外知识
UNSAFE
unsafe的方法操控的都是内存中的元素,而不是每个线程中栈的元素。
//获得一个UNSAFE对象
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class tc = HashEntry[].class;
Class sc = Segment[].class;
//得到HashEntry数组的偏移量
TBASE = UNSAFE.arrayBaseOffset(tc);
//得到Segment数组的偏移量
SBASE = UNSAFE.arrayBaseOffset(sc);
//将s0放在ss的(SBASE+0)的位置上
//其实就将s0放在ss的第一个位置上
UNSAFE.putOrderedObject(ss, SBASE, s0);
源码属性
Segment中的HashEntry数组才是真正存放数据的地方
segments ConcurrentHashMap中就是通过这个数组来进行分段锁的,避免将整个对象上锁。在初始化后,Segments数组的长度是固定不变得。
segmentMask hash于上这个Mask来的得到存放到哪个Segment数组的位置上。大小总是数组长度-1.
源码方法
1.初始化
空构造方法传入的是默认的数组大小(16)和默认的加载因子(0.75)跟HashMap是一样的。
初始化ConcurrentHashMap对象中的值
//initialCapacity--希望的数据大小
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// 由ssize求log的对数
int sshift = 0;
//Segments数组的长度
//进行下面的处理后,ssize的值是16(大于等于传入的concurrencyLevel的2次幂数)
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//32减去2的幂次数
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//c是每个Segment中HashEntry数组的长度
//通过传入的数组容量和Segments数组的长度算出每个Segment对象中HashEntry数组的大小
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
//c是有着最小的限制(2)而且c的大小肯定大于等于传入的容量大小
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
//创建一个Segment对象然后存放在Segments数组中的第一个位置,方便其他位置在new一个Segment对象的时候使用它的属性
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
//真正的创建Segments数组
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
2.put方法
ConcurrentHashMap是不能存放key为null的键值对的。
进行put方法是先是找到要存放的Segment对象,然后再放在Segment对象中的HashEntry数组中。
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
//跟HashMap是一样的
int hash = hash(key);
//其实就是将hash的高32-segmentShift位的十进制数当做下标。
int j = (hash >>> segmentShift) & segmentMask;
//判断j下标的Segment是不是为空,要是为空的话,就新建一个Segment对象
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
在Segmengt对象没有创建时,ConcurrentHashMap就是在j下标的位置生成一个Segment对象,当然生成这个对象也是线程安全的。
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
//提前算出要放入的位置
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
//第一次检查是不是为空,不是就取值,返回
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
//取出第一个位置的Segment来获得其中的属性
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
//实际key-value存放的数组
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//第二次检查是不是为空,因为有可能其他线程在当前线程运行上面的操作时已经把Segment赋值了
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
//这里才真的是实例化Segment对象
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
//第三次判断是不是为空
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
//最后前面都认为是空了,这里在往数组里放对象时又再检查了一遍,只有当前位置是null才放进去
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
在取得要放在哪个Segment并且获得之后,我们就要把key-value放进Segment中的HashEntry数组中了。这里的操作跟HashMap差不多,就只有在一开始获得锁不一样。
//onlyIfAbsent(为True就相当于该key存在就不操作)
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//这里是为了给当前Segment对象上锁,只有拿到锁了才会进行下面的操作(为了保证操作对象时不受其他线程的影响
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//获得存放数据的数组(链表结构)
HashEntry<K,V>[] tab = table;
//算出要放在tab中的哪个位置
int index = (tab.length - 1) & hash;
//获得数组中链表的头结点,有可能为null
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
//不为空,其实就是遍历链表看看key存不存在,存在就覆盖
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
//为空
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
//数组长度+1
int c = count + 1;
//判断是否需要扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
//头插法
setEntryAt(tab, index, node);
//对操作次数加一
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//解锁
unlock();
}
return oldValue;
}
3.scanAndLockForPut put中的上锁
上面讲述了,对ConcurrentHashMap进行put操作时,我们要获得这个Segment对象的锁,才能进行下一步的操作。
这个方法就是获得锁的操作了,当然了,一开始tryLock()获得锁后就没有这么多屁事了,可是ConcurrentHashMap就是为了线程安全而产生的,没办法还得继续看。
虽然这个锁的获取一开始是不阻塞的,可是在获取锁到达一定的次数就会转换为阻塞的获取锁了。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
//用来标识下面等待锁的时候要进入的分支
int retries = -1; // negative while locating node
//不阻塞的获取锁,这样子就可以在等待锁的获取的同时,还能进行一些操作
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
//对node进行的操作,先查看该下标的链表中是不是已经存在这个key了,否则就生成这个node
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
//下面就是尝试tryLock一定次数后就会转换成lock阻塞了
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
4.rehash扩容
跟HashMap的扩容机制一样,但也更加直接,一旦存放元素的个数大于阈值就直接扩容。但是扩容后的数组长度还是有上限的。
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
也是跟HashMap是一样的,不过这里用了一个算是比较快的方法吧。将链表最后一个非空元素的新地址做比较,拿到连续的、跟最后一个元素地址相同的子链表进行转移,这样子一般情况下是有一定的优化的,因为是直接转移一串过去,速率快了不少。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
//翻倍的数组长度
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//新的Mask
int sizeMask = newCapacity - 1;
//遍历数组上的每一个非空链表
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
//算出新的存放下标
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
//这里就是找到与最后一个元素相同的地址的链表了
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
//就是这里,因为这个循环最后是会遍历到最后一个元素的
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//直接转移一串
newTable[lastIdx] = lastRun;
// Clone remaining nodes
//一个个转移
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
//node是新插入的元素,扩容后当然也要重新计算hash值
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
5.get方法
这个方法比较简单
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//拿到Segment对象
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
6.size方法
个人认为这个size只能放回Integet.MAX_VALUE以内的大小,他在方法里面计算了看元素的数量是否会溢出。
大致流程是先不加锁的计算两次,然后对比两次的的数量是否相同,相同就直接返回,不相同就把Segments数组中的全部Segment对象加锁算出。
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
//计算两次后不相同,全部加锁计算
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
//解锁
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
//判断是否会溢出
return overflow ? Integer.MAX_VALUE : size;
}