关于Java里面volatile关键字的重排序
程序员文章站
2022-05-11 15:03:47
...
Java里面volatile关键字主要有两个作用:
(1)可见性
(2)禁止指令重排序
第一条可见性比较容易理解,就是使用volatile修饰的共享变量,如果有一个线程修改了值,其他的线程里面是立即可见的。原理是对volatile变量的读写,都会强制线程操作从主内存。
第二条禁止指令重排序,能够保证局部的代码执行的顺序。假设我们现在有如下的一段代码:
从顺序上看a应该先执行,而b会后执行,但实际上却不一定是,因为cpu执行程序的时候,为了提高运算效率,所有的指令都是并发的乱序执行,如果a和b两个变量之间没有任何依赖关系,那么有可能是b先执行,而a后执行,因为不存在依赖关系,所以谁先谁后并不影响程序最终的结果。这就是所谓的指令重排序。
ok,接着我们继续分析下面稍加改动后的代码:
这段代码里,不管a和b如何乱序执行,c的结果都是3,因为c变量依赖a和b变量,所以c变量是不会重排序到a或者b之前,a和b也不会重排到c之后,这其实是由happens-before关系里面的单线程下的as-if-serial语义限制的。
这里面还有一种特殊情况,需要注意一下:
上面的例子中a和b变量,虽然没有依赖关系,但是在try-catch块里面发生了重排,b先执行,然后发生了异常,那么a的值最终还是3,由JVM保证在重排序发生异常的时候,在catch块里面作相关的特殊处理。这一点需要注意。
在单线程环境下,指令重排序是不会影响程序的最终执行结果的,但是重排序如果发生多线程环境下,就有可能影响程序正常执行,看下面的代码:
上面的代码里面,有三个成员变量,其中最后一个是用volatile修饰的,有2对方法:
第一对方法里面:
上面的代码,由于指令会重排序,当线程一里面执行write1方法的flag=true的时候,同时线程2执行了read1
方法,那么count的值是不确定的,可能是10,也可能是2,这个其实和操作系统有很大关系,如果cpu不支持指令重排,那么就不会出现问题,比如在X86的CPU上运行代码测试,可能不会出现多个值,但这不能说明其他的操作系统也不会出现。指令重排序在多线程环境下会带来不确定性,想要正确的使用,需要理解JMM内存模型。
第二对方法里面:
注意这里的sync变量是加了volatile修饰,意味着禁止了重排序,第一个线程调用write2方法时候,同样第二个线程在调用read2方法时候,如果sync=true,那么count的值一定是10,有朋友可能会说count变量没有用volatile修饰啊,如何保证100%可见性呢? 确实在jdk5之前volatile关键字确实存在这种问题,必须都得加volatile修饰,但是在jdk5及以后修复了这个问题,也就是在jsr133里面增强了volatile关键字的语义,volatile变量本身可以看成是一个栅栏,能够保证在其前后的变量也具有volatile语义,同时由于volatile的出现禁止了重排序,所以在多线程下仍然可以得到正确的结果。
总结:
在Java里面除了volatile有禁止重排序的功能,内置锁synchronized和并发包的Lock也都有同样的语义。同步手段解决的主要问题是要保证代码执行的原子性,有序性,可见性。内置锁和J.U.C的锁同时具有这三种功能,而volatile不能保证原子性,所以在必要的时候还需要配合锁一起使用,才能编写出正确的多线程应用。
jsr133详细介绍:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile
为了方便更好的交流,互助,学习,讨论问题,欢迎加入我们的“攻城师互助学习交流群”微信群,为了保证交流环境,想加入的小伙伴,可关注公众号,然后后台发送关键词微信群,加我微信号,由我拉你进去。
(1)可见性
(2)禁止指令重排序
第一条可见性比较容易理解,就是使用volatile修饰的共享变量,如果有一个线程修改了值,其他的线程里面是立即可见的。原理是对volatile变量的读写,都会强制线程操作从主内存。
第二条禁止指令重排序,能够保证局部的代码执行的顺序。假设我们现在有如下的一段代码:
``` int a=2; int b=1; ```
从顺序上看a应该先执行,而b会后执行,但实际上却不一定是,因为cpu执行程序的时候,为了提高运算效率,所有的指令都是并发的乱序执行,如果a和b两个变量之间没有任何依赖关系,那么有可能是b先执行,而a后执行,因为不存在依赖关系,所以谁先谁后并不影响程序最终的结果。这就是所谓的指令重排序。
ok,接着我们继续分析下面稍加改动后的代码:
``` int a=2; int b=1; int c=a+b; ```
这段代码里,不管a和b如何乱序执行,c的结果都是3,因为c变量依赖a和b变量,所以c变量是不会重排序到a或者b之前,a和b也不会重排到c之后,这其实是由happens-before关系里面的单线程下的as-if-serial语义限制的。
这里面还有一种特殊情况,需要注意一下:
``` int a = 1; int b = 2; try { a = 3; //A b = 1 / 0; //B } catch (Exception e) { } finally { System.out.println("a = " + a); } ```
上面的例子中a和b变量,虽然没有依赖关系,但是在try-catch块里面发生了重排,b先执行,然后发生了异常,那么a的值最终还是3,由JVM保证在重排序发生异常的时候,在catch块里面作相关的特殊处理。这一点需要注意。
在单线程环境下,指令重排序是不会影响程序的最终执行结果的,但是重排序如果发生多线程环境下,就有可能影响程序正常执行,看下面的代码:
``` public class ReorderDemo1 { private int count=2; private boolean flag=false; private volatile boolean sync=false; public void write1() { count=10; flag=true;//没有volatile修饰,实际执行顺序,有可能是flag=true先执行 } public void read1() { if(flag){ System.out.print(count); // 有些jvm会打印10,有些jvm会打印2,这是不确定的 } } public void write2() { count=10; sync=true;// 由于出现了volatile,所以这里禁止重排序 } public void read2() { if(sync){ System.out.print(count); // 在jdk5之后,由volatile保证,count的值总是等于10 } } public static void main(String[] args) { for(int i=0;i<300;i++){ //实例化变量 ReorderDemo1 reorderDemo1=new ReorderDemo1(); //写线程 Thread t1=new Thread(()-> { reorderDemo1.write1();}); //读线程 Thread t2=new Thread(()-> { reorderDemo1.read1(); }); t1.start(); t2.start(); } } } ```
上面的代码里面,有三个成员变量,其中最后一个是用volatile修饰的,有2对方法:
第一对方法里面:
``` private int count=2; private boolean flag=false; private volatile boolean sync=false; public void write1() { count=10; flag=true;//没有volatile修饰,实际执行顺序,有可能是flag=true先执行 } public void read1() { if(flag){ System.out.print(count); // 有些jvm会打印10,有些jvm会打印2,这是不确定的 } } ```
上面的代码,由于指令会重排序,当线程一里面执行write1方法的flag=true的时候,同时线程2执行了read1
方法,那么count的值是不确定的,可能是10,也可能是2,这个其实和操作系统有很大关系,如果cpu不支持指令重排,那么就不会出现问题,比如在X86的CPU上运行代码测试,可能不会出现多个值,但这不能说明其他的操作系统也不会出现。指令重排序在多线程环境下会带来不确定性,想要正确的使用,需要理解JMM内存模型。
第二对方法里面:
``` private int count=2; private boolean flag=false; private volatile boolean sync=false; public void write2() { count=10; sync=true;// 由于出现了volatile,所以这里禁止重排序 } public void read2() { if(sync){ System.out.print(count); // 在jdk5之后,由volatile保证,count的值总是等于10 } } ```
注意这里的sync变量是加了volatile修饰,意味着禁止了重排序,第一个线程调用write2方法时候,同样第二个线程在调用read2方法时候,如果sync=true,那么count的值一定是10,有朋友可能会说count变量没有用volatile修饰啊,如何保证100%可见性呢? 确实在jdk5之前volatile关键字确实存在这种问题,必须都得加volatile修饰,但是在jdk5及以后修复了这个问题,也就是在jsr133里面增强了volatile关键字的语义,volatile变量本身可以看成是一个栅栏,能够保证在其前后的变量也具有volatile语义,同时由于volatile的出现禁止了重排序,所以在多线程下仍然可以得到正确的结果。
总结:
在Java里面除了volatile有禁止重排序的功能,内置锁synchronized和并发包的Lock也都有同样的语义。同步手段解决的主要问题是要保证代码执行的原子性,有序性,可见性。内置锁和J.U.C的锁同时具有这三种功能,而volatile不能保证原子性,所以在必要的时候还需要配合锁一起使用,才能编写出正确的多线程应用。
jsr133详细介绍:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile
为了方便更好的交流,互助,学习,讨论问题,欢迎加入我们的“攻城师互助学习交流群”微信群,为了保证交流环境,想加入的小伙伴,可关注公众号,然后后台发送关键词微信群,加我微信号,由我拉你进去。
上一篇: Solr中Group和Facet的用法
下一篇: 学而不思则罔,思而不学则殆