JAVA重点基础、进阶知识及易错点总结(17)线程安全 synchronized 同步锁
Java 巩固进阶 · 第17天主题线程安全 synchronized 同步锁 —— 并发编程的第一道防线 进度概览今天攻克多线程最核心难题线程安全。这是面试必考、生产环境必用的知识点直接决定你的代码能否扛住高并发。 核心价值数据安全防止超卖、重复扣款、库存负数等资损事故守护业务底线。面试通关synchronized原理、JMM 三大特性、锁升级是初级→高级开发的分水岭。框架基石理解 SpringBootTransactional、Redis 分布式锁、数据库行锁的底层思想。思维升级从能跑就行到并发正确建立线程安全的编码意识。一、线程安全本质为什么i会出错1. 什么是线程安全┌─────────────────────────────────────┐ │ ✅ 线程安全 │ │ 多线程操作共享数据时无论系统如何 │ │ 调度结果都与预期一致 │ └─────────────────────────────────────┘ ┌─────────────────────────────────────┐ │ ❌ 线程不安全 │ │ 多线程操作共享数据时结果不可预测 │ │ 可能数据错误、丢失更新、脏读 │ └─────────────────────────────────────┘2. 经典陷阱i为什么不是原子操作// ❌ 看似简单实则危险privateintcount0;publicvoidincrement(){count;// 线程不安全}底层拆解count实际是 3 步操作线程A执行 count 1️⃣ 读从内存读取 count 值假设5→ 寄存器 2️⃣ 改寄存器中 1 → 6 3️⃣ 写将 6 写回内存 ⚠️ 问题线程切换可能发生在任意一步 时间线演示 T0: 线程A 读 count5 T1: 线程B 读 count5 A还没写完 T2: 线程A 写 count6 T3: 线程B 写 count6 B也基于5计算覆盖了A的结果 ✅ 预期执行2次count7 ❌ 实际count6丢失1次更新3. 真实业务场景资损高发区场景不安全代码可能后果库存扣减stock--超卖库存-1订单2余额扣款balance - amount透支余额负数资损订单号生成orderId重复两个订单同号数据冲突计数器统计pv少计UV/PV 数据不准影响决策记忆口诀“共享变量 多线程 至少一个写 线程不安全”只要满足这三个条件就必须考虑同步二、解决方案synchronized同步锁 1. 核心原理Monitor监视器锁┌─────────────────────────────────────┐ │ synchronized 底层机制 │ │ 每个 Java 对象都有一个 Monitor │ │ 线程执行 synchronized 代码前 │ │ 1. 尝试获取对象的 Monitor 锁 │ │ 2. 成功 → 进入临界区执行 │ │ 3. 失败 → 阻塞等待直到锁释放 │ │ 4. 执行完毕/异常 → 自动释放锁 │ └─────────────────────────────────────┘2. 三种用法 锁对象对比⭐ 必背publicclassSyncDemo{// 场景1同步代码块最灵活推荐⭐privatefinalObjectlocknewObject();// ✅ 专用锁对象避免外部干扰publicvoidmethod1(){// 只锁关键代码粒度最小性能最优synchronized(lock){// 临界区操作共享数据sharedData;}// 非临界区可并发执行提升吞吐doOtherWork();}// 场景2同步实例方法锁 thispublicsynchronizedvoidmethod2(){// 等价于synchronized(this) { ... }// ⚠️ 注意锁的是当前实例外部可通过 this 获取锁sharedData;}// 场景3同步静态方法锁 Class 对象publicstaticsynchronizedvoidmethod3(){// 等价于synchronized(SyncDemo.class) { ... }// ⚠️ 注意锁的是类对象所有实例共享同一把锁staticCounter;}} 锁对象选择指南锁对象作用范围适用场景风险提示this当前实例单实例内的共享数据外部代码可能也用 this 加锁导致意外阻塞Class整个类静态变量/单例模式锁粒度大并发度低专用对象private final Object lock自定义范围生产环境首选✅ 避免外部干扰锁粒度可控字符串常量lock❌ 禁止使用-字符串常量池可能被其他类复用导致死锁⚠️致命陷阱锁对象不一致// ❌ 错误两个线程用不同锁对象无法互斥synchronized(lock1){...}// 线程Asynchronized(lock2){...}// 线程B → 同时执行线程不安全// ✅ 正确必须用同一把锁privatestaticfinalObjectLOCKnewObject();synchronized(LOCK){...}// 所有线程都用 LOCK三、实战案例卖票系统从不安全到安全❌ 版本1线程不安全演示问题classUnsafeTicketimplementsRunnable{privateinttickets10;// 共享库存Overridepublicvoidrun(){while(true){if(tickets0)break;// ⚠️ 临界区读-判断-写非原子操作System.out.println(Thread.currentThread().getName() 卖票tickets);tickets--;// ❌ 多线程下可能超卖try{Thread.sleep(10);}catch(InterruptedExceptione){}}}}// 测试3 个窗口卖 10 张票UnsafeTickettasknewUnsafeTicket();newThread(task,窗口-1).start();newThread(task,窗口-2).start();newThread(task,窗口-3).start();// 可能输出// 窗口-1 卖票3// 窗口-2 卖票3 ← 重复卖// 窗口-3 卖票2// ... 最终票数 0超卖✅ 版本2synchronized 修复标准写法classSafeTicketimplementsRunnable{privateinttickets10;privatefinalObjectlocknewObject();// ✅ 专用锁Overridepublicvoidrun(){while(true){// 加锁同一时间只有一个线程能进入临界区synchronized(lock){if(tickets0)break;// 二次检查防唤醒后超卖System.out.println(Thread.currentThread().getName() 卖票tickets);tickets--;// ✅ 原子执行不会超卖}// 自动释放锁其他线程可竞争// 非临界区休眠不放锁提升并发try{Thread.sleep(10);}catch(InterruptedExceptione){Thread.currentThread().interrupt();// ✅ 恢复中断break;}}}} 关键细节解析为什么锁内要二次检查if (tickets 0)线程A: 获得锁检查 tickets1准备卖 线程B: 阻塞等待 线程A: 卖完 tickets0释放锁 线程B: 获得锁如果不二次检查会卖 tickets0超卖 ✅ 二次检查确保获得锁后数据仍有效为什么sleep()放在锁外锁内 sleep持有锁休眠 → 其他线程全部阻塞 → 并发度1 ❌ 锁外 sleep释放锁后休眠 → 其他线程可竞争 → 并发度1 ✅ 原则锁粒度越小并发性能越高四、锁的三大黄金法则生产环境守则⚖️法则1必须是同一把锁才能互斥// ❌ 错误锁对象不同形同虚设publicvoidwrong(){synchronized(newObject()){// 每次 new 新对象// 线程A 和 线程B 的锁不同可同时进入sharedData;}}// ✅ 正确锁对象单例全局唯一privatestaticfinalObjectLOCKnewObject();publicvoidright(){synchronized(LOCK){// 所有线程竞争同一把锁sharedData;}}法则2锁粒度越小并发性能越高// ❌ 粗粒度锁住整个方法非关键代码也串行publicsynchronizedvoidprocessOrder(Orderorder){validate(order);// 纯计算无需同步saveToDB(order);// ✅ 关键写数据库需同步sendEmail(order);// 网络调用无需同步且应异步}// ✅ 细粒度只锁共享资源其他代码并发执行publicvoidprocessOrder(Orderorder){validate(order);// 并发执行synchronized(dbLock){// 只锁数据库操作saveToDB(order);}sendEmail(order);// 并发执行甚至可异步}法则3避免死锁进阶预警// ⚠️ 死锁示例线程循环等待对方锁// 线程A: 持有 lock1等待 lock2// 线程B: 持有 lock2等待 lock1// 结果互相等待永久阻塞 ❌// ✅ 预防统一锁获取顺序// 所有线程都先获取 lock1再获取 lock2synchronized(lock1){synchronized(lock2){// 业务逻辑}}死锁排查命令线上应急jps-l# 找到 Java 进程 IDjstackpid|grepdeadlock-A20# 打印死锁线程栈五、深层原理JMM 三大特性 synchronized 如何保证 1. 线程不安全根源JMM 内存模型┌─────────────────────────────────────┐ │ 每个线程有自己的工作内存 (Working Memory)│ │ ️ 所有线程共享主内存 (Main Memory) │ │ │ │ 线程操作变量流程 │ │ 1. 从主内存复制变量到工作内存 │ │ 2. 在工作内存中计算 │ │ 3. 将结果刷新回主内存 │ │ │ │ ⚠️ 问题线程间工作内存不可见 │ │ 线程A 修改了变量线程B 可能看不到 │ └─────────────────────────────────────┘2. 三大特性详解特性含义不安全示例synchronized 如何保证原子性操作不可分割要么全做要么不做i读-改-写三步✅ 锁确保临界区代码互斥执行整体原子可见性一个线程修改其他线程立即可见线程A 改flagtrue线程B 仍看到false✅ 解锁前强制刷新工作内存→主内存加锁前强制从主内存重新加载有序性程序执行顺序与代码顺序一致指令重排序a1; b2;→b2; a1;✅ 禁止锁内代码与锁外代码重排序happens-before 原则 happens-before 原则简化版1️⃣ 程序顺序规则单线程内代码顺序即执行顺序 2️⃣ 锁规则unlock 操作 happens-before 后续对同一锁的 lock 操作 → 确保线程A 解锁前的写对线程B 加锁后的读可见 3️⃣ volatile 规则写 volatile 变量 happens-before 后续读该变量 4️⃣ 传递性A happens-before B, B happens-before C → A happens-before C一句话理解synchronized通过加锁时刷新内存 临界区互斥执行 解锁时写回内存一举解决原子性、可见性、有序性三大问题六、 今日实战任务银行账户系统任务1复现余额扣款线程不安全/** * 要求 * 1. 创建 BankAccount 类余额 1000 元 * 2. 实现 withdraw(amount) 方法余额充足则扣款返回成功/失败 * 3. 3 个线程同时尝试取款 300 元理论应 2 成功 1 失败 * 4. 不加锁运行 10 次观察是否出现余额负数或重复扣款 * * 提示 * - 在 withdraw 中加入 Thread.sleep(10) 模拟网络延迟放大竞争问题 * - 打印每次操作的线程名、操作前余额、操作后余额 */任务2用 synchronized 修复账户安全/** * 要求 * 1. 为 withdraw 方法添加 synchronized 同步 * 2. 对比修复前后的执行结果 * 3. 思考锁粒度是否合理能否优化 * * 挑战 * - 如果增加转账功能A→B如何设计锁避免死锁 * - 提示按账户 ID 排序后加锁统一获取顺序 */任务3SpringBoot 服务层同步实践ServicepublicclassOrderService{AutowiredprivateInventoryServiceinventoryService;/** * 创建订单扣库存 生成订单需保证原子性 * * 要求 * 1. 用 synchronized 保证查库存-扣库存-创建订单的原子性 * 2. 锁对象选择this / 专用锁 / 商品 ID 锁分析利弊 * 3. 思考高并发下单锁会成为瓶颈吗如何优化明天学 ReentrantLock */publicsynchronizedOrderResultcreateOrder(CreateOrderRequestreq){// TODO: 实现业务逻辑// 1. 校验库存// 2. 扣减库存// 3. 生成订单// 4. 返回结果}}任务4性能对比实验理解锁的代价/** * 对比无锁 / synchronized / 细粒度锁 的吞吐量 * * 要求 * 1. 创建 Counter 类实现三种版本的 increment() * 2. 用 10 线程并发执行 100 万次累加 * 3. 统计每种方案的耗时 最终结果正确性 * * 预期结论 * - 无锁最快但结果错误 ❌ * - 粗粒度 synchronized安全但较慢 ⚠️ * - 细粒度锁如 LongAdder 思想安全 较快 ✅ */ 第17天 · 核心总结极简背诵版线程安全判定共享变量 多线程 至少一个写操作 线程不安全 解决方案同步机制synchronized / Lock / volatilesynchronized 三种用法// ✅ 推荐同步代码块 专用锁对象privatefinalObjectlocknewObject();synchronized(lock){/* 临界区 */}// ⚠️ 慎用同步实例方法锁 this易被外部干扰publicsynchronizedvoidmethod(){...}// ⚠️ 慎用同步静态方法锁 Class粒度大publicstaticsynchronizedvoidstaticMethod(){...}锁的三大黄金法则同一把锁所有竞争线程必须用同一个锁对象最小粒度只锁共享数据操作非关键代码放锁外避免死锁多锁时统一获取顺序设置超时进阶JMM 三大特性 synchronized 保障特性问题synchronized 解决方案原子性i非原子临界区互斥执行整体原子可见性工作内存不可见解锁前刷主内存加锁前重载有序性指令重排序禁止锁内外代码重排序生产环境守则✅ 锁对象用private final Object避免外部干扰✅ 临界区代码越少越好休眠/网络调用放锁外✅ 日志记录加锁/解锁时间便于性能分析❌ 禁止在锁内调用外部未知方法可能死锁
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2473842.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!