大家好今天我们聊一个并发编程中最经典、最容易踩坑的问题——两个线程每个都执行10次countcount初始值为0并发执行后最终结果往往达不到20。很多新手第一次遇到这个问题时都会一脸困惑1010明明等于20为什么计算机算出来会是18、19甚至更低其实这个问题的根源恰恰是我们上一篇博客中提到的并发编程核心痛点——原子性、可见性问题只是它藏在看似简单的count操作背后容易被忽略。今天我们就彻底扒开这个问题的底层逻辑从CPU指令、线程调度、并发安全三个层面一步步讲清楚“为什么加不到20”帮你吃透并发编程的核心底层原理避开这个基础却致命的坑。一、先看现象复现“加不到20”的场景为了让大家更直观感受我们先写一段简单的代码以Java为例复现这个问题public class CountTest { // 共享变量count初始值0 private static int count 0; // 线程执行的任务count执行10次 public static void increment() { for (int i 0; i 10; i) { count; } } public static void main(String[] args) throws InterruptedException { // 创建两个线程 Thread t1 new Thread(CountTest::increment); Thread t2 new Thread(CountTest::increment); // 启动两个线程并发执行 t1.start(); t2.start(); // 等待两个线程执行完毕 t1.join(); t2.join(); // 打印最终结果 System.out.println(最终count值 count); } }按照直觉t1执行10次countt2执行10次最终count应该是20。但实际运行后你会发现结果大概率是19、18偶尔会是20运气好的情况但绝不会稳定达到20。这不是代码写错了也不是计算机出了bug而是count这个看似“原子”的操作在底层其实是非原子操作——这就是问题的核心根源。二、核心原理count到底做了什么非原子性拆解我们肉眼看到的count是一行简单的代码但在CPU层面它会被拆分成3个独立的指令这3个指令不是原子的无法被中断要么全执行、要么全不执行而是可以被线程调度打断的。count的底层CPU指令拆解关键读取LoadCPU从内存中读取当前count的值加载到CPU的寄存器中比如count初始值0此时寄存器中存储0自增AddCPU对寄存器中的值进行自增操作比如从0变成1写入StoreCPU将寄存器中自增后的值写回内存中覆盖原来的count值比如将1写回内存count变成1。这三个步骤单独看都是简单的操作但在多线程并发场景下线程的调度是由操作系统控制的——操作系统会随机切换线程抢占CPU执行权也就是说一个线程执行到一半比如刚完成“读取”或“自增”就可能被切换让另一个线程执行这就会导致“计数丢失”。三、关键场景为什么会出现计数丢失图文拆解我们以“t1和t2同时执行count导致计数丢失”为例拆解最典型的一种场景实际还有多种类似场景本质都是指令被打断帮你直观理解前提count初始值为0t1和t2同时启动操作系统随机调度。t1获得CPU执行权执行“读取”指令从内存中读取count0加载到t1的寄存器中此时操作系统切换线程t1被暂停t2获得CPU执行权t2执行“读取”指令同样从内存中读取count0因为t1还没执行“写入”操作内存中的count还是0加载到t2的寄存器中t2继续执行“自增”指令寄存器中的0变成1t2继续执行“写入”指令将1写回内存此时内存中count1操作系统再次切换线程t1恢复执行继续执行“自增”指令t1寄存器中的0变成1t1继续执行“写入”指令将1写回内存覆盖了t2写入的1此时t1和t2各自执行了一次count但最终count只从0变成了1而不是2——这就是一次计数丢失这个场景中问题的核心的是t1和t2都读取了同一个“旧值”count0各自自增后又将同一个“新值”1写回内存相当于两次count只生效了一次。而在我们的测试代码中t1和t2各执行10次count这种“指令被打断、计数丢失”的场景可能会发生1次、2次甚至更多次所以最终结果往往会小于20。补充为什么偶尔会得到20—— 运气好的情况下操作系统没有在count的3个指令之间切换线程t1完整执行10次count从0到10然后t2再完整执行10次从10到20此时结果就是20但这种情况是随机的无法保证。四、延伸思考不止count这些操作也有同样问题很多新手会误以为只有count会出现这个问题但实际上所有“读取-修改-写入”的组合操作只要是多线程并发执行都可能出现类似的计数丢失问题比如count--底层同样是“读取-自减-写入”三个指令count 1和count原理完全一致非原子操作自定义的复合操作比如“if(count 10) count”也是多步操作可能被打断。本质原因这些操作都不是“原子操作”无法抵抗线程切换带来的并发冲突——这也是我们上一篇博客中提到的“并发安全性问题”的核心体现原子性缺失。
解惑:双线程各执行10次count++,为何最终加不到20?
大家好今天我们聊一个并发编程中最经典、最容易踩坑的问题——两个线程每个都执行10次countcount初始值为0并发执行后最终结果往往达不到20。很多新手第一次遇到这个问题时都会一脸困惑1010明明等于20为什么计算机算出来会是18、19甚至更低其实这个问题的根源恰恰是我们上一篇博客中提到的并发编程核心痛点——原子性、可见性问题只是它藏在看似简单的count操作背后容易被忽略。今天我们就彻底扒开这个问题的底层逻辑从CPU指令、线程调度、并发安全三个层面一步步讲清楚“为什么加不到20”帮你吃透并发编程的核心底层原理避开这个基础却致命的坑。一、先看现象复现“加不到20”的场景为了让大家更直观感受我们先写一段简单的代码以Java为例复现这个问题public class CountTest { // 共享变量count初始值0 private static int count 0; // 线程执行的任务count执行10次 public static void increment() { for (int i 0; i 10; i) { count; } } public static void main(String[] args) throws InterruptedException { // 创建两个线程 Thread t1 new Thread(CountTest::increment); Thread t2 new Thread(CountTest::increment); // 启动两个线程并发执行 t1.start(); t2.start(); // 等待两个线程执行完毕 t1.join(); t2.join(); // 打印最终结果 System.out.println(最终count值 count); } }按照直觉t1执行10次countt2执行10次最终count应该是20。但实际运行后你会发现结果大概率是19、18偶尔会是20运气好的情况但绝不会稳定达到20。这不是代码写错了也不是计算机出了bug而是count这个看似“原子”的操作在底层其实是非原子操作——这就是问题的核心根源。二、核心原理count到底做了什么非原子性拆解我们肉眼看到的count是一行简单的代码但在CPU层面它会被拆分成3个独立的指令这3个指令不是原子的无法被中断要么全执行、要么全不执行而是可以被线程调度打断的。count的底层CPU指令拆解关键读取LoadCPU从内存中读取当前count的值加载到CPU的寄存器中比如count初始值0此时寄存器中存储0自增AddCPU对寄存器中的值进行自增操作比如从0变成1写入StoreCPU将寄存器中自增后的值写回内存中覆盖原来的count值比如将1写回内存count变成1。这三个步骤单独看都是简单的操作但在多线程并发场景下线程的调度是由操作系统控制的——操作系统会随机切换线程抢占CPU执行权也就是说一个线程执行到一半比如刚完成“读取”或“自增”就可能被切换让另一个线程执行这就会导致“计数丢失”。三、关键场景为什么会出现计数丢失图文拆解我们以“t1和t2同时执行count导致计数丢失”为例拆解最典型的一种场景实际还有多种类似场景本质都是指令被打断帮你直观理解前提count初始值为0t1和t2同时启动操作系统随机调度。t1获得CPU执行权执行“读取”指令从内存中读取count0加载到t1的寄存器中此时操作系统切换线程t1被暂停t2获得CPU执行权t2执行“读取”指令同样从内存中读取count0因为t1还没执行“写入”操作内存中的count还是0加载到t2的寄存器中t2继续执行“自增”指令寄存器中的0变成1t2继续执行“写入”指令将1写回内存此时内存中count1操作系统再次切换线程t1恢复执行继续执行“自增”指令t1寄存器中的0变成1t1继续执行“写入”指令将1写回内存覆盖了t2写入的1此时t1和t2各自执行了一次count但最终count只从0变成了1而不是2——这就是一次计数丢失这个场景中问题的核心的是t1和t2都读取了同一个“旧值”count0各自自增后又将同一个“新值”1写回内存相当于两次count只生效了一次。而在我们的测试代码中t1和t2各执行10次count这种“指令被打断、计数丢失”的场景可能会发生1次、2次甚至更多次所以最终结果往往会小于20。补充为什么偶尔会得到20—— 运气好的情况下操作系统没有在count的3个指令之间切换线程t1完整执行10次count从0到10然后t2再完整执行10次从10到20此时结果就是20但这种情况是随机的无法保证。四、延伸思考不止count这些操作也有同样问题很多新手会误以为只有count会出现这个问题但实际上所有“读取-修改-写入”的组合操作只要是多线程并发执行都可能出现类似的计数丢失问题比如count--底层同样是“读取-自减-写入”三个指令count 1和count原理完全一致非原子操作自定义的复合操作比如“if(count 10) count”也是多步操作可能被打断。本质原因这些操作都不是“原子操作”无法抵抗线程切换带来的并发冲突——这也是我们上一篇博客中提到的“并发安全性问题”的核心体现原子性缺失。