【Java并发编程系列2】volatile
发布时间:2026/9/30 7:10:15 锦皓数字建站

主要讲解volatile的相关知识以及容易遇到的坑。volatile变量的特性保证可见性不保证原子性当写一个volatile变量时JMM会把该线程本地内存中的变量强制刷新到主内存中去这个写操作会导致其他线程中的volatile变量缓存无效。禁止指令重排我们回顾一下重排序需要遵守一定规则重排序操作不会对存在数据依赖关系的操作进行重排序。比如a1;ba; 这个指令序列由于第二个操作依赖于第一个操作所以在编译时和处理器运行时这两个操作不会被重排序。重排序是为了优化性能但是不管怎么重排序单线程下程序的执行结果不能被改变。比如a1;b2;cab这三个操作第一步a1)和第二步(b2)由于不存在数据依赖关系 所以可能会发生重排序但是cab这个操作是不会被重排序的因为需要保证最终的结果一定是cab3。volatile禁止指令重排规则使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量在编译时会在指令序列中插入内存屏障来禁止特定类型的处理器重排序volatile禁止指令重排序也有一些规则当程序执行到volatile变量的读操作或者写操作时在其前面的操作的更改肯定全部已经进行且结果已经对后面的操作可见在其后面的操作肯定还没有进行在进行指令优化时不能将对volatile变量访问的语句放在其后面执行也不能把volatile变量后面的语句放到其前面执行。即执行到volatile变量时其前面的所有语句都执行完后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。volatile禁止指令重排分析该部分相关内容我直接copy上一篇文章不是为了凑篇幅因为有同学没有看上一篇文章直接看这篇为了能让每一篇文章能独立成章可能会引用之前文章中的内容。先看下面未使用volatile的代码class ReorderExample { int a 0; boolean flag false; public void writer() { a 1; //1 flag true; //2 } Public void reader() { if (flag) { //3 int i a * a; //4 System.out.println(i); } } }因为重排序影响所以最终的输出可能是0具体分析请参考我的上一篇文章《Java并发编程系列1-基础知识》如果引入volatile我们再看一下代码class ReorderExample { int a 0; boolean volatile flag false; public void writer() { a 1; //1 flag true; //2 } Public void reader() { if (flag) { //3 int i a * a; //4 System.out.println(i); } } }这个时候volatile禁止指令重排序也有一些规则这个过程建立的happens before关系可以分为两类根据程序次序规则1 happens before 2; 3 happens before 4。根据volatile规则2 happens before 3。根据happens before 的传递性规则1 happens before 4。上述happens before关系的图形化表现形式如下在上图中每一个箭头链接的两个节点代表了一个happens before 关系。黑色箭头表示程序顺序规则橙色箭头表示volatile规则蓝色箭头表示组合这些规则后提供的happens before保证。这里A线程写一个volatile变量后B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量在B线程读同一个volatile变量后将立即变得对B线程可见。volatile不适用场景volatile不适合复合操作下面是变量自加的示例public class volatileTest { public volatile int inc 0; public void increase() { inc; } public static void main(String[] args) { final volatileTest test new volatileTest(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j1000;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保证前面的线程都执行完 Thread.yield(); System.out.println(inc output: test.inc); } }测试输出inc output:8182因为inc不是一个原子性操作可以由读取、加、赋值3步组成所以结果并不能达到10000。解决方法采用synchronizedpublic class volatileTest1 { public int inc 0; public synchronized void increase() { inc; } public static void main(String[] args) { final volatileTest1 test new volatileTest1(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j1000;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保证前面的线程都执行完 Thread.yield(); System.out.println(add synchronized, inc output: test.inc); } }采用Lockpublic class volatileTest2 { public int inc 0; Lock lock new ReentrantLock(); public void increase() { lock.lock(); inc; lock.unlock(); } public static void main(String[] args) { final volatileTest2 test new volatileTest2(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j1000;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保证前面的线程都执行完 Thread.yield(); System.out.println(add lock, inc output: test.inc); } }采用AtomicIntegerpublic class volatileTest3 { public AtomicInteger inc new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final volatileTest3 test new volatileTest3(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j100;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保证前面的线程都执行完 Thread.yield(); System.out.println(add AtomicInteger, inc output: test.inc); } }三者输出都是1000如下add synchronized, inc output:1000 add lock, inc output:1000 add AtomicInteger, inc output:1000单例模式的双重锁为什么要加volatile先看一下单例代码public class penguin { private static volatile penguin m_penguin null; // 避免通过new初始化对象 private void penguin() {} public void beating() { System.out.println(打豆豆); }; public static penguin getInstance() { //1 if (null m_penguin) { //2 synchronized(penguin.class) { //3 if (null m_penguin) { //4 m_penguin new penguin(); //5 } } } return m_penguin; //6 } }在并发情况下如果没有volatile关键字在第5行会出现问题。instance new TestInstance();可以分解为3行伪代码a. memory allocate() //分配内存 b. ctorInstanc(memory) //初始化对象 c. instance memory //设置instance指向刚分配的地址上面的代码在编译运行时可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序即先执行了a和c没有执行b。那么由于A线程执行了c导致instance指向了一段地址所以B线程判断instance不为null会直接跳到第6行并返回一个未初始化的对象。总结volatile可以保证线程可见性且提供了一定的有序性但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现加入volatile关键字时会多出一个lock前缀指令lock前缀指令实际上相当于一个内存屏障也称内存栅栏内存屏障会提供3个功能它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置也不会把前面的指令排到内存屏障的后面即在执行到内存屏障这句指令时在它前面的操作已经全部完成它会强制将对缓存的修改操作立即写入主存如果是写操作它会导致其他CPU中对应的缓存行无效。最后也讲解了volatile不适用的场景以及解决的方法并解释了单例模式为何需要使用volatile。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。