Java多线程并发时计数器结果小于预期值的原因是什么
你遇到的是典型的多线程竞态条件问题,既不是JVM调度漏执行了操作,也不是循环逻辑写错,核心原因是你对共享变量counter的累加操作不是原子操作,并发场景下会出现更新丢失。
counter += add在JVM执行时不是单步完成的,会拆成三个独立步骤:
- 从主内存读取
counter的当前值,拷贝到线程的本地工作缓存 - 在本地缓存中对读取到的值做加法运算
- 把运算得到的新值写回主内存的
counter变量
只要两个线程的这三个步骤出现交叉执行,就会出现更新覆盖,导致计数小于预期:
举个会导致数值低于98000的典型场景:
- ThreadOne第一次循环,读取到
counter=0,准备计算+1000,此时CPU切换到ThreadTwo - ThreadTwo第一次循环,也读取到
counter=0,计算+1得到1,写回主内存,然后进入sleep - CPU切回ThreadOne,它不会重新读取主内存的最新值,而是用之前读到的0计算0+1000=1000,写回主内存
这一轮执行下来,两个线程各做了一次累加,理论上应该是1001,但实际主内存里counter只有1000,ThreadTwo加的1直接被覆盖丢失了。如果这种交叉发生在ThreadOne自己的两次累加之间(比如ThreadOne第一次加1000读到0还没写回,第二次累加又读到0,两次加1000最终只生效一次),一次就会丢失1000的计数,多次碰撞后最终结果自然会低于98000。
你偶尔能得到正确结果,只是因为两个线程每次更新后都sleep 50ms,大部分时候两个线程的更新步骤刚好错开,没有出现交叉,但这个时机完全由JVM线程调度决定,没有任何保障,所以结果无法稳定复现。
要解决计数不准的问题,核心是保证共享变量操作的原子性和内存可见性,两种常用改法:
方案1:使用synchronized加锁
给操作共享变量的方法加上synchronized关键字,利用Java内置锁保证同一时间只有一个线程能执行累加、读取操作,从根源上避免步骤交叉:
class Accum { private int counter = 0; private static Accum a = new Accum(); private Accum() {} public static Accum getAccum() { return a; } // 加锁保证读操作的可见性 public synchronized int getCount() { return counter; } // 加锁保证累加操作的原子性 public synchronized void updateCounter(int add) { counter += add; } }
方案2:使用原子类
用JUC包下的AtomicInteger替代普通int类型,它基于CAS机制实现了无锁的原子更新,不需要手动加锁,性能更好:
import java.util.concurrent.atomic.AtomicInteger; class Accum { private AtomicInteger counter = new AtomicInteger(0); private static Accum a = new Accum(); private Accum() {} public static Accum getAccum() { return a; } public int getCount() { return counter.get(); } public void updateCounter(int add) { counter.addAndGet(add); } }
补充说明:以上修改只能保证计数最终累加的结果是正确的(总和981000 + 991 = 98099),但无法保证两个线程的打印顺序、打印时的数值完全符合你的预期——因为线程调度是随机的,可能ThreadTwo先跑完循环打印结果,也可能ThreadOne打印的时候ThreadTwo还剩最后几次累加没执行。如果要严格匹配你写的预期输出顺序和数值,还需要通过
Thread.join()等机制控制线程的执行、打印时机。
内容的提问来源于stack exchange,提问作者Ali Major

