JDK 源码笔记
ArrayList
核心就是
newCapacity
方法,这个方法用于确定扩容后的数组大小,正常是原来的 1.5 倍(老二进制运算了),若扩容后仍不够大,则仅保证能放下新加入的数据即可(当使用 ``addAll方法时可能触发);若扩容后溢出,则仅保证能放下新加入的数据即可;若扩容后逼近溢出,则返回
MAX_ARRAY_SIZE或
Integer.MAX_VALUE;另外两次扩容后过大也会检查
minCapacity` 是否溢出,防止数据错误。
HashMap
核心是根据 hash 取数组下标 index,并且 1.8 的扩容后 rehash 利用二进制高低位优化了计算下标的效率;扩容为原来的两倍,即左移 1 位。
为什么 HashMap
的初始容量以及扩容后的容量均为 2 的指数幂
因为计算机做运算时, 取模运算速度远远慢于位运算, 而若容量始终为 2 的指数幂, 则根据 hash 获取数组下标时只需要 使用
(数组长度-1) & hash 值
即可确定数组下标, 与取模得到的下标一样可靠.而扩容后后, 因为需要进行 rehash 运算来确定 数据的新下标, 多次进行取数组下标则更能体现位运算的优势.
为什么 HashMap
的加载因子是 0.75 (3/4)
1 | 使用排除法: |
为什么 HashMap
1.8 扩容无需 rehash
1 | 1. 因为1.8的获取 hash 值的算法优化了. 无需一个 hashSeed 进行辅助运算 (主因) |
总得来说, 1.8 优化了 hash 算法, 使
hashcode
的高 16 位与 低 16 位进行异或运算, 降低了碰撞率而 resize 算法也优化链表节点的迁移, 避免了 1.7 的链环产生
最大的区别就是, 1.7 没有将二进制的神奇发挥到极致, 依然像普通 java 程序一般逻辑. 而 1.8 则充分利用了二进制的优点(也充分的让人头晕), 提高了
HashMap
的效率.
为什么 HashMap
从链表达到 8 个时转成红黑树, 达到 6 个时转回链表?
1 | 1.根据 Poisson distribution 定律, 凑齐8个节点碰撞到同一个下标, 组成长度为 8 的链表概率极低, 约为 0.00000006, 而超过 8 个的几率则更低, 大约为千万分之一. 所以将阈值设置为 8, 因为这种概率极低. 因此可以减少链表转红黑树的, 提高增删改效率. |
JUC
ReentrantLock
加锁
1 | // AQS 的 acquire: AbstractQueuedSynchronizer#acquire() |
总结:先判断后更新 state 状态(即是否有线程持有此锁),被持有则加入队列并睡眠;醒来后再重复判断 state 过程,若持有成功则更新队列(主要目的是令我的下一个节点被唤醒后可以尝试持有锁,因为一醒来就是判断 node.prev==head)。
释放锁
1 | // AQS 的 release:AbstractQueuedSynchronizer#release() |
总结:先更新后判断 state 状态,若为 0,则代表可唤醒下一个节点,找到下一个节点,唤醒即可!
从上得到可重入锁原理,同一线程第二次加锁,会令 state+1,
而释放锁时,会先令 state-1,若 state 减到 0,才唤醒下一个节点的线程。
AQS
1 | AQS |
总结:一个具体的 AQS 维护一个队列以及 state 数据,总是通过 tryAcquire、tryRelease 维护 state,并在 acquire 中完成队列的添加与移除。
AQS 维护的队列总是在尾部增加节点,并且移除节点时,总是移除 head 节点。
通过实行具体的 tryAcquire、tryRelease 方法,可控制 线程是否等待,何时唤醒线程。
再总结: AQS 的主要作用就是将线程加入到队列、移除队列、控制休眠和唤醒。
ReentrantReadWriteLock
- 读锁加锁:只要 state 中没有写锁,又没有超过最大数量(受 32 位二进制长度限制),则能加锁成功(非公平锁不进队列等待,公平锁只可能会排在队列中写锁节点后面);
- 读锁解锁:先减少 state 中的读锁数量,减到 0 则可触发 releaseShared 来唤醒队列中可能存在的写锁节点;
- 写锁加锁:若没有写锁也没有读锁,令 state+1 写锁,记录重入次数;
- 写锁解锁:先减少 state (可重入锁),若写锁数量为 0,则可唤醒队列中的下一个节点,若下一个节点为读锁加入的节点,则会唤醒下下个是读锁的节点,重复,则唤醒大量读锁节点。
总结:重写 AQS 的核心方法,使其得以控制读锁、写锁能否加锁成功,利用 AQS 的 SHARED 节点在 releaseShared 时的传播性,使得写锁结束后,可以唤醒一连串的读锁。
CountDownLatch
1 | // new CountDownLatch(n):设置 state 的初始值为 n,使得需要调用 n 次 countDown 才会唤醒调用 await 的线程 |
总结:利用 releaseShared 会传播唤醒队列上相连的 SHARED 节点特性和 state 的维护,使 await 的线程进入休眠队列,直到 countDown 调用次数足够多才会唤醒这些线程。
Semaphore
1 | // new Semaphore(n):设置 state 的初始值为 n,当想唤醒调用 acquire(x) 的线程,则需要调用 release(x-n) |
总结:利用 AQS 可控制线程休眠和唤醒的特性,重写 tryAcquireShared、tryReleaseShared 方法实现逻辑;另外,虽然 acquire、release 的语义与正常 AQS 保持一致,但其对 state 的操作(加减)却是反着来的。
CyclicBarrier
含义: 凑足一定个数线程, 然后批量唤醒.
await(): 利用 ReenrantLock 的 lock 和 condition 的 await 进入休眠
当凑足后,用condition 的 singleAll 唤醒所有 await 的线程.
ConcurrentHashMap
不同之处有二
一是 put 一个新的 key 时,会使用 CAS 确保这次 put 没有线程竞争;
二是 put 覆盖一个 key 时,直接使用 synchronized 同步块进行同步。
ThreadPoolExecutor
1 | // java.util.concurrent.ThreadPoolExecutor#execute |
- 若当前线程数少于核心线程数,则添加一个线程来执行任务,直到线程数超过核心数;
- 超过之后,会放入到 workQueue 队列中,并重新检查线程池状态及是否有线程可执行队列中的任务
- 若放入失败(即队列已满),则尝试添加一个线程来执行这个任务,此时若线程达到最大线程数,则会失败并调用 reject 方法触发用户设置的拒绝策略。
总结:线程池先创建满核心线程数,超出放入队列,队列也放不下则会新建线程来救急,如果救急的线程创建过多,最终总线程数超过最大线程数,则触发拒绝策略,线程池不再接收任务,除非队列空出位置或线程数量降下来。