说明:转载本人掘金文章 https://juejin.cn/post/7082242009242550285
as-if-serial语义
as-if-serial语义初衷是为了提高指令执行速度(指令执行顺序优化),大概语义是保证在单线程情况下可以允许多个指令在没有数据依赖的情况下可以进行指令重排。
数据没有依赖
public static void test3(){
int a=0,b=0;
a=1; //和b=1没有数据依赖
b=1; //和a=1没有数据依赖
}
这种情况第二句和第三句没有数据依赖,是可以指令重排,哪个先执行都一样,遵守as-if-serial语义
数据有依赖
public static void test4(){
int a=0,b=0;
a=1; //和b=1+a*a没数据依赖
b=1+a*a; //和a=1有数据依赖
}
这种情况第二句和第三句有数据依赖,是不可以指令重排,会影响到最后结果一致性义。为了遵守as-if-serial语义,这种情况不会进行重排
总结
根据JMM,JAVA代码在单线程情况下,没有内存可见性问题; 遵守as-if-serial语义,也没有指令重排问题
多线程内存可见性和重排问题
可见性问题
/**
* 多线程可见性问题
*/
private static void test2(){
InterruptTest.Flag flag=new InterruptTest.Flag();
Thread t1 = new Thread(() -> {
System.out.println("开始执行");
while (!flag.flag) {
//System.out.println("执行中");
}
System.out.println("执行完成");
});
t1.start();
sleep(2000L);
//打断
new Thread(()->{
flag.flag=true;
System.out.println("中断执行");
}).start();
}
由于是多线程,基于JMM,多线程之间是基于cpu缓存执行的,共享变量不能实时可见,无法及时交换线程数据,导致两线程无法知道互相状态,导致运行结果不一致。
重排序问题
//单例模式
class MyBeanFactory {
//策略类
private class MyBean {
}
private MyBean instance;
public MyBean getInstance() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new MyBean();
}
}
}
return instance;
}
}
这种模式单例,一般是为了延迟加载初始化对象。所以一般实例化这种对象都是在多线程环境下运行的,getInstance一般是多个用户线程同时执行的,这里也加了同步代码块,其实只有一个线程会执行instance = new MyBean(),但是这句底层是多个指令,并且多个指令满as-if-serial语义,可以重排。单线程执行是没问题的,但是多线程情况下,第一次检测instance == null是可以多个线程执行的,由于instance = new MyBean()重排,会返回一个不完整的对象。
happens-before规则
规则来源于java并发编程艺术。其实happens-before规则和as-if-serial语义的目的一样,只不过happens-before规则是为了多线程解决多线程的可见性问题和重排序问题。
a.规则1 只要存在happens-before关系,前一个操作都对后一个可见
b.规则2用来防止多线程代码块重排(并发执行)而设定的,多个线程的同一个synchronized代码块之间,执行是有序的,多个代码块之间通过锁来互斥,这么就达到了多个线程代码之间的happens-before关系
c.规则3用来解决多线程共享变量可见性问题的,这也是基于JMM的原因,单线程不存在这个问题,多线程存在可见性问题
volatile关键字语义
1.volatile实现了happens-before的规则3,保证多线程共享变量可见性
2.volatile是as-if-serial语义的增强,使用volatile修饰的变量,使用内存屏障,来保证单线程内指令重排
synchronized关键字语义
1.synchronized保证了多线程代码的执行顺序,或者说是happens-before关系,实现了happens-before的规则2,也就是解决了多线程指令重排(并发执行指令)问题
2.synchronized确立了多线程的happens-before关系,也就符合规则1,保证符合happens-before关系之间的可见性,也就是synchronized代码内是可见的
3.synchronized内部代码还是遵守as-if-serial语义,在不破坏语义情况下,可以重排
栏目2提出问题解决方案
问题1解决方案
两个线程只是为了达到共享变量的可见性,并不对两个线程代码执行上有严格的顺序,所以只要满足可见性,加上volatile修饰就可
问题2解决方案
单例的创建是在多个线程并发情况执行的,主要是对多线程代码执行顺序导致的,也就是主要涉及重排问题,这里有两个解决方案。
1.保证单线程代码内部不重排序,直接使用volatile修饰,弥补as-if-serial的不足
2.保证多线程代码不重排序(有序执行),直接把整个方法进行synchronized,这样其它线程无法拿到因为单线程重排导致返回错误的结果
扩展问题
static class Flag{
//volatile boolean flag=false;
boolean flag=false;
}
/**
* 多线程可见性问题
*/
private static void test2() {
InterruptTest.Flag flag = new InterruptTest.Flag();
Thread t1 = new Thread(() -> {
System.out.println("开始执行");
while (!flag.flag) {
//System.out.println("执行中");
}
System.out.println("执行完成");
});
t1.start();
sleep(2000L);
//打断
Object o=new Object();
new Thread(() -> {
synchronized (o) {
flag.flag = true;
System.out.println("中断执行");
}
}).start();
}
大家觉得线程t1能停止下来吗? 答案留言,后面我会告诉大家(是否存在happens-before关系思考)
总结
as-if-serial语义和happens-before规则的目的都一样,都是为了提高执行效率,但是又能保证执行语义一致而出现的规则。as-if-serial语义保证单线程的可见性和重排序(volatile作为实现补充);happens-before保证多线程的可见性和重排序(volatile,synchronized作为实现)。