JUC

JUC 核心知识点整理

26-4-7 与 Arwen 复习整理

目录


一、ConcurrentHashMap

1.7 结构

  • 数据结构: Segment数组 + HashEntry数组 + 链表,头插法
  • 锁机制: Segment继承ReentrantLock,锁的粒度是整个Segment,一个Segment下挂多个槽位
  • 并发度: 默认16个Segment,最多16个线程同时写不同Segment

1.8 结构

  • 数据结构: Node数组 + 链表/红黑树,尾插法
  • 锁机制: synchronized锁住槽位的头节点,粒度从段级别细化到槽级别
  • 空槽位写入: 不加锁,用CAS(Unsafe.compareAndSwapObject),期望值为null,只有一个线程能成功,失败的线程重新走put流程进入synchronized逻辑
  • ABA问题: 此场景不存在。槽位从null→非null是单向的,扩容时旧槽位放的是ForwardingNode而不是null,构不成ABA

链表转红黑树

  • 树化条件: 链表长度 ≥ 8 数组长度 ≥ 64,两个条件同时满足才树化
  • 退化条件: 红黑树节点数 ≤ 6 时退回链表
  • 为什么不都是8: 防止在临界值附近频繁转换(抖动),6和8之间留7作为缓冲带
  • 为什么数组要≥64: 数组小的时候碰撞多是因为槽位不够,正确做法是扩容rehash分散碰撞,而不是树化。树化是最后手段

size() 计数机制

  • 内部维护 baseCount + CounterCell[],机制和LongAdder一致
  • 写入时: put成功+1,remove成功-1。先CAS改baseCount,失败则根据线程hash找一个CounterCell去CAS
  • 读取时: size() = baseCount + 所有CounterCell求和
  • Cell数组和槽位没有映射关系,纯粹分散计数竞争
  • size()是近似值,不是强一致的
  • CounterCell用 @Contended 防伪共享

二、LongAdder

为什么要LongAdder

并发场景下需要对一个数做高频的+1/-1操作。最直觉的做法是用AtomicLong,所有线程CAS同一个变量。问题是并发越高,CAS失败越多,失败了就自旋重试,大量线程空转抢一个变量,吞吐量急剧下降,这叫热点竞争

LongAdder的思路:既然抢一个变量是瓶颈,那就把这个变量拆成多个格子,让线程分散去写,读的时候再加起来。

底层数据结构

内部维护两部分:

  • base:一个long值,竞争不激烈时线程直接CAS改这里
  • Cell[]:一个Cell数组,每个Cell内部包一个long值

写入流程:

  1. 线程先尝试CAS修改base
  2. 如果CAS成功,直接返回
  3. 如果CAS失败(说明有竞争),根据线程hash值映射到Cell[]中的某个Cell,对该Cell做CAS
  4. 如果Cell的CAS也失败,会rehash换一个Cell重试,或者对Cell[]扩容

读取流程:

  • sum() = base + Cell[0].value + Cell[1].value + ... + Cell[n].value
  • 遍历求和,但求和过程中不加锁,结果是近似值

Cell细节:

  • 每个Cell用 @Contended 注解做缓存行填充,防止伪共享(false sharing)

本质

空间时间,牺牲了精确性


三、AQS

是什么

JUC中绝大多数同步工具的底层骨架。提供:一个状态变量 + 一个等待队列 + 一组模板方法。子类只需定义”获取和释放的语义”,排队、阻塞、唤醒AQS全包。

基于AQS的实现:

实现state语义
ReentrantLock重入次数,0=无人占,1=被占,>1=重入
CountDownLatch剩余计数
Semaphore剩余许可
ReentrantReadWriteLock高16位读锁计数,低16位写锁计数

两个核心组件

1. state(int类型)

  • volatile修饰,保证可见性
  • 通过CAS修改,保证原子性
  • 语义由子类定义,AQS本身不关心state代表什么

