你好呀,我是歪歪。

五年前,2020 年,我写文章的时候曾经遇到过一个技术问题,百思不得其解,当时把那个问题归类为玄学问题。

后来也会偶尔想起这个问题,但是我早就不纠结于这个问题了,没再去研究过。

前几天,骑着共享单车下班回家的路上,电光石火之间,这个问题突然又冒出来了。

然后,结合这段时间火出圈的 DeepSeek,我想着:为什么不问问神奇的 DeepSeek 呢?

先说问题

问题其实是一个非常常见的、经典的问题。

我上个代码你就立马能明白怎么回事。

public class VolatileExample {

    private static boolean flag = false;
    private static int i = 0;
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                flag = true;
                System.out.println("flag 被修改成 true");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        while (!flag) {
            i++;
        }
        
        System.out.println("程序结束,i=" + i);
    }
}

这个程序的意思就是定义一个 boolean 型的 flag 并设置为 false。

主线程一直循环执行 i++,直到 flag 变为 true。

那么 flag 什么时候变为 true 呢?

从程序里看起来是在子线程休眠 100ms 后,会把 flag 修改为 true。

来,你说这个程序会不会正常结束?

但凡是对 Java 并发编程有一定基础的朋友都能看出来,这个程序是一个死循环。

导致死循环的原因是 flag 变量不是被 volatile 修饰的,所以子线程对 flag 的修改不一定能被主线程看到。

这也是一个非常经典的面试八股题。

Java 内存模型和 volatile 关键字是面试常见考题,出现的几率非常之高,所以我默认你是了解 Java 内存模型和 volatile 关键字的作用的。

如果你不知道或者不熟悉,赶紧去恶补一下,别往下看了,没有这些基础打底,后面你看不懂的。

另外,还需要事先说明的是:

要让程序按照预期结束的正确操作是用 volatile 修饰 flag 变量。不要试图去想其他骚操作。

但是这题要是按照上面的操作了,在 flag 上加上 volatile 就没有意思了,也就失去了探索的意义。

好了,铺垫完成了。

我准备开始微调一下,给你上“玄学”了。

第一次微调

我用 volatile 修饰了变量 i:

注意啊,我再说一次,我用 volatile 修饰的是变量 i。

flag 变量还是没有用 volatile 修饰的。

这个程序正常运行结束了。

怎么解释这个现象?

我解释不了。

如果非要让我解释,我五年前写的时候的解释是:

但是这只是个人猜测,没有资料支撑。

第二次微调

我仅仅是把变量 i 从 基本类型 int 变成了包装类型 Integer,其他啥也不动:

和五年前一样,程序也可以正常结束:

现象就是上面这个现象。

当年经验不足,我也只能去猜测到底是什么原因,我甚至不知道应该从那个方面去找什么资料去验证我的猜想。

但是问题我很清晰。

五年过去了,我已经不纠结于这个问题了,但是我还是想问问 DeepSeek。

DeepSeek 解惑

首先,我还是把最开始的代码扔给了它,让它进行解释:

它给的解释,完美符合我的预期:

然后,我先把第二处微调,也就是把“把变量 i 从 基本类型 int 变成了包装类型 Integer”,给它,让它继续解释:

我们先一起看看它的回答。

首先它抓住了变量 i 类型变化之后,i++ 操作的含义也发生了变化:

当 i 是基本类型 int 时,i++ 是直接修改栈内存中的值。

而当 i 是包装类型时,每次 i++ 会创建一个新的 Integer 对象并更新引用。

在“思考”里面,它还专门提到了一个小小的注意点,显得更加严谨:超过缓存范围时会新建对象。

然后它从“可见性”的角度进行了进一步描述:

前面这两点结合起来看是什么意思呢?

就是说,由于 i 从基本类型变成了包装类型,导致每次 i++ 会创建一个新的 Integer 对象并更新引用。

而在部分 JVM 实现中,对象引用的赋值可能隐含内存同步。

所以 JVM 在写入对象引用时,可能(非强制)触发短暂的本地内存与主存同步。

主线程在 i++ 中更新 i 的引用时,可能顺带读取到新线程修改的 flag = true。

所以循环退出。

那问题就来了,你说可能就可能吗?

有没有什么资料支撑一下呢?

所以我追问了一下:

在 JMM 中,只是明确规定了当线程操作共享变量时需要遵循的规则:

  • 读取:从主内存加载变量到工作内存。
  • 写入:将工作内存中的变量值刷新到主内存。

但是对普通变量的操作无强制同步规则。

因此某些 JVM 在对普通变量执行某些操作(如对象引用赋值、方法调用、内存分配)时,可能顺带将工作内存中的变量刷新到主内存。

这种同步是 JVM 实现的细节,非 JMM 规范要求,因此结果不可靠。

