JUC常见核心知识梳理02 AQS ReentrantLock CAS 原子类 线程池

五 AQS 与 JUC 显式锁体系

AQS:JUC 的灵魂

本质AbstractQueuedSynchronizer = 一个 volatile int state(资源状态) + 一个 FIFO 双向等待队列(CLH 变体) + park/unpark 阻塞恢复机制 ,用模板方法模式让你只写"怎么抢资源",其余排队/唤醒由框架代劳。

组成 说明
state 32 位 volatile int,用 CAS 更新(早期试过 long,跨平台表现不好)
同步队列 双向链表,借鉴 CLH;headDummy 哨兵节点(不关联线程)
阻塞原语 LockSupport.park/unpark(不是 suspend/resume
两种模式 独占(EXCLUSIVE)/ 共享(SHARED)

子类需实现的方法 (默认都抛 UnsupportedOperationException):

tryAcquire / tryRelease / tryAcquireShared / tryReleaseShared / isHeldExclusively

Node 的 waitStatus 取值

常量 含义
1 CANCELLED 已取消(超时/中断)
-1 SIGNAL 后继节点待唤醒(释放锁时要 unpark 它)
-2 CONDITION 在条件队列里等
-3 PROPAGATE 共享模式下唤醒需向后传播
0 --- 初始状态

ReentrantLock vs synchronized

维度 synchronized ReentrantLock
层面 JVM 关键字,自动释放 JDK API,必须 finally 手动 unlock
可重入 ✅(state++/state--
可中断 lockInterruptibly()
超时放弃 tryLock(timeout)
公平性 仅非公平 new ReentrantLock(true) 公平锁
条件变量 1 个(WaitSet) 多个 newCondition()
读写分离 配合 ReentrantReadWriteLock
java 复制代码
lock.lock();
try { /* 临界区 */ } finally { lock.unlock(); }   // ❗unlock 必须放 finally

非公平锁加锁流程

复制代码
lock() → CAS(state 0→1) 成功? → setExclusiveOwnerThread(自己),结束
              │ 失败
              ▼
       acquire(1) → tryAcquire → addWaiter(EXCLUSIVE) 入队尾
              → acquireQueued 死循环:
                   前驱是 head 且 tryAcquire 成功 → 出队、设自己为 head
                   否则 shouldParkAfterFailedAcquire 把前驱 waitStatus 改成 -1
                   再次失败 → park() 阻塞

六个源码细节

  1. 非公平体现在nonfairTryAcquirestate==0直接 CAS 抢完全不看队列里有没有人排队 ;公平锁则先 hasQueuedPredecessors()
  2. 是否需要 unpark 取决于前驱节点的 waitStatus == SIGNAL(-1),而不是本节点状态。
  3. shouldParkAfterFailedAcquire 第一次返回 false(把前驱改成 -1),第二次才可能返回 true 真正 park。
  4. 解锁必须 state 减到 0 才算释放成功,重入一次就要 unlock 一次。
  5. unparkSuccessor从队列尾部向前找最近的有效节点(因为入队时 next 指针可能还没连好)。
  6. 不可打断模式 下,被 interrupt 的线程会继续待在队列里抢锁,抢到后才通过 selfInterrupt() 补上中断;可打断模式 lockInterruptibly() 则在 park 中被打断时直接抛 InterruptedException 并取消排队。

Condition(条件变量)

  • 每个 Condition 一条独立的条件队列(firstWaiter/lastWaiter
  • await() = 建 Node.CONDITION(-2) 入条件队列 → fullyRelease 一次性释放全部重入的 state → park
  • signal() = 把条件队列首节点转移到同步队列尾部,它还要重新抢锁
  • 中断模式常量THROW_IE(-1) 抛异常,REINTERRUPT(1) 重设中断标记

ReentrantReadWriteLock

核心:state 高 16 位 = 读锁计数,低 16 位 = 写锁计数sharedCount(c) = c >>> 16exclusiveCount(c) = c & 0xFFFF)。

组合 是否并发
读-读 ✅ 并发
读-写 ❌ 互斥
写-写 ❌ 互斥

三条必背结论

  1. 读锁不支持条件变量newCondition() 抛异常)
  2. 不支持锁升级 :持有读锁再去拿写锁 → 死锁
  3. 支持锁降级:持有写锁 → 拿读锁 → 释放写锁(保证数据可见性的常用手法)
java 复制代码
// 锁降级示例(保证本线程修改后的数据对后续读可见)
w.lock();
try {
    data = update();     // 写
    r.lock();            // 降级:先拿读锁
} finally { w.unlock(); } // 再放写锁
try { use(data); } finally { r.unlock(); }

StampedLock(JDK 8)

三种模式:写锁 / 悲观读锁 / 乐观读

java 复制代码
long stamp = lock.tryOptimisticRead();   // 乐观读,不加锁
int cur = x, curY = y;
if (!lock.validate(stamp)) {             // 期间有没有人写过?
    stamp = lock.readLock();             // 有 → 升级为悲观读
    try { cur = x; curY = y; } finally { lock.unlockRead(stamp); }
}

特点不可重入不支持条件变量 ;乐观读完全无锁,读极多写极少时性能最好。

三把"工具锁"对比

工具 核心 典型用途 可重用
Semaphore permitsstateacquire/release 限流、资源池、连接池
CountDownLatch 计数递减,await() 阻塞到 0 主线程等 N 个子任务完成 ❌ 一次性
CyclicBarrier 线程 await() 到齐后放行 多线程分阶段同步(如多人加载完再开局) reset()

提醒CyclicBarrier 构造时设置的数量必须与实际线程数一致,否则永远凑不满,达不到"人满发车"的效果。

CountDownLatchCyclicBarrier 的本质区别:前者是一个/少数线程等别人(事件计数),后者是线程之间互相等(线程计数)且可循环使用


六 无锁:CAS 与原子类

CAS 本质

CAS(Compare And Swap) = compareAndSet(expect, update):内存值等于期望值才更新,否则失败重试。底层是 CPU 原子指令 cmpxchg(多核下加 lock 前缀锁总线/缓存行)。

两个前提

  • CAS 必须在多核 CPU 下使用 ,且最好是 CPU 核数 ≥ 线程数------单核下自旋重试纯属空转。
  • CAS 要配合 volatile 读,才能拿到最新值。

CAS 的三大问题

问题 表现 解法
ABA 值从 A→B→A,CAS 以为没变过 AtomicStampedReference(带版本号)/ AtomicMarkableReference(带 boolean 标记)
自旋开销 高竞争下 CPU 空转 LongAdder 分段;或退化为锁
只能操作一个变量 无法原子更新多个字段 封装成对象用 AtomicReference

原子类全家桶

类别 代表 用途
原子基本类型 AtomicInteger / AtomicLong / AtomicBoolean 单变量原子更新
原子引用 AtomicReference<T> 对象引用原子更新
带版本号引用 AtomicStampedReference<T> 解决 ABAgetStamp()
带标记引用 AtomicMarkableReference<T> 只关心"是否被改过"
原子数组 AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray 保护数组元素(不是数组本身)
字段更新器 AtomicIntegerFieldUpdater / AtomicReferenceFieldUpdater 对象的某个 volatile 字段 原子更新(省内存;字段必须 volatile、不能是 static final
原子累加器 LongAdder / DoubleAdder(JDK 8) 高并发计数场景首选 ,远快于 AtomicLong

LongAdder 为什么快 ------分段 + 伪共享

复制代码
AtomicLong:所有线程 CAS 同一个 value → 竞争激烈,大量失败重试
LongAdder :base + Cell[] 数组 → 不同线程打散到不同 Cell,最后 sum() 求和
  • 无竞争时直接累加 base;有竞争才创建 CounterCell[]初始 2 个,竞争激烈再扩容)。
  • 伪共享(False Sharing) :CPU 缓存行 64 字节 ,多个 Cell 挨在一起会互相失效 → 用 @sun.misc.Contended 注解填充(需配合 -XX:-RestrictContended)。
  • 代价:sum() 结果不保证强一致 (累加过程中可能有并发写),适合计数统计而非精确控制。

选型 :追求精确 (如库存、序列号)用 AtomicLong;追求吞吐 (如 QPS 统计、词频统计)用 LongAdder

Unsafe 与 VarHandle

  • UnsafeobjectFieldOffset() / compareAndSwapInt() 等,所有原子类的底层实现;getUnsafe() 受类加载器限制,通常用反射获取。
  • 【版本边界】JDK 9+ 官方推荐 VarHandleMethodHandles.lookup().findVarHandle(...)),支持 compareAndSetgetAndAdd 以及更细的内存屏障语义getVolatile / setOpaque / setRelease 等),是 Unsafe 的替代品。