2. CLH等待队列(双向链表)

  • 每个节点是一个Node,代表一个等待中的线程
  • 有虚拟head节点(哨兵),不代表任何线程
  • Node的waitStatus:
    • 0:初始状态
    • SIGNAL(-1):后继节点需要被唤醒(是对后继的承诺,不是自己的状态)
    • CANCELLED(1):线程取消了,跳过
    • CONDITION(-2):在Condition队列中
    • PROPAGATE(-3):共享模式下释放需要向后传播
代码块PLAINTEXT · 1 行收起展开
head(哨兵) ⇄ Node(线程A) ⇄ Node(线程B) ⇄ Node(线程C) = tail

独占模式——获取流程

代码块JAVA · 14 行收起展开
acquire(arg)
tryAcquire(arg)          // 子类实现,尝试一次获取
    → 成功:返回
    → 失败:
addWaiter()           // 封装成Node,CAS加入队尾
acquireQueued()       // 自旋+阻塞
          for(;;) {
            前驱是head?→ 再试tryAcquire → 成功则自己变成新head
            不是或失败 → shouldParkAfterFailedAcquire()
                         把前驱waitStatus设为SIGNAL
parkAndCheckInterrupt()
                         LockSupport.park() 阻塞
                       → 被唤醒后回到for循环再试
          }

关键细节:

  • 先设prev再CAS设tail,保证从后往前遍历安全
  • 不是入队就立刻阻塞,先看自己是不是排第一个
  • 必须先把前驱设成SIGNAL再park,否则release时不知道要唤醒后继

独占模式——释放流程

代码块JAVA · 5 行收起展开
release(arg)
tryRelease(arg)          // state减,归零返回true
true
unparkSuccessor()    // 从head.next开始找第一个有效节点
        → LockSupport.unpark() 唤醒
  • 从tail往前找有效节点,不是从head往后找(因为next可能还没设上)
  • 只唤醒一个节点,不是惊群

公平与非公平

唯一区别在tryAcquire():

  • 非公平: 新线程直接CAS抢state,不管队列
  • 公平: 先看队列里有没有人排着,有就乖乖排队
  • 非公平性能更好但可能导致饥饿

共享模式

与独占模式的核心区别:

  • 独占:一个线程获取成功后,其他线程必须等
  • 共享:多个线程可以同时获取成功
  • 传播唤醒: 一个线程被唤醒拿到资源后,如果还有剩余资源,继续唤醒后面的节点,形成链式传播

Condition队列

  • 每个Condition维护一个独立的单向等待队列,与CLH队列是两个独立队列
  • condition.await():完全释放锁 → 从CLH队列移到Condition队列 → park阻塞
  • condition.signal():把Condition队列头节点移回CLH队列尾部 → 重新竞争锁
代码块PLAINTEXT · 2 行收起展开
await():  CLH队列 → Condition队列(释放锁,阻塞)
signal(): Condition队列 → CLH队列(重新排队竞争锁)

四、CountDownLatch

是什么

一次性的线程等待工具。一方线程await()阻塞等待,另一方线程countDown()通知。计数归零时所有等待线程被唤醒。

底层机制

  • AQS的state = 初始计数n
  • countDown():CAS将state减1,减到0时唤醒所有等待线程
  • await():检查state是否为0,不为0则入CLH队列park阻塞
  • 一次性:没有方法把state重新设回n

核心特征

等的人和通知的人是两拨,通知的人之间互不等待

代码块JAVA · 3 行收起展开
CountDownLatch latch = new CountDownLatch(5);
// 工人线程:doWork() → latch.countDown()(干完就走)
// 主线程:  latch.await()(等所有工人完成)

五、CyclicBarrier

是什么

可循环使用的屏障。所有参与线程都调await(),最后一个到达时全部放行,屏障自动重置。

底层机制

  • 不是基于AQS,而是基于ReentrantLock + Condition
  • 内部维护count计数器,初始值 = parties
  • await():加锁 → count减1 → 不为0则condition.await()阻塞 → 减到0则signalAll()唤醒所有人 + 重置count

核心特征

所有人既是等待者也是通知者,没有主从之分

CDL vs CB 对比

CountDownLatchCyclicBarrier
谁等谁一方等另一方所有人互等
可否复用一次性自动重置,可循环
底层实现AQSReentrantLock + Condition
到达后行为countDown后可以继续走必须等所有人到齐
额外能力可传入barrierAction,到齐时执行