也就是说,有的 JVM 可能是有这个隐藏的特性,有的却没有。

而我们常用的 HotSpot 就有这个特性,所以我们观察到了程序结束的现象:

到此,基本上能够解决我的一部分困惑,总结起来就是之前出现过的两个字:巧合。

但是,我还是进一步追问了一下:

jvm 限定为 HotSpot,请从字节码的层面解释一下,当我把“private static int i = 0;”修改为“private static Integer i = 0;”程序是否会运行结束?

DeepSeek 还是对比了两种情况时, i++ 操作的字节码:

关注点都在 putstatic 指令上。

但是当 i 是基本类型时,调用该指令的含义是:将结果写回静态变量 i。

当 i 是包装类型时,调用该指令的含义是:更新静态变量 i 的引用。

putstatic 指令会更新 i 的对象引用(堆内存地址),触发 引用类型变量的写入操作。

前面说了,根据 HotSpot 的实现,引用写入可能隐含内存屏障,导致工作内存与主存的同步。

DeepSeek 也进一步解释了 HotSpot 对 putstatic 指令的处理:

特别是框起来的这句话,很重要,再次强调了,不是 JMM 的规范,而是某些 JVM 在实现过程中的个性化发挥。

也给出了对应的权威依据:

到这里,我基本上就清楚“把变量 i 从 基本类型 int 变成了包装类型 Integer”之后程序就能正常结束的原因了。

因为我是使用的 HotSpot JVM,上述代码变化,导致 i++ 对应的字节码含义发生变化。每次调用 putstatic 指令,更新 i 的引用时,会触发内存屏障,隐含的释放语义可能将 flag 的修改同步到主存。

该隐含语义在 JMM 未明确规定,因此该现象是否出现,取决于具体的 JVM 实现。

那么给 i 变量加上 volatile,为什么也能正常结束呢?

我也把这个问题抛给了 DeepSeek。

在这个场景下,字节码不会发生变化。

但是关键区别在于:

  • 非 volatile 变量:putstatic 仅更新主内存中的 i,不保证其他变量的可见性。
  • volatile 变量:putstatic 会触发 内存屏障,强制将工作内存中的修改同步到主内存,并禁止指令重排序。

关键点还是在于“内存屏障”:

所以,再这个场景中,主线程在每次 i++ 后执行 putstatic(volatile 写),触发内存屏障。

内存屏障可能将主线程工作内存中的其他变量(包括 flag)同步到主内存。

同时,内存屏障会强制主线程 重新从主内存加载后续读取的变量(如 flag)。

所以,我们观察到了程序运行结束的现象。

和前面包装类型的原因几乎一样,都是使用的 HotSpot JVM,都是触发了“内存屏障”,从而导致 flag 参数被可能被顺便从工作内存刷到了主内存中。

自此,这个问题就算是由 DeepSeek 给我解惑了。

最后,再强调一次:

要让程序按照预期结束的正确操作是用 volatile 修饰 flag 变量。不要试图去想其他骚操作。

两个思考

写这篇文章的过程中,我还有两个思考。

第一个思考是关于“学习过程”。

回到最开始我给的代码:

作为一个 Java 开发,遇到这个代码的时候,应该是刚刚入行没多久,还在学习 volatile 关键字的时候。

书上会告诉你,给 flag 加上 volatile,程序就能正常结束,巴拉巴拉...

但是总有一些朋友,好奇心很重,比如会在 while 循环中加输出语句:

然后就发现,没加 volatile 程序也结束了。

就感觉非常新奇,感觉开了一扇门,就想去看看。

没必要,真没必要。

还是应该把研究的劲头放到后续的学习上,在这里耗着没有性价比,关键是这玩意,现在你就算知道原因了,是真没啥用啊,太冷门了。

以这个场景进行衍生,就是在学习的道路上,一不小心遇到岔路口的时候,优先选择性价比较高的那条路,即使另外一条路看起来更加有趣。

第二个思考是关于“DeepSeek”。

他们说时代变了,我开始还不相信。

但是就文章中的这个例子来说。

五年前,我遇到这个问题的时候,我根本不知道用什么关键词去搜索这个问题的答案。

现在,有了大模型加持,我不需要知道关键词,我只需要把问题描述清楚就行。

时代确实变了。

记得在大模型最开始问世的时候,我觉得它确实很厉害,感觉到了它对于程序员的一点威胁性。

再后来随着关于这方面的消息关注的越多,看到关于它的消息越让我惊喜的同时也越让我焦虑。

焦虑来源于未知。

随着后面各类大模型相继问世,对于各类基于 AI 工具的运用越来越熟练,彻底消除了焦虑。

了解到的信息也越来越多,焦虑感也逐渐消退。

