JAVA重点基础、进阶知识及易错点总结(20)Lock锁 + 线程高级特性

张开发
2026/4/6 17:35:16 15 分钟阅读

分享文章

JAVA重点基础、进阶知识及易错点总结(20)Lock锁 + 线程高级特性
Java 巩固进阶 · 第20天主题Lock锁 线程高级特性 —— 从够用到专业的并发控制 进度概览今天学习synchronized 的进阶替代方案。掌握ReentrantLock、volatile、Atomic系列让你的并发代码更灵活、更高效、更可控。 核心价值灵活控制tryLock()超时获取、lockInterruptibly()响应中断解决synchronized的死等问题。性能优化volatile保证可见性且无锁开销Atomic类用 CAS 实现无锁线程安全。框架基石理解 Redis 分布式锁、Spring 事务传播、AQS 同步器底层的核心思想。面试通关Lock vs synchronized、volatile 原理、CAS 机制是高级开发必考题型。一、为什么需要 Locksynchronized 的三大局限 1. synchronized 的硬伤// ❌ 局限1无法中断等待死锁时只能重启synchronized(lock){// 如果拿不到锁线程会一直阻塞无法响应 interrupt()doSomething();}// ❌ 局限2无法超时放弃高并发下可能雪崩synchronized(lock){// 拿不到锁就无限等待无法设置最多等3秒doSomething();}// ❌ 局限3非公平锁可能导致线程饥饿// 新来的线程可能插队拿到锁等待久的线程反而后执行2. Lock 接口的核心优势┌─────────────────────────────────────┐ │ Lock 接口java.util.concurrent.locks│ │ │ │ ✅ 可中断lockInterruptibly() │ │ 等待锁时可响应中断避免死锁僵持 │ │ │ │ ✅ 可超时tryLock(timeout, unit) │ │ 最多等3秒拿不到就放弃 │ │ │ │ ✅ 可轮询tryLock() │ │ 尝试拿锁拿到就执行拿不到就跳过│ │ │ │ ✅ 可公平new ReentrantLock(true) │ │ 按等待顺序分配锁避免线程饥饿 │ │ │ │ ✅ 多条件newCondition() │ │ 一个锁可绑定多个等待/通知条件 │ └─────────────────────────────────────┘一句话选型简单同步 →synchronized语法简洁JVM 自动优化复杂控制 →ReentrantLock灵活强大需手动 unlock二、ReentrantLock 实战卖票程序升级版 1. 基础用法lock() / unlock()⭐ 必须 finally 中解锁classTicketLockimplementsRunnable{privateinttickets10;// ✅ 推荐private final 专用锁对象privatefinalReentrantLocklocknewReentrantLock();Overridepublicvoidrun(){while(true){// 手动加锁⚠️ 必须在 finally 中解锁lock.lock();try{if(tickets0)break;// 模拟业务处理锁内耗时尽量短System.out.println(Thread.currentThread().getName() 卖票tickets);tickets--;Thread.sleep(10);// ⚠️ 锁内 sleep 会降低并发生产环境建议放锁外}catch(InterruptedExceptione){// ✅ 中断处理恢复标志 优雅退出Thread.currentThread().interrupt();break;}finally{// ✅ 铁律unlock() 必须放在 finally 中// 否则异常时锁无法释放 → 其他线程永久阻塞 → 死锁lock.unlock();}}}}2. 进阶用法tryLock() 超时放弃高并发防雪崩/** * 带超时的卖票避免线程无限等待 */publicvoidrunWithTimeout(){try{// 尝试获取锁最多等 3 秒if(lock.tryLock(3,TimeUnit.SECONDS)){try{// ✅ 拿到锁执行临界区if(tickets0){System.out.println(Thread.currentThread().getName() 卖票tickets--);}}finally{lock.unlock();// ✅ 仍需在 finally 中解锁}}else{// ⚠️ 没拿到锁执行降级策略System.out.println(Thread.currentThread().getName() 获取锁超时执行降级逻辑);// 可记录日志 / 返回默认值 / 异步重试}}catch(InterruptedExceptione){Thread.currentThread().interrupt();// 处理中断}}3. ReentrantLock vs synchronized 对比表特性synchronizedReentrantLock适用场景实现机制JVM 内置 MonitorJava 代码实现AQS-锁释放自动代码块结束/异常手动必须 finally 中 unlocksynchronized 更安全可中断❌ 等待时不可中断✅lockInterruptibly()需响应中断时用 Lock可超时❌ 无限等待✅tryLock(timeout)高并发防雪崩必备公平性❌ 非公平默认✅ 可构造时指定new ReentrantLock(true)避免线程饥饿多条件❌ 一个锁只能一个等待集✅newCondition()支持多条件生产者-消费者多队列性能JDK6 已优化差距很小略高但需手动管理优先 synchronized复杂场景用 LockSpringBoot 实践// 分布式锁伪代码基于 Redis ReentrantLock 思想publicbooleantryDistributedLock(Stringkey,longtimeoutMs){// 1. 尝试 setnx 获取锁原子操作BooleanacquiredredisTemplate.opsForValue().setIfAbsent(key,locked,timeoutMs,TimeUnit.MILLISECONDS);// 2. 获取成功 → 执行业务if(Boolean.TRUE.equals(acquired)){try{returndoBusiness();}finally{// 3. 用 Lua 脚本保证判断删除原子性防误删redisTemplate.execute(releaseScript,Collections.singletonList(key));}}// 4. 获取失败 → 降级/重试returnfallback();}三、volatile 关键字轻量级线程安全 1. 核心作用可见性 禁止重排序// ❌ 问题普通变量在多线程下可能不可见privatebooleanflagfalse;// 线程A修改后线程B可能看不到// ✅ 解决用 volatile 保证写立即刷新到主内存读立即从主内存重载privatevolatilebooleanflagfalse;2. volatile 的两大保障✅ 但 ≠ 原子性保障含义代码示例可见性一个线程修改 volatile 变量其他线程立即可见volatile boolean shutdown false;禁止重排序编译器和处理器不会对 volatile 读写进行重排序优化单例模式双重检查锁定的关键3. ⚠️ 重要陷阱volatile 不保证原子性// ❌ 错误以为 volatile 能解决 i 问题privatevolatileintcount0;publicvoidincrement(){count;// 仍线程不安全i 读改写非原子操作}// ✅ 正确方案三选一// 方案1synchronizedpublicsynchronizedvoidincrement(){count;}// 方案2ReentrantLocklock.lock();try{count;}finally{lock.unlock();}// 方案3AtomicInteger⭐ 推荐无锁高性能privateAtomicIntegercountnewAtomicInteger(0);publicvoidincrement(){count.incrementAndGet();}4. 经典应用双重检查锁定DCL单例publicclassSingleton{// ✅ volatile 双重检查保证单例 线程安全 高性能privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){// 第一次检查避免每次调用都同步提升性能if(instancenull){synchronized(Singleton.class){// 第二次检查防止多线程同时进入同步块if(instancenull){// ⚠️ 关键instance new Singleton() 不是原子操作// 可能重排序为1.分配内存 2.返回引用 3.初始化对象// 如果没有 volatile其他线程可能拿到未初始化的对象instancenewSingleton();// ✅ volatile 禁止重排序}}}returninstance;}}volatile 适用场景✅ 状态标志位volatile boolean running true;线程间通信✅ 单例模式DCL 双重检查锁定✅ 一次性发布volatile Config config;发布后不可变❌ 复合操作count、if (x) x等仍需同步四、Atomic 系列CAS 实现无锁线程安全 ⚡1. CAS 思想Compare And Swap比较并交换┌─────────────────────────────────────┐ │ CAS 原理硬件级原子指令 │ │ │ │ 操作步骤 │ │ 1. 读取内存值 V假设5 │ │ 2. 计算新值 N V 1 6 │ │ 3. 比较内存值是否仍为 5 │ │ ├─ ✅ 是 → 写入 6成功 │ │ └─ ❌ 否 → 重试自旋 │ │ │ │ ✅ 优势无锁、高性能、避免线程阻塞 │ │ ⚠️ 风险高并发时自旋消耗 CPUABA 问题│ └─────────────────────────────────────┘2. AtomicInteger 实战线程安全计数器// ✅ 推荐用 AtomicInteger 替代 volatile synchronizedprivateAtomicIntegercountnewAtomicInteger(0);// 原子自增底层 CAS 实现publicvoidincrement(){count.incrementAndGet();// 等价于 count线程安全}// 原子获取当前值publicintgetCount(){returncount.get();}// 原子累加如统计总耗时publicvoidaddCost(longcost){totalCost.addAndGet(cost);// totalCost 也是 AtomicInteger}3. Atomic 家族速查表类适用类型核心方法典型场景AtomicIntegerintincrementAndGet(),compareAndSet()计数器、序列号生成AtomicLonglong同上高精度计数、时间戳AtomicBooleanbooleangetAndSet(),compareAndSet()状态标志位AtomicReferenceT对象引用get(),set(),compareAndSet()无锁更新对象引用AtomicIntegerArrayint 数组getAndIncrement(i)线程安全数组操作4. ⚠️ CAS 的 ABA 问题 解决方案 ABA 问题演示 线程1: 读取 VA准备更新为 C 线程2: 读取 VA → 更新为 B → 又更新回 A 线程1: 比较时发现仍是 A → 更新成功但中间值已变 ✅ 解决方案AtomicStampedReference带版本号的引用 AtomicStampedReferenceString ref new AtomicStampedReference(A, 0); // 更新时需同时检查引用 版本号 ref.compareAndSet(A, C, oldStamp, newStamp); // 版本号不同则失败生产建议大部分场景用AtomicInteger/Long即可无需担心 ABA仅当引用对象可能被回收复用时如对象池才用AtomicStampedReference五、 今日实战任务构建高并发计数器服务任务1用 ReentrantLock 重写卖票程序/** * 要求 * 1. 用 ReentrantLock 替代 synchronized 实现卖票 * 2. 添加 tryLock(1, TimeUnit.SECONDS) 超时机制 * 3. 对比正常情况 / 高并发超时时的行为差异 * * 调试技巧 * - 用 lock.isLocked() / lock.getQueueLength() 监控锁状态 * - 模拟锁竞争在临界区增加 sleep(50) */任务2用 volatile 实现优雅停机标志位/** * 业务场景后台任务线程需支持外部触发停止 * * 要求 * 1. 定义 volatile boolean running true; * 2. 任务线程循环检查 running为 false 时退出 * 3. 主线程调用 stop() 设置 running false * 4. 验证主线程修改后任务线程能否立即感知 * * 挑战 * - 如果任务线程正在 sleep/block如何快速中断 * - 提示结合 interrupt() volatile 双重保障 */publicclassGracefulTaskimplementsRunnable{privatevolatilebooleanrunningtrue;Overridepublicvoidrun(){while(running){// ✅ volatile 保证可见性// 业务逻辑...Thread.sleep(100);// ⚠️ 需配合 interrupt() 处理}cleanup();// 资源清理}publicvoidstop(){runningfalse;// ✅ 主线程修改任务线程立即可见}}任务3用 AtomicInteger 实现分布式序列号生成器本地版/** * 模拟雪花算法的本地序列部分 * * 要求 * 1. 支持并发生成唯一递增序列号如20240402-00001 * 2. 每日零点自动重置序列结合 LocalDateTime * 3. 线程安全 高性能避免锁竞争 * * 关键设计 * - 用 AtomicInteger 存储当日序列 * - 用 volatile 双重检查 处理日期变更 * - 序列号格式yyyyMMdd-000015 位数字不足补零 */publicclassLocalIdGenerator{privatevolatileLocalDatecurrentDate;privateAtomicIntegerdailySeqnewAtomicInteger(0);publicStringnextId(){LocalDatetodayLocalDate.now();// 日期变更时重置序列双重检查 CASif(!today.equals(currentDate)){synchronized(this){if(!today.equals(currentDate)){currentDatetoday;dailySeq.set(0);// 重置序列}}}// ✅ 原子递增 格式化intseqdailySeq.incrementAndGet();returnString.format(%s-%05d,currentDate.format(DateTimeFormatter.BASIC_ISO_DATE),seq);}}任务4对比实验synchronized vs Lock vs Atomic 性能/** * 压测 100 线程 × 10000 次累加对比三种方案耗时 * * 要求 * 1. 方案1synchronized int * 2. 方案2ReentrantLock int * 3. 方案3AtomicInteger * 4. 统计总耗时 / 吞吐量ops/s / CPU 使用率 * * 预期结论 * - 低并发三者差距不大 * - 高并发Atomic Lock ≈ synchronized但 Lock 更灵活 * - 关键根据业务场景选型而非盲目追求最快 */ 第20天 · 核心总结极简背诵版Lock 选型决策树需要可中断/超时/公平/多条件 ├─ ✅ 是 → ReentrantLock记得 finally 中 unlock └─ ❌ 否 → synchronized语法简洁自动管理volatile 两大保障 一大陷阱✅ 保障1可见性 → 写立即刷主内存读立即重载 ✅ 保障2禁止重排序 →DCL单例的关键 ❌ 陷阱不保证原子性 → i仍需用同步/AtomicAtomic 系列核心思想 CAS Compare And Swap比较并交换 ✅ 优势无锁、高性能、避免线程阻塞 ⚠️ 注意高并发自旋消耗 CPU简单场景优先用生产环境守则✅ 优先synchronized代码简洁 JVM 优化✅ 复杂控制用ReentrantLock但必须try-finally解锁✅ 状态标志位用volatile复合操作用Atomic/synchronized❌ 禁止在锁/volatile 变量上执行耗时操作网络/IO/复杂计算高频面试题速记问题核心答案Lock vs synchronized可中断/超时/公平/多条件 vs 自动管理/简洁volatile 原理内存屏障 禁止重排序保证可见性CAS 的 ABA 问题用版本号AtomicStampedReference解决AtomicInteger 底层Unsafe 类 CAS 指令 自旋重试

更多文章