凌晨三点半的Java并发编程笔记
咖啡杯已经见底三次了,显示器上还开着三个IDEA窗口。每次我以为自己搞懂了Java并发,现实总会甩过来一记响亮的耳光——上周生产环境那个死锁问题,现在想起来太阳穴还在突突跳。
为什么并发编程这么难搞?
刚毕业那会儿觉得"不就是加个synchronized嘛",后来被线上监控报警教做人。现在回头看,Java并发难就难在这些坑都藏在细节里:
- 测试环境跑100次都正常,上线就死锁
- 明明用了线程池,CPU还是飚到100%
- 那个volatile变量,在同事电脑上表现和你的完全不一样
内存可见性:最隐形的刺客
记得有次排查个诡异bug,代码逻辑简单到令人发指:
// 线程A | flag = true; |
// 线程B | while(!flag) { /* 死循环 */ } |
你猜怎么着?线程B永远看不到flag变成true。这就是著名的内存可见性问题,JVM在背后偷偷优化指令重排时,根本不会通知你。
线程安全的三重境界
青铜选手:synchronized走天下
刚开始都这样,见到共享变量就加锁。直到某天发现系统吞吐量还不如单线程——锁的粒度太粗了,整个方法都被卡住。
白银玩家:CAS操作真香
发现AtomicInteger比synchronized快十倍时,像捡到宝似的到处用。直到遇见ABA问题,才明白compareAndSet也不是银弹。
王者段位:读源码如看小说
能对着ConcurrentHashMap的transfer方法傻笑半小时,发现Doug Lea在resize时处理哈希冲突的巧妙设计,比追剧还过瘾。
那些年踩过的并发坑
- 线程池参数乱配:把maxPoolSize设成Integer.MAX_VALUE,OOM教你做人
- 误用ThreadLocal:用完不remove,内存泄漏查到你怀疑人生
- 锁顺序死锁:方法A先锁X再锁Y,方法B反着来,线上直接挂掉
最绝的是那次用ForkJoinPool处理Excel导出,任务分解得太细反而比单线程还慢——上下文切换开销把优势全吃掉了。
实战中的救命技巧
凌晨四点整理的救命清单,有些是官方文档不会告诉你的:
场景 | 解决方案 |
需要保证可见性但不要原子性 | volatile + 单线程写 |
读多写少的计数器 | LongAdder代替AtomicLong |
防止缓存击穿 | ConcurrentHashMap.computeIfAbsent |
对了,用CompletableFuture时一定要记得处理异常,否则错误会被默默吞掉。别问我怎么知道的,都是血泪史。
关于锁的冷知识
synchronized在JDK6之后早就不是重量级锁了,偏向锁、自旋锁、锁消除...JVM团队为了性能真是操碎了心。《Java并发编程实战》里说锁竞争会影响性能,但没说现代JVM能自动把锁降级。
窗外天都快亮了,最后分享个真实案例:有次用parallelStream处理集合,明明加了线程安全控制,结果还是数据错乱。后来发现是Spliterator的特性导致的——ForkJoinPool会偷懒重用线程,导致ThreadLocal变量串味。
咖啡机又响了,得去续杯。下次再聊CountDownLatch和CyclicBarrier那些相爱相杀的故事...