上面这张图没有给出是否需要排队的结论,这是因为需要结合实际情况分析,比如初识化有16个银行,只有两个人来办理业务,那自然不需要排队;如果现在16个银行都有人在办理业务,这时候来了第17个人,那么他还是需要排队的。由于「银行者联盟」事先无法得知会有多少人来办理业务,所以在它创立的时候需要制定一个「标准」,即初始银行数量,人多的情况「银行者联盟」应该多开几家银行,避免别人排队;人少的情况应该少开,避免浪费钱(什么,你说不差钱?那也不行)
2.当有人来办理业务的时候,「银行者联盟」怎么确定此人去哪个银行?
正常情况下,如果所有银行都是未上锁状态,那么有人来办理业务去哪都不用排队,当其中有些银行已经上锁,那么后续「银行者联盟」给人推荐的时候就不能把客户往上锁的银行引了,否则分分钟给人锤成麻瓜。因此「银行者联盟」需要时刻保持清醒的头脑,对自己的银行空闲情况了如指掌,每次给用户推荐都应该是最好的选择。
3.「银行者联盟」怎么保证同一时间不会有两个人在同一个银行拥有存权限?
通过对指定银行加锁/解锁的方式实现。
源码分析
Java7 源码分析
通过 Java7 的源码分析下代码实现,先看下一些重要的成员
//默认的数组大小16(HashMap里的那个数组)
static final int DEFAULT_INITIAL_CAPACITY = 16;
//扩容因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//ConcurrentHashMap中的数组
final Segment<K,V>[] segments
//默认并发标准16
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//Segment是ReentrantLock子类,因此拥有锁的操作
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//HashMap的那一套,分别是数组、键值对数量、阈值、负载因子
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int threshold;
final float loadFactor;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
}
//换了马甲还是认识你!!!HashEntry对象,存key、value、hash值以及下一个节点
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
//segment中HashEntry[]数组最小长度
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//用于定位在segments数组中的位置,下面介绍
final int segmentMask;
final int segmentShift;
上面这些一下出来有点接受不了没关系,下面都会介绍到。
接下来从最简单的初识化开始分析
ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
默认构造函数会调用带三个参数的构造函数
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
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;
// Find power-of-two sizes best matching arguments
//步骤① start
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
//步骤① end
//步骤② start
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
//步骤② end
// create segments and segments[0]
//步骤③ start
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
//步骤③ end
}
上面定义了许多临时变量,注释写的又少,第一次看名字根本不知道这鬼东西代表什么意思,不过我们可以把已知的数据代进去,算出这些变量的值,再分析能不能找出一些猫腻。假设这是第一次默认创建:
- 步骤① concurrencyLevel = 16 ,可以计算出 sshift = 4,ssize = 16,segmentShift = 28,segmentMask = 15;
- 步骤② c = 16/16 = 1,cap = 2;
- 步骤③有句注释,创建 Segment 数组 segments 并初始化 segments [0] ,所以 s0 初始化后数组长度为2,负载因子0.75,阈值为1;再看这里的ss的初始化(重点,圈起来要考!!!), ssize 此时为16,所以默认数组长度16,给人一种感觉正好和我们传的 concurrencyLevel 一样?看下下面的例子
例子1 | 例子2 |
---|---|
ssize = 1,concurrencyLevel = 10 | ssize = 1,concurrencyLevel = 8 |
ssize <<= 1 —> 2<10 满足 | ssize <<= 1 —> 2<10 满足 |
ssize <<= 1 —> 4<10 满足 | ssize <<= 1 —> 4<10 满足 |
ssize <<= 1 —> 8<10 满足 | ssize <<= 1 —> 8<10 不满足 ssize = 8 |
ssize <<= 1 —> 16<10 不满足 ssize = 16 |
所以我们传 concurrencyLevel 不一定就是最后数组的长度,长度的计算公式:
长度 = 2的n次方(2的n次方 >= concurrencyLevel)
到这里只是创建了一个长度为16的Segment 数组,并初始化数组0号位置,segmentShift和segmentMask还没派上用场,画图存档:
接着看 put 方法
public V put(K key, V value) {
Segment<K,V> s;
//步骤①注意valus不能为空!!!
if (value == null)
throw new NullPointerException();
//根据key计算hash值,key也不能为null,否则hash(key)报空指针
int hash = hash(key);
//步骤②派上用场了,根据hash值计算在segments数组中的位置
int j = (hash >>> segmentShift) & segmentMask;
//步骤③查看当前数组中指定位置Segment是否为空
//若为空,先创建初始化Segment再put值,不为空,直接put值。
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
步骤①可以看到和 HashMap 的区别,这里的 key/value 为空会报空指针异常;步骤②先根据 key 值计算 hash 值,再和前面算出来的两个变量计算出这个 key 应该放在哪个Segment中(具体怎么计算的有兴趣可以去研究下,先高位运算再取与),假设我们算出来该键值对应该放在5号,步骤③判断5号为空,看下 ensureSegment() 方法
private Segment<K,V> ensureSegment(int k) {
//获取segments
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 0一样的segment
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
//大小和segment 0一致,为2
int cap = proto.table.length;
//负载因子和segment 0一致,为0.75
float lf = proto.loadFactor;
//阈值和segment 0一致,为1
int threshold = (int)(cap * lf);
//根据大小创建HashEntry数组tab
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//再次检查
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) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
该方法重点在于拷贝了segments[0],因此新创建的Segment与segment[0]的配置相同,由于多个线程都会有可能执行该方法,因此这里通过UNSAFE的一些原子性操作的方法做了多次的检查,到目前为止画图存档:
现在“舞台”也有了,请开始你的表演,看下 Segment 的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//步骤① start
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
//步骤① end
V oldValue;
try {
//步骤② start
//获取Segment中的HashEntry[]
HashEntry<K,V>[] tab = table;
//算出在HashEntry[]中的位置
int index = (tab.length - 1) & hash;
//找到HashEntry[]中的指定位置的第一个节点
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;😉 {
//如果不为空,遍历这条链
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;
//插入成功返回null
oldValue = null;
break;
}
}
//步骤② end
} finally {
//步骤③
//解锁
unlock();
}
//修改成功,返回原值
return oldValue;
}
上面的 put 方法其实和 Java7 HashMap里大致是一样的,只是多了加锁/解锁两步,也正因为这样才保证了同一时刻只有一个线程拥有修改的权限。按步骤分析下上面的流程:
- 步骤① 执行 tryLock 方法获取锁,拿到锁返回null,没拿到锁执行 scanAndLockForPut 方法;
- 步骤② 和 HashMap 里的那一套思路是一样的,不理解可以看下之前的文章介绍(情况②下面介绍);
- 步骤③ 执行 unLock 方法解锁
假设现在Thread1进来存值,前面没人来过,它可以成功拿到锁,根据计算,得出它要存的键值对应该放在HashEntry[] 的0号位置,0号位置为空,于是新建一个 HashEntry,并通过 setEntryAt() 方法,放在0号位置,然而还没等 Thread1 释放锁,系统的时间片切到了 Thread2 ,先画图存档
Thread2 也来存值,通过前面的计算,恰好 Thread2 也被定位到 segments[5],接下来 Thread2 尝试获取锁,没有成功(Thread1 还未释放),执行 scanAndLockForPut() 方法:
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//通过Segment和hash值寻找匹配的HashEntry
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
//步骤①
if (retries < 0) {
//情况① 没找到,之前表中不存在
if (e == null) {
if (node == null) // speculatively create node
//新建 HashEntry 备用,retries改成0
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
//情况② 找到,刚好第一个节点就是,retries改成0
else if (key.equals(e.key))
retries = 0;
//情况③ 第一个节点不是,移到下一个,retries还是-1,继续找
else
e = e.next;
}
//步骤②
//尝试了MAX_SCAN_RETRIES次还没拿到锁,简直B了dog!
else if (++retries > MAX_SCAN_RETRIES) {
//泉水挂机
lock();
break;
}
//步骤③
//在MAX_SCAN_RETRIES次过程中,key对应的entry发生了变化,则从头开始
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
通过上面的注释分析可以看出,Thread2 虽然此刻没有权限修改,但是它也没闲着,利用等锁的这个时间,把自己要放的键值对在数组中哪个位置计算出来了,这样当 Thread2 一拿到锁就可以立马定位到具体位置操作,节省时间。上面的步骤③稍微解释下,比如 Thread2 通过查找得知自己要修改的值在0号位置,但在 Thread1 里面又把该值改到了1号位置,如果它还去0号操作那肯定出问题了,所以需要重新确定。
假设 Thread2 put 值为(“亚索”,“98”),对应1号位置,那么在 scanAndLockForPut 方法中对应情况①,画图存档:
再回到 Segment put 方法中的情况②,当 Thread1 释放锁后,Thread2 持有锁,并准备把亚索放在1号位置,然而此时 Segment[5] 里的键值对数量2 > 阈值1,所以调用 rehash() 方法扩容,
private void rehash(HashEntry<K,V> node) {
/*
- Reclassify nodes in each list to new table. Because we
- are using power-of-two expansion, the elements from
- each bin must either stay at same index, or move with a
- power of two offset. We eliminate unnecessary node
- creation by catching cases where old nodes can be
- reused because their next fields won’t change.
- Statistically, at the default threshold, only about
- one-sixth of them need cloning when a table
- doubles. The nodes they replace will be garbage
- collectable as soon as they are no longer referenced by
- any reader thread that may be in the midst of
- concurrently traversing table. Entry accesses use plain
- array indexing because they are followed by volatile
- table write.
*/
//旧数组引用
HashEntry<K,V>[] oldTable = table;
//旧数组长度
int oldCapacity = oldTable.length;
//新数组长度为旧数组的2倍
int newCapacity = oldCapacity << 1;
//修改新的阈值
threshold = (int)(newCapacity * loadFactor);
//创建新表
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
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);
}
}
}
}
//添加新的节点(链表头插入方式)
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
同样是扩容转移,这里的代码比 HashMap 中的 transfer 多了一些操作,在上上篇学习 HashMap 扩容可知,扩容后键值对的新位置要么和原位置一样,要么等于原位置+旧数组的长度,所以画个图来理解下上面代码这么写的原因:
前提:当前 HashEntry[] 长度为8,阈值为 8*0.75 = 6,所以 put 第7个键值对需要扩容 ,盖伦和亚索扩容前后位置不变,妖姬和卡特扩容后位置需要加上原数组长度,所以执行上面代码流程:
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
1715868412625)]
[外链图片转存中…(img-vwyDX5Ub-1715868412626)]
[外链图片转存中…(img-J9wBwSOD-1715868412627)]
[外链图片转存中…(img-Qzy4iP23-1715868412629)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!