乐观 vs 悲观

方案 实现 适合
悲观互斥 synchronized / Lock 冲突频繁、临界区大
乐观重试 CAS 自旋(如 AccountCasAtomicInteger + while(true) compareAndSet 冲突少、临界区小(如余额扣减)

七、不可变、享元与线程安全设计

不可变(Immutable)

实现要点 :类 final(防子类破坏)、字段 private final、不提供 setter、构造期间不 this 逃逸、内部可变对象做保护性拷贝

提醒"不可变类"在单个线程里是线程安全的,但在多线程里不能自动保证线程安全 ------因为引用本身的可变性 不被控制(例如 AtomicReference<String> 里的引用仍可能被改)。不可变保证的是"对象内容不变",不保证"引用不变"。

典型

说明
String 不可变,天然线程安全
LocalDateTime / DateTimeFormatter 不可变,可共享
SimpleDateFormat 非线程安全 (内部共享 Calendar),必须每次 new 或放 ThreadLocal

享元模式(Flyweight):复用有限对象

类型 缓存范围
Byte / Short / Long -128 ~ 127
Integer 默认 -128 ~ 127 ,上限可用 -XX:AutoBoxCacheMax=-Djava.lang.Integer.IntegerCache.high=
Character 0 ~ 127
Boolean TRUE / FALSE

经典应用:数据库连接池。讲义给出两种实现:

  • wait/notify 版AtomicIntegerArray states(0 空闲 / 1 繁忙) + synchronized + wait/notify
  • Semaphore 版(更优)new Semaphore(poolSize) 控制许可 + CAS 标记状态,性能和可读性更好

生产别手写:关系库用 HikariCP / Druid ,通用对象池用 Apache Commons Pool

无状态类

不含成员变量的类(如典型的无状态 Servlet / Controller)天然线程安全。


八 线程池(面试第一大考点)

七大参数

java 复制代码
new ThreadPoolExecutor(
    int corePoolSize,          // 1 核心线程数(即使空闲也保留,除非 allowCoreThreadTimeOut)
    int maximumPoolSize,       // 2 最大线程数
    long keepAliveTime,        // 3 非核心线程空闲存活时间
    TimeUnit unit,             // 4 时间单位
    BlockingQueue<Runnable> workQueue,  // 5 任务阻塞队列
    ThreadFactory threadFactory,        // 6 线程工厂(务必自定义线程名!)
    RejectedExecutionHandler handler    // 7 拒绝策略
);

执行流程(重点)

复制代码
提交任务
   │
   ├─ 运行中线程数 < corePoolSize ?  ──是──> 创建核心线程执行
   │                        │否
   │                        ▼
   ├─ 工作队列未满 ?        ──是──> 入队等待
   │                        │否
   │                        ▼
   ├─ 线程数 < maximumPoolSize ? ──是──> 创建非核心(救急)线程执行
   │                        │否
   │                        ▼
   └────────────────> 执行拒绝策略

易错 :队列满了才会创建非核心线程,不是"线程数到 core 就立刻扩到 max"。

ctl 字段 :一个 AtomicInteger高 3 位存线程池状态 (RUNNING / SHUTDOWN / STOP / TIDYING / TERMINATED),低 29 位存工作线程数------用一次 CAS 同时维护两个值。

四种拒绝策略

策略 行为 适用
AbortPolicy默认 RejectedExecutionException 常规业务,快速失败
CallerRunsPolicy 让提交任务的线程自己执行 重要任务、天然削峰(反馈式降速
DiscardPolicy 静默丢弃 无所谓的任务
DiscardOldestPolicy 丢弃队列里最老的任务,再尝试提交 允许丢弃旧数据

为什么禁止用 Executors 创建线程池?(必考)

工厂方法 隐患
newFixedThreadPool(n) 无界 LinkedBlockingQueue(默认容量 Integer.MAX_VALUE)→ 任务堆积 OOM
newCachedThreadPool() 核心 0、最大 Integer.MAX_VALUESynchronousQueue → 线程数暴涨 OOM
newSingleThreadExecutor() 同样无界队列
newScheduledThreadPool(n) 最大线程数 Integer.MAX_VALUE,同样有 OOM 风险

结论 :生产必须 new ThreadPoolExecutor(...) 显式指定有界队列、线程名、拒绝策略

线程数怎么定?(有公式)

  • CPU 密集型线程数 ≈ CPU 核数 + 1(+1 用于应对页缺失等暂停时顶上)

  • I/O 密集型(通用公式):

    线程数 = CPU核数 × 期望CPU利用率 × (CPU计算时间 + 等待时间) / CPU计算时间

例:4 核,CPU 占 50%、等待 50%,期望 100% 利用 → 4 × 100% × 100% / 50% = 8

例:4 核,CPU 占 10%、等待 90% → 4 × 100% × 100% / 10% = 40

  • 笔记补充:CPU 空闲(等待)时间越长,应创建的线程数越多。
  • 实践 :先按公式给初值,再压测 + 监控 调整;IO 密集可配动态线程池(如 Hippo4j / Dynamic TP)。

优雅关闭与异常处理

方法 行为
shutdown() 平缓关闭:不再接新任务,执行完队列中的任务
shutdownNow() 立刻:中断所有线程,返回队列中未执行的任务

异常处理的坑

  • execute() 提交:异常会打印堆栈 并且该线程直接死亡(但线程池会补一个新线程)
  • submit() 提交:异常被封装进 Future不调用 get() 就永远看不到("异常被吞")
  • 正确做法 :任务内部 try-catch 兜底 + 记录日志;或用 ThreadFactory 设置 UncaughtExceptionHandler

线程池的"饥饿"问题

现象:固定大小线程池里,任务 A 需要等待它提交的子任务 B 的结果,如果所有线程都在等 B,B 永远得不到线程执行 → 死锁。

复制代码
newFixedThreadPool(2):两个工人都去"点餐",点完餐要等"做菜"的结果 → 没人做菜 → 饥饿

解法不同任务类型使用不同线程池 (服务员池 + 厨师池),这也是生产上"线程池隔离"的依据。

Fork/Join 与工作窃取(JDK 7)

java 复制代码
class AddTask extends RecursiveTask<Integer> {   // 有返回值用 RecursiveTask,无返回值用 RecursiveAction
    public Integer compute() {
        if (end - begin == 1) return end + begin;
        int mid = (begin + end) / 2;
        AddTask t1 = new AddTask(begin, mid).fork();
        AddTask t2 = new AddTask(mid + 1, end).fork();
        return t1.join() + t2.join();
    }
}
new ForkJoinPool(4).invoke(new AddTask(1, 10));   // 55

工作窃取(Work-Stealing) :每个线程有自己的双端队列,空闲线程从其他队列尾部"偷"任务执行,提高 CPU 利用率。

ScheduledThreadPoolExecutor vs Timer

维度 Timer ScheduledThreadPoolExecutor
线程模型 单线程,串行 多线程池
异常 一个任务抛异常 → 整个 Timer 线程死掉 互不影响
时间基准 依赖绝对系统时间(改系统时间会出问题) 相对时间
结论 ❌ 已废弃 ✅ 推荐
java 复制代码
// 每周四 18:00 执行(用 Duration 精确计算首次延迟)
long initDelay = Duration.between(now, nextThursday18).toMillis();
executor.scheduleAtFixedRate(task, initDelay, 7*24*3600*1000, TimeUnit.MILLISECONDS);
// scheduleAtFixedRate:以上一次"开始时间"为基准(固定频率)
// scheduleWithFixedDelay:以上一次"结束时间"为基准(固定间隔)

CompletableFuture(JDK 8,异步编排)

java 复制代码
CompletableFuture.supplyAsync(() -> remoteCall(), computePool)   // 有返回值
                 .thenApply(r -> transform(r))                    // 同步转换
                 .thenAcceptAsync(r -> save(r), ioPool)           // 异步消费(换池)
                 .exceptionally(e -> { log(e); return null; });   // 异常兜底
CompletableFuture.allOf(f1, f2, f3).join();   // 等全部完成
  • 默认使用 ForkJoinPool.commonPool()共用池,易互相干扰,建议显式传自定义池
  • thenApply vs thenApplyAsync:前者可能复用同一线程,后者一定提交给线程池
  • 【版本边界】JDK 21+ IO 密集场景可以不用 CompletableFuture 编排------直接"每请求一条虚拟线程 + 同步代码"更简单

未完待续

相关推荐
CodeStats1 小时前
【Java数据类型】Java底层数据类型原理(上):基本类型、转换与封装核心
java·数据类型·string·封装·转换
陈皮波比茶1 小时前
SpringCloudAlibaba
java·spring cloud·微服务
重生之小比特1 小时前
【Java SE】程序逻辑控制
java·开发语言·python
find1star1 小时前
LeetCode 25:K 个一组翻转链表
java·数据结构·算法·leetcode·链表·职场和发展·动态规划
染的人1 小时前
Java 10x15cm面单PDF转换A4格式PDF
java·开发语言·pdf
泡海椒1 小时前
JQuick-java (JQuick-ASM)性能优化原理:字节码生成与缓存机制深度解析
java·缓存·性能优化
Bs_MoneyMagnet1 小时前
基于springboot+vue的家居生活商城平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
微三云 - 廖会灵 (私域系统开发)2 小时前
Spring Boot实战:五级分销定价与自动分佣系统设计与实现
java·spring boot·后端
Json____2 小时前
从零构建在线拍卖系统:Java 全栈开发实践
java·开发语言·管理系统·it学习·wwwoop.com