JDK11下Java并发测试synchronized计数远低于预期的原因
count1计数远低于预期的根本原因
核心问题:synchronized锁的对象会动态变化,同步逻辑完全失效
你代码中用于加锁的count1是Integer类型的包装对象,根本不适合作为锁的持有对象,核心逻辑漏洞如下:
count1++的操作本质不是原子操作,底层流程是:读取当前count1引用指向的int值 → 数值+1 → 将新的int值通过自动装箱生成一个全新的Integer对象 → 把count1的引用指向这个新生成的对象。- 哪怕在计数小于127的时候,因为Integer缓存池的存在,可能碰巧拿到同一个锁对象,只要计数超过缓存范围(-128~127),每次自增装箱都会生成全新的独立Integer对象。
- 因为count1被
volatile修饰,它的引用是可以被任意线程修改的,这就导致t1、t2两个执行method1的线程,每次进入synchronized (count1)同步块时,拿到的锁对象根本不是同一个。相当于两个线程完全没有互斥关系,各跑各的同步逻辑,synchronized的保护作用完全没生效。
丢失的计数去哪了
那些“没被统计”的count++调用并没有消失,全部都因为没有互斥保护,在「读-改-写」的非原子流程中被覆盖了:
典型的线程穿插场景:t1线程从内存读到当前count值是100,正准备做+1操作写回结果时,被操作系统调度挂起;此时t2线程同样读到当前count值是100,顺利完成+1操作把值写回为101;等t1线程被唤醒后,基于之前读到的旧值100做+1,把结果101再次写回内存。两次独立的+1操作,最终只让总计数涨了1,大量这类穿插覆盖的操作累计下来,最终count1的数值就会远低于预期的2000000。
为什么count2的结果是准确的
对比来看,count2使用的是固定引用的AtomicInteger对象,全程不会因为值的更新更换锁的作用对象,内部基于CPU的CAS指令完成无锁的原子更新,没有线程穿插导致的覆盖问题,所以最终计数结果是准确的。
修复方向
如果要让synchronized的逻辑正常生效,需要使用一个全程不变的固定对象作为锁,比如单独声明一个private final Object lock = new Object();,把所有同步块的锁对象换成这个固定的lock,不要用动态变化的count1本身。此时再跑测试,就能观察到符合预期的结果:count1能达到满值2000000,且synchronized实现的总耗时会高于AtomicInteger的实现。
内容的提问来源于stack exchange,提问作者Charles Swires
相关产品推荐
相关产品推荐

