From adea89547acc567ed838c4f3dba44c9f908d0fb0 Mon Sep 17 00:00:00 2001 From: godelgnis <2275433755@qq.com> Date: Sun, 21 May 2023 00:01:45 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(aqs.md):=20=E4=BF=AE=E6=AD=A3Semaphore?= =?UTF-8?q?=E5=8E=9F=E7=90=86=E6=8F=8F=E8=BF=B0=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?tryAcquireShared=20=E5=92=8C=20tryReleaseShared=20=E6=BA=90?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/concurrent/aqs.md | 46 ++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/java/concurrent/aqs.md b/docs/java/concurrent/aqs.md index e004b872..d79fdda8 100644 --- a/docs/java/concurrent/aqs.md +++ b/docs/java/concurrent/aqs.md @@ -154,7 +154,9 @@ public Semaphore(int permits, boolean fair) { `Semaphore` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `permits`,你可以将 `permits` 的值理解为许可证的数量,只有拿到许可证的线程才能执行。 -调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state >= 0` 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果 `state<0` 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。 +调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state > 0` 的话,则表示可以获取成功,如果 `state <= 0` 的话,则表示许可证数量不足,获取失败。 + +如果可以获取成功的话(`state > 0` ),会尝试使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果获取失败则会创建一个 Node 节点加入阻塞队列,挂起当前线程。 ```java /** @@ -170,13 +172,40 @@ public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); - // 尝试获取许可证,arg为获取许可证个数,当可用许可证数减当前获取的许可证数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。 + // 尝试获取许可证,arg为获取许可证个数,当获取失败时,则创建一个节点加入阻塞队列,挂起当前线程。 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } +/** + * 共享模式下尝试获取资源(在Semaphore中的资源即许可证): + * 1、获取失败,返回负值 + * 2、共享模式下获取成功,但后续的共享模式获取会失败,返回0 + * 3、共享模式获取成功,随后的共享模式也可能获取成功,返回正值 + */ +protected int tryAcquireShared(int acquires) { + return nonfairTryAcquireShared(acquires); +} +/** + * 非公平的共享模式获取许可证,acquires为许可证数量,根据代码上下文可知该值总是为1 + * 注:公平模式的实现会先判断队列中是否有节点在排队,有则直接返回-1,表示获取失败,没有则执行下面的操作 + */ +final int nonfairTryAcquireShared(int acquires) { + for (;;) { + // 当前可用许可证数量 + int available = getState(); + /* + * 尝试获取许可证,当前可用许可证数量小于等于0时,返回负值,表示获取失败, + * 当前可用许可证大于0时才可能获取成功,CAS失败了会循环重新获取最新的值尝试获取 + */ + int remaining = available - acquires; + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } +} ``` -调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state>=0` 则获取令牌成功,否则重新进入阻塞队列,挂起线程。 +调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state > 0` 则获取令牌成功,否则重新进入阻塞队列,挂起线程。 ```java // 释放一个许可证 @@ -194,6 +223,17 @@ public final boolean releaseShared(int arg) { } return false; } +// 尝试释放资源 +protected final boolean tryReleaseShared(int releases) { + for (;;) { + int current = getState(); + int next = current + releases; // 可用许可证+1 + if (next < current) // overflow + throw new Error("Maximum permit count exceeded"); + if (compareAndSetState(current, next)) // 通过CAS修改 + return true; + } +} ``` #### 实战 From 8d464bf9019619815391cef8f10189226af7f556 Mon Sep 17 00:00:00 2001 From: godelgnis <2275433755@qq.com> Date: Sun, 21 May 2023 12:17:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(hashmap-source-code.md):=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=90=8D=E8=AF=8D=E7=9A=84=E4=BD=BF=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=8E=9F=E7=90=86=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/collection/hashmap-source-code.md | 51 +++++++++++++-------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/docs/java/collection/hashmap-source-code.md b/docs/java/collection/hashmap-source-code.md index 21105837..5e82eaa7 100644 --- a/docs/java/collection/hashmap-source-code.md +++ b/docs/java/collection/hashmap-source-code.md @@ -13,7 +13,7 @@ HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现, `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个 -JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 +JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 `HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。 @@ -78,11 +78,11 @@ public class HashMap extends AbstractMap implements Map, Cloneabl static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; - // 默认的填充因子 + // 默认的负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; - // 当桶(bucket)上的结点数大于这个值时会转成红黑树 + // 当桶(bucket)上的结点数大于等于这个值时会转成红黑树 static final int TREEIFY_THRESHOLD = 8; - // 当桶(bucket)上的结点数小于这个值时树转链表 + // 当桶(bucket)上的结点数小于等于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小容量 static final int MIN_TREEIFY_CAPACITY = 64; @@ -94,24 +94,24 @@ public class HashMap extends AbstractMap implements Map, Cloneabl transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; - // 临界值(容量*填充因子) 当实际大小超过临界值时,会进行扩容 + // 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容 int threshold; - // 加载因子 + // 负载因子 final float loadFactor; } ``` -- **loadFactor 加载因子** +- **loadFactor 负载因子** - loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。 + loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。 **loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值**。 - 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 \* 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 + 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 \* 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 - **threshold** - **threshold = capacity \* loadFactor**,**当 Size>=threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 + **threshold = capacity \* loadFactor**,**当 Size>threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 **Node 节点类源码:** @@ -201,7 +201,7 @@ HashMap 中有四个构造方法,它们分别如下: this(initialCapacity, DEFAULT_LOAD_FACTOR); } - // 指定“容量大小”和“加载因子”的构造函数 + // 指定“容量大小”和“负载因子”的构造函数 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); @@ -210,10 +210,13 @@ HashMap 中有四个构造方法,它们分别如下: if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; + // 初始容量暂时存放到 threshold ,在resize中再赋值给 newCap 进行table初始化 this.threshold = tableSizeFor(initialCapacity); } ``` +> 值得注意的是上述四个构造方法中,都初始化了负载因子 loadFactor,由于HashMap中没有 capacity 这样的字段,即使指定了初始化容量 initialCapacity ,也只是通过 tableSizeFor 将其扩容到与 initialCapacity 最接近的2的幂次方大小,然后暂时赋值给 threshold ,后续通过 resize 方法将 threshold 赋值给 newCap 进行 table 的初始化。 + **putMapEntries 方法:** ```java @@ -222,18 +225,25 @@ final void putMapEntries(Map m, boolean evict) { if (s > 0) { // 判断table是否已经初始化 if (table == null) { // pre-size - // 未初始化,s为m的实际元素个数 + /* + * 未初始化,s为m的实际元素个数,ft=s/loadFactor => s=ft*loadFactor, 跟我们前面提到的 + * 阈值=容量*负载因子 是不是很像,是的,ft指的是要添加s个元素所需的最小的容量 + */ float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); - // 计算得到的t大于阈值,则初始化阈值 + /* + * 根据构造函数可知,table未初始化,threshold实际上是存放的初始化容量,如果添加s个元素所 + * 需的最小容量大于初始化容量,则将最小容量扩容为最接近的2的幂次方大小作为初始化。 + * 注意这里不是初始化阈值 + */ if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,并且m元素个数大于阈值,进行扩容处理 else if (s > threshold) resize(); - // 将m中的所有元素添加至HashMap中 + // 将m中的所有元素添加至HashMap中,如果table未初始化,putVal中会调用resize初始化或扩容 for (Map.Entry e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); @@ -276,7 +286,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, // 桶中已经存在元素(处理hash冲突) else { Node e; K k; - // 判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。 + //快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; @@ -401,7 +411,7 @@ final Node getNode(int hash, Object key) { ### resize 方法 -进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。 +进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。resize方法实际上是将 table 初始化和 table 扩容 进行了整合,底层的行为都是给 table 赋值一个新的数组。 ```java final Node[] resize() { @@ -420,14 +430,16 @@ final Node[] resize() { newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold + // 创建对象时初始化容量大小放在threshold中,此时只需要将其作为新的数组容量 newCap = oldThr; else { - // signifies using defaults + // signifies using defaults 无参构造函数创建的对象在这里计算容量和阈值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } - // 计算新的resize上限 if (newThr == 0) { + // 创建时指定了初始化容量或者负载因子,在这里进行阈值初始化, + // 或者扩容前的旧容量小于16,在这里计算新的resize上限 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } @@ -442,8 +454,10 @@ final Node[] resize() { if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) + // 只有一个节点,直接计算元素新的位置即可 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) + // 将红黑树拆分成2棵子树,拆分后的子树节点数小于等于6,则将树转化成链表 ((TreeNode)e).split(this, newTab, j, oldCap); else { Node loHead = null, loTail = null; @@ -486,6 +500,7 @@ final Node[] resize() { } ``` + ## HashMap 常用方法测试 ```java