同事写了一个后台线程,主线程把 running 改成 false,工作线程却像没听见,日志一直刷。代码里明明赋值了,问题常出在变量没加 volatile,线程读到的是自己工作内存里的旧值。
volatile 保证的是可见性和有序性
java volatile 修饰的变量,写入后会尽快对其他线程可见,读的时候一般会重新去主内存拿值。它还会阻止一部分指令重排,典型用途是一个线程改标志位,另一个线程循环检查。它不等同于加锁,也不保证复合操作原子。
count++ 为什么照样会丢
count++ 看着是一行,实际分成读 count、加一、写回三步。两个线程同时读到 10,各自写回 11,最后就少加了一次。把 count 声明成 volatile 也救不了,得换 AtomicInteger,或者用 synchronized 包住自增。判断一段代码安不安全,先看它是不是只有一个线程写。
一个常见的停止标志写法
volatile boolean running = true; while (running) { doWork(); } running = false;这段代码的重点是标志位只由外部线程改,工作线程只读。若循环里还要累加计数,计数变量不能跟着沾光,仍要单独处理。volatile 的读写成本不高,但也别给所有字段都加上,先弄清共享和可见性再说。
真机上排查时,用 javap -c -p 类名 看字节码,普通字段和 volatile 字段的访问指令不一样;再用 jstack 看线程状态,确认到底是卡在循环还是锁上。若现象只在 server 模式、运行一段时间后出现,JIT 优化把判断提到循环外的可能性就要考虑。改完压测一轮,比盯着源码猜更靠谱。
该用和不该用的边界
一个写、多个读的状态开关适合 volatile;多线程一起改的计数器不适合。需要一组变量一起变化时,volatile 也管不住整体一致性,还是要锁或不可变对象。java volatile 不是性能开关,它解决的是可见性问题,别把它当万能补丁。