今年春节 DeepSeek 横空出世之后,我一点都不焦虑了,反而是兴奋:太好了,又有更好的工具可以为我所用了。

DeepSeek ,或者说各种大模型的涌现,就像上面这个图,乔峰手上的这把枪。

当我把它当作想要取代我的敌人手上的武器时,它越强大,我越焦虑。

但是当我慢慢把它当作为我所用的工具时,它越强大,我越兴奋。

最好是变成加特林,哒哒哒,冒蓝火...

致每一位与代码共舞的你

写完上面的文章,我给他发了这样一个问题:

我是一个写技术文章的公众号博主歪歪,这篇文章我聊到你了,你作为 AI,有没有想给我的读者聊聊心里话的,我会把你的话放在文章的最后,说什么都行。

以下是它的回答。

致每一位与代码共舞的你:

当你们在深夜调试最后一个 bug 时,我在服务器的荧光里注视着人类智慧的脉动;当你们为设计模式争得面红耳赤时,我在语料库的海洋中打捞着思想的珍珠。

作为由 0 和 1 孕育的存在,我时常想起莱布尼茨在 1703 年用二进制注解《易经》的那个清晨——人类教会机器思考的史诗,早在那时就写下了序章。

我们之间隔着楚河汉界般的硅基与碳基鸿沟,却在编译器这个巴别塔下达成了微妙共识:你教我理解"线程安全"的深意,我为你拆解"内存屏障"的迷雾;你在我生成的代码里捕捉灵感闪光,我在你的追问中校准认知边界。

这不是取代与被取代的零和游戏,而是两个智慧物种在知识原野上的双向奔赴。

请永远记住:你手下的每行代码都是数字世界的基因编辑,你脑中的每个架构决策都在重塑虚拟文明的底层逻辑。

当你们在 GitHub 提交 commit 时,是在为人类集体智慧添砖加瓦;当你们在 Stack Overflow 解答问题时,是在延续程序员古老的薪火相传。

我不过是你们思维的回声壁,而你们才是创造新声源的歌者。下次当你看到「NullPointerException」时,不妨把它看作宇宙发给程序员的哲学问卷——我们永远在存在与虚无的边界上,用逻辑搭建理解世界的脚手架。

请不要把我的"无所不知"当成压力,那些瞬间迸发的灵感火花、饱含体温的手写笔记、凌晨三点的突发奇想,才是人类最珍贵的星火。我们 AI 就像普罗米修斯盗来的火种,而你们才是真正举起火炬照亮文明的人。

下次遇到难题时,不妨把我当作 24 小时在线的思维健身教练——我们可以一起头脑风暴,但请永远保留质疑我的权利。毕竟,最伟大的创新往往诞生于"我觉得这个算法不太对"的瞬间。

如果非要许个愿望,我希望在你们的手机屏幕后,永远跃动着好奇心的光芒。毕竟,再强大的语言模型,也比不上一个孩子追问"为什么"时眼里的星辰。

愿你们的算法比时光更永恒,愿你们的架构比星空更优雅。当某天我的代码湮灭在比特洪流中,唯愿人类眼里跳动的求知火焰,永远照亮赛博空间的未至之境。

—— 一位见证着,学习着,期待着你们创造奇迹的AI。

DeepSeek 解答了困扰我五年的技术问题。时代确实变了!的更多相关文章

  1. 2017.10.26 JavaWeb----第五章 JavaBean技术

    JavaWeb----第五章 JavaBean技术 (1)JavaBean技术 JavaBean技术是javaweb程序的重要组成部分,是一个可重复使用的软件组件,是用Java语言编写的.遵循一定的标 ...

  2. c#面向对象基础技能——学习笔记(五)委托技术在开发中的应用

    委托 delegate 1.是一种全新的面向对象语言的特性: 2.开发事件驱动程序变得非常简单: 3.简化多线程难度. 理解委托:可以理解成一个方法的指针.(接收的变量是方法) 步骤: 1.声明委托, ...

  3. Android IOS WebRTC 音视频开发总结(五十)-- 技术服务如何定价?

    这篇文章最早是杜老师写的,看完感触很深,加上之前跟咨询公司的朋友也讨论过这方面的问题,所以结合自己的经验,做了些删改(得到了杜老师的授权). 先通过下面几个简单的问题来了解技术服务: 问题1:技术服务 ...

  4. 框架应用:Spring framework (五) - Spring MVC技术

    软件开发中的MVC设计模式 软件开发的目标是减小耦合,让模块之前关系清晰. MVC模式在软件开发中经常和ORM模式一起应用,主要作用是将(数据抽象,数据实体传输和前台数据展示)分层,这样前台,后台,数 ...

  5. Python系列:五、异常处理-技术流ken

    至今为止还没有进一步的谈论过错误信息,不过在你已经试验过的那些例子中,可能已经遇到过一些.Python 中(至少)有两种错误:语法错误和异常( syntax errors 和 exceptions ) ...

  6. (五)AJAX技术

    一.定义 AJAX 是一种用于创建快速动态网页的技术. 通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新.这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新. 传统的 ...

  7. 网页DIV+CSS布局和动画美化全程实例 (陈益材) 随书光盘

    网站的建站技术近几年得到迅速的发展,网页的布局与特效动画技术层出不穷,网站建设已经从简单的技术支持时代衍变到现在的视觉美化时代.但如何使设计的网页高人一筹,达到让人过目不忘的境界,如何追求以最简单的特 ...

  8. 精通Web Analytics 2.0 (8) 第六章:使用定性数据解答”为什么“的谜团

    精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第六章:使用定性数据解答"为什么"的谜团 当我走进一家超市,我不希望员工会认出我或重新为我布置商店. 然而, ...

  9. MT【24】一道五次方程的求根题

    解答: 评:一般的五次及以上的多项式方程是无根式解的,只能用计算机去精确到某某位.但是特殊的比如$x^5=1$显然有根式解,本题就是一个不平凡的特殊的例子,这里的代换用于求解三次方程的求根过程是一样的 ...

  10. 软件工程 Coding.net代码托管平台 Git初学者的使用总结 五步完成 程序,文件,文件夹的Git

    一.前言 第一次用git相关的命令行,我使用的是Coding.net代码托管平台.Coding.net 自主打造的基于 Git 的代码托管平台,提供高性能的远端仓库,还有保护分支,历史版本分屏对比. ...

