从jmm模型漫谈到happens-befor原则

Posted yangfeiORfeiyang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从jmm模型漫谈到happens-befor原则相关的知识,希望对你有一定的参考价值。

首先,代码都没有用ide敲,所以不要在意格式,能看懂就行
jmm内存模型:
jmm是什么?

jmm说白了就是定义了jvm中线程和主内存之间的抽象关系的一种模型,也就是线程之间的共享变量存储在主内存,而每个线程都拥有自己的工作内存

happens-befor原则是什么?

在说happens-befor原则之前,我们得先说说jmm的问题所在,如上述所述,每个线程都有自己的一个工作内存,那么我们以一个代码实例来看

public class Test{
int a=1;
public static void main(String []args){
for(int i=0;i<=100000;i++){
new Thread(()->{

a++;

}).start();

}
system.out.println(a);
}

}
OK,大家能看到,这里是有一个开启了100000个线程做自增操作,结果是99130,并不是预期中的100000,那么这是为什么呢?大家都知道,线程是CPU运行的最小单元,那么也就是说,多线程的情况下,cpu会去随机运行的(哪怕是设置了优先级也只是一个权重问题,无法保证强顺序
并且任务一定执行完),所以,因为我们的a++并不是一个原子操作的原因(实际上是4步),也就是说,很可能面临这种情况,当一个线程拿到了cpu的时间切片的时候,首先cpu会将a读到内存中,此时假设a=1然后弄一个临时变量,之后将临时变量增加,之后再返回结构到主内存,但如果在创建了
临时变量但还没有做自增操作的时候,cpu的时间切片突然换到了另一个线程的上面,这个时候这个线程成功做完了自增操作,此时a=2,之后cpu又切回了之前的线程,因为线程里有一个程序计数器,记录了当前线程运行到了哪行代码,所以这个时候第一个线程继续做+1操作,但此时
由于第一个线程的工作内存里的a还是1,所以这个时候线程a在+1之后还是2,之后刷到主内存,此时a=2.所以这两个线程虽然各自运行了一次a++操作,但主内存里的a其实只是加了一次而已.
那么怎么避免这种情况呢?此时就需要我们的happens-befor原则了

happens-befor原则:
1:程序在运行的时候必须按照编写的顺序一样,不能进行指令重排序,指令重排会导致什么后果呢?
public class Test{
int a = 0;
boolean b = false;
public void write(){
a=1;
b = true;
}
public void read(){
if(b){
a = a+1;
}
}
}
而指令重排后可能会是
public class Test{
int a = 0;
boolean b = false;
public void write(){
b = true;
a=1;

}
public void read(){
if(b){
a = a+1;
}
}
}
如果此时有两个线程
new Thread(->(
write();
)).start();
new Thread(->{
read();
}).stert();
假设write()方法线程肯定先于read()方法执行的情况下,此时可能会导致,在b=true的时候,read方法进入,并导致a最后=1,但我们代码的原意其实a=1优先执行的话,a=1的情况会因为read方法的bool没有变成true所以无法进入,因此指令重排已经
干扰到了我们的代码原意了.

那么在什么情况下指令不会重排呢?两种,一种是上下代码有依赖关系如:
int a = 1;
int b = a+1;
此时就不会出现重排;
还有一种就是使用大名鼎鼎的volatile关键字,它利用了内存屏障的性质保证了指令不会重排,最后来点拓展知识,就是long和double这种64字节的数据类型,在读到工作内存的时候不会原子性的,而是每次只读32字节,最后分两次读,但如果加了volatile关键字的话,那么内存屏障
能保证一次性读完64字节.

2:一个锁的解锁,必须要在这个程序的加锁之前,也就是说,我不解锁,那你就别想再加锁,保证了串行
3:对于共享数据,上一个线程对于它的修改,必须要对后续任意操作它的线程可见
4:传递性,假设有三个操作,a,b,c可以理解为a happens befor b,b happens befor c ,那么a happens befor c;

OK,volatile除了内存屏障之外,其实还有另一个作用,就是第三点,也就是保证了可见性,它是怎么保证的呢?其实就是将工作内存给去掉,也就是让每次cpu读数据都必须要主内存里里拿,就这样保证在一个线程修改了数据后,他对所有线程都是可见的.可惜的是,这种可见性也并不能保证线程安全,因为线程安全需要两个保证,一个可见性,还有一个原子性.
假设现有一个线程1,一个线程2,一个共享变量a=1,此时线程1将a拿到工作内存做a++操作,在它还没有返回的期间线程b也拿了a到工作内存做++操作,之后不管谁先返回,a都只是做了一次操作而已.所以volatile只能保证那些赋值操作的线程安全,如:Boolean bool = true;

最后,推荐大家看下CAS的源码,利用volatile加乐观锁,实现了不需要synchronized也能保证线程安全.






































































以上是关于从jmm模型漫谈到happens-befor原则的主要内容,如果未能解决你的问题,请参考以下文章

Juc11_Java内存模型之JMM八大原子操作三大特性读写过程happens-before

JVM技术专题深入研究JMM的实现原理之Happens-Before原则和As-If-Serial语义「入门篇」

Juc11_Java内存模型之JMM八大原子操作三大特性读写过程happens-before

Android-JMM内存模型-指令重排-Happens-Before原则-volatile-lock指令-内存屏障

Android-JMM内存模型-指令重排-Happens-Before原则-volatile-lock指令-内存屏障

JMM(Java 内存模型)详解