ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

12-并发编程 8 大坑,每一个都让线上崩过

12-并发编程 8 大坑,每一个都让线上崩过 这些坑不是八股是真实生产环境里反复炸过的地方死锁、活锁、伪共享、上下文切换、双重检查锁、volatile 误用、Future 异常吞噬、ThreadLocal 泄漏。把这篇当 checklist上线前对照一遍能少背很多锅。坑一死锁Deadlock两个线程各自持有对方想要的锁互相等// 线程1先锁 A 再锁 Bsynchronized(A){synchronized(B){...}}// 线程2先锁 B 再锁 A ← 反方向必死锁synchronized(B){synchronized(A){...}}四个必要条件互斥、持有等待、不可剥夺、循环等待。破任何一个即可。最实用的是统一加锁顺序所有地方都按固定顺序如按对象哈希/id 排序加锁循环等待链就断了。其他手段用tryLock(timeout)超时放弃、用ReentrantLock替代、缩小锁范围。排查jstack pid直接打印Found one Java-level deadlock列出互相持有的锁和线程栈定位极快。坑二活锁Livelock线程没死锁也没阻塞却「一直忙却没进展」。典型两个礼貌线程互相礼让——while(冲突){退让();重试();}// 两边都退让永远重试CPU 空转和死锁区别死锁是卡住不动活锁是忙等无进展。解法退让时加随机退避而非立刻重试打破对称或引入仲裁/重试上限。坑三伪共享False Sharing最隐蔽的性能坑。CPU 缓存以「缓存行」为单位通常 64 字节。两个线程各改不同变量但这两个变量恰巧在同一缓存行硬件会认为「你改了我就失效」反复同步性能暴跌——即使变量毫无关系。// 两个线程分别狂改 a.count 和 b.count若 a,b 同缓存行 → 伪共享classCounter{volatilelongcount;}// 相邻对象可能同缓存行解法缓存行填充JDK 8 用sun.misc.Contended注解 JVM 参数-XX:-RestrictContended或手动在变量前后塞 7 个 long 占位把对象撑满一行。LongAdder内部正是用「分散到多个 Cell」规避伪共享所以高并发计数快。坑四上下文切换开销线程不是免费的。切换要保存/恢复寄存器、刷新 TLB每次几微秒。线程数远超核数时CPU 大量时间花在「切来切去」而非干活吞吐量反而下降——这叫上下文切换颠簸。现象vmstat/top里%sy系统态 CPU很高、cscontext switch每秒几十万。解法线程数按任务类型配见第 8 篇公式IO 密集才放大别newCachedThreadPool无限建线程用线程池复用而非大量短命线程。坑五双重检查锁忘了 volatile经典单例错误第 3 篇详述instance new Singleton()可能重排序别的线程拿到半初始化对象。privatestaticvolatileSingletoninstance;// 必须 volatile 防重排staticSingletonget(){if(instancenull){synchronized(Singleton.class){if(instancenull)instancenewSingleton();}}returninstance;}漏volatile就是埋雷。更省心用「静态内部类」或enum实现单例天然线程安全且懒加载不用手写 DCL。坑六volatile 当锁用第 3 篇强调过volatile只保可见性i这种复合操作它管不了。有人写了volatile int count; count以为线程安全结果计数永远少。涉及「读-改-写」一律synchronized/原子类/Lock。坑七Future/CF 异常被吞submit的异常藏在Future里不get()就永远看不见任务失败CompletableFuture链不exceptionally也静默。后果数据没处理、订单没落库监控却一片绿。Future?fpool.submit(()-risky());// 抛了异常外面不知道// 必须 f.get() 或链尾 .exceptionally(...)否则「静默失败」建议提交Runnable内部自己try/catch打日志CompletableFuture链尾永远加exceptionally/handle监控线程池的「完成任务数 vs 预期」做对账。坑八ThreadLocal 内存泄漏ThreadLocal把变量绑在线程上线程池线程是复用的若不清理值会跨任务串味甚至 OOMprivatestaticThreadLocalUserctxnewThreadLocal();voidhandle(Useru){ctx.set(u);doWork();// 忘了 ctx.remove()线程还池里下个任务读到上个人的 u}正确用try/finally务必ctx.remove()或用完即清。Web 框架如 Spring通常在请求结束时清理但自己写工具类务必手动remove。ThreadLocalMap的 key 是弱引用value 是强引用不remove且线程常驻就会泄漏。一份上线前并发 Checklist所有共享可变状态要么final不可变要么synchronized/Lock/原子类保护加锁顺序全局统一杜绝死锁关键锁有超时tryLock不用volatile保护复合操作单例用enum/静态内部类或 DCL 带volatile线程池手填七参队列有界拒绝策略显式线程命名提交任务后必消费结果get/exceptionally不吞异常ThreadLocal用完remove不在并行流/公共池跑 IO 阻塞CPU 密集与 IO 密集拆池线程数按公式配关键路径加监控池 activeCount、队列长、拒绝数、任务耗时总结并发 8 大坑①死锁统一加锁顺序/tryLock超时破循环等待②活锁随机退避破对称③伪共享缓存行填充/LongAdder分散④上下文切换颠簸线程数按公式别无限建⑤双重检查锁漏volatile致半初始化用 enum/静态内部类更稳⑥volatile当锁管不了i⑦Future/CompletableFuture异常被吞必get/exceptionally⑧ThreadLocal不remove致串味/OOMtry-finally 清理。上线前按 Checklist 逐项核对。
返回列表