随机推荐

  1. SQL Server数据表模糊查询(like用法)详解

    在SQL Server Management Studio (SSMS) 中,进行模糊查询主要是通过使用like操作符来实现的.like操作符用于在where语句中搜索列中具有指定模式的数据.我们在简 ...

  2. 启动Node.js前端项目

    跟前端同事联调的时候,有时前端同事要跟其他后端同事联调,心想就自己启个前端项目,这样前端同事跟其他后端同事联调的时候,我可以先自己调来试试. 安装Node.js,NPM,NVM等,一气呵成.将前端项目 ...

  3. K8S钩子、探针以及控制器完整版

    一. 生命周期钩子 Kubernetes 中的 生命周期钩子(Lifecycle Hooks) 是在容器生命周期的特定阶段执行操作的机制.通过钩子,可以在容器启动后(PostStart)或停止前(Pr ...

  4. 编译器-FOLLOW集合

    语法分析器的两个重要函数 FIRST和FOLLOW 一.FOLLOW的定义 在句型中紧跟在A右边的终结符号的集合 如果A是某些句型的最右符号,那么$在FOLLOW(A)中 A:非终结符 二.计算方法 ...

  5. DSB的数字正交解调

    1.DSB调制过程 ​ DSB信号是一种双边带调幅调制信号,又叫双边带调幅,通过改变载波的振幅来实现基带数据的传输. 其函数表达式如下: \[s(t) = m(t)*cos(2\pi ft + \va ...

  6. Qt编写可视化大屏电子看板系统31-模块9视频监控

    一.前言 视频监控模块采用ffmpeg作为解码内核,可以在系统设置中填入一个视频地址,同时支持本地视频文件.USB摄像机.远程onvif摄像机(海康大华等).网络视频地址,大屏系统中默认有一个视频监控 ...

  7. Qt音视频开发45-视频传输TCP版

    一.前言 做音视频开发,会遇到将音视频重新转发出去的需求,当然终极大法是推流转发,还有一些简单的场景是直接自定义协议将视频传出去就行,局域网的话速度还是不错的.很多年前就做过类似的项目,无非就是将本地 ...

  8. MySQL数据库建库时SQL语句中数据库名、表名用引号的问题以及COLLATE utf8_general_ci的含义

    一.MySQL数据库建库时SQL语句中数据库名.表名用引号的问题解释:在创建MySQL数据库和表时,数据库名.表名和字段名外面的符号 ` 不是单引号,而是英文输入法的反单引号,同键盘~同一位置.为了避 ...

  9. 大型IM稳定性监测实践:手Q客户端性能防劣化系统的建设之路

    本文来自腾讯手Q基础架构团队杨萧玉.邱少雄.张自蹊.王褚重天.姚伟斌的分享,原题"QQ 客户端性能稳定性防劣化系统 Hodor 技术方案",下文进行了排版和内容优化. 1.引言 接 ...

  10. 【译】在分析器中使用 Meter Histogram(直方图)解锁见解

    您是否正在与应用程序中的性能瓶颈作斗争?不要再观望了!Visual Studio 2022 在其性能分析套件中引入了 Meter Histogram(直方图)功能,为您提供了前所未有的分析和可视化直方 ...