六、Semaphore

是什么

信号量,控制同时访问某个资源的线程数量。本质是许可计数器。

底层机制

  • AQS的state = 许可数n
  • acquire():CAS将state减1,减到0则阻塞
  • release():CAS将state加1,唤醒一个等待线程
  • 支持公平/非公平模式

和 ReentrantLock 的区别(许可数=1时)

Semaphore(1)ReentrantLock
所有权无,线程A acquire线程B可以release有,谁lock的必须谁unlock
可重入不可重入,同一线程acquire两次死锁可重入
语义资源许可互斥

七、ThreadPoolExecutor

核心参数

参数含义
corePoolSize核心线程数
maximumPoolSize最大线程数
keepAliveTime非核心线程空闲存活时间
workQueue任务队列(BlockingQueue)
threadFactory线程工厂
handler拒绝策略

ctl字段

一个AtomicInteger同时存两个信息:

  • 高3位:线程池状态(RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED)
  • 低29位:当前工作线程数

execute() 流程

代码块PLAINTEXT · 8 行收起展开
提交任务
  → 工作线程数 < corePoolSize?
    → Yes:创建核心线程执行
    → No:workQueue.offer() 入队成功?
      → Yes:double-check状态
      → No(队列满):创建非核心线程?
        → Yes:非核心线程执行
        → No(达到maximumPoolSize):拒绝策略

顺序:核心线程 → 队列 → 非核心线程 → 拒绝策略

Worker 工作循环

代码块PLAINTEXT · 3 行收起展开
Worker.run() → runWorker()
  → while循环 { 执行firstTask → getTask()从队列取 → 执行 → 取 → ... }
  → getTask()返回null → 线程退出

getTask()——决定线程生死

  • 核心线程:workQueue.take(),无限等待,永不超时
  • 非核心线程:workQueue.poll(keepAliveTime),超时没任务返回null,线程退出
  • 这就是keepAliveTime生效的地方

四种拒绝策略

策略行为
AbortPolicy(默认)抛 RejectedExecutionException
CallerRunsPolicy调用者线程自己执行任务
DiscardPolicy静默丢弃
DiscardOldestPolicy丢弃队列最老的任务,重新提交当前任务

八、BlockingQueue

ArrayBlockingQueue vs LinkedBlockingQueue

ArrayBlockingQueueLinkedBlockingQueue
底层结构数组链表
有界/无界有界,构造时必须指定容量默认无界(Integer.MAX_VALUE)
锁设计一把锁,put和take共用两把锁,putLock和takeLock分开
并发性能较低,生产消费互斥较高,生产消费可同时进行

为什么LinkedBQ能用两把锁: 链表头尾是两个独立节点,放在尾部取在头部,操作不同内存。数组是共享连续内存,放和取都操作count和下标,无法拆开。

其他实现

  • SynchronousQueue: 不存储元素,put必须有人正在take才能成功,一手交一手
  • PriorityBlockingQueue: 带优先级排序,无界
  • DelayQueue: 元素到了延迟时间才能取出

BlockingQueue vs 线程池

  • BlockingQueue满了 → put() 阻塞等待,等别人take腾空间
  • 线程池用的是 offer(),满了直接返回false不阻塞,再走创建非核心线程 → 拒绝策略的逻辑

九、LockSupport

是什么

JUC底层的线程阻塞/唤醒工具,AQS内部用的就是它。

vs Object.wait()/notify()

LockSupportwait/notify
前置条件无,随时可调必须在synchronized块内
顺序问题先unpark后park,不会阻塞(许可存着)先notify后wait,永远阻塞(notify白费了)
精确性unpark(指定线程),精确唤醒notify()随机唤醒一个

许可模型(燃料模型)

  • unpark(thread):给一块燃料(许可),最多存一块
  • park():消耗一块燃料,有就直接通过,没有就阻塞等
  • 许可不累积: 连续unpark两次 → 还是只有一块燃料 → 第一次park通过,第二次park阻塞