并发编程-java内存模型

时间:2022-11-12 17:57:50

文/属衣

并发编程-java内存模型

             Java内存模型的抽象构示意

 Java内存模型包含主内存和工作内存(本地内存),所有的变量都存储在主内存中。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。

A线程与B线程的通信过程:1)线程A把本地内存共享变量刷新到主内存 2)B线程读取A线程已更新过的共享变量

但实际情况,A线程与B线程的通信会出现“脏读”等问题。

1.原子性

       原子性即一个操作或者多个操作要么全部成功执行,要么都不执行。

  int i=10;①   i--;②   j=i;  ③

只有语句①是原子性,其余需要先读取变量再进行赋值,所以是非原子性。 

2.可见性

  可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  int i=10;①   j=i;②

线程A在执行语句①,并未将赋值后的结果刷新到主内存,线程B进行语句②操作时,导致读取到旧值,无法保证可见性。

3.有序性

  有序性即程序执行的顺序按照代码的先后顺序执行。一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

  在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

线程A:
context = loadContext();
inited = true;
线程B:
while(!inited ){
   sleep()
}
initConfig(context);
语句可能会被重排序,假如线程A先执行了语句,会导致线程B直接跳过while循环,导致 initConfig(context)失败!
Java内存模型具备一些先天的“有序性”,即happens-before原则(先行发生原则)。

volatile

  volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。

1.使用volatile修饰的共享变量(类的成员变量、类的静态成员变量),保证了可见性与有序性(禁止进行指令重排序)。

线程A:
boolean stop = false ;
while (!stop){
  doSomething();
}
线程B:
stop = true ;
以上代码可能出现死循环!线程B先将stop变量读到工作内存进行修改,然后一直没有将stop修改后的结果刷新到主内存,导致线程A 一直处于while循环中。
使用volatile修饰时,线程B的stop变量会被立即刷新到主内存,线程A会及时退出while循环。
2.volatile保证不了原子性。
public class Test {
     public volatile int inc = 0 ;
     public void increase() {
         inc++;
     }
     public static void main(String[] args) {
         final Test test = new Test();
         for ( int i= 0 ;i< 10 ;i++){
             new Thread(){
                 public void run() {
                     for ( int j= 0 ;j< 1000 ;j++)
                         test.increase();
                 };
             }.start();
         }
 
         while (Thread.activeCount()> 1 //保证前面的线程都执行完
             Thread.yield();
         System.out.println(test.inc);
     }
}
大部分执行结果小于10000!
这是因为 increase不是原子性的,假如线程1执行时inc=10,线程1先将inc读取到工作内存(此时保证读取到的是最新结果),此时被阻塞。线程2将inc读取到工作内存并进行+1操作,然后放入主内存inc=11,线程1接着进行inc+1操作(inc已经被读取过了inc=10),所以会将inc=11放入主内存。
可以选择java.util.concurrent.atomic包下的原子操作类保证原子性。

volatile的实现原理

1.可见性

  处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

2.有序性

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

volatile的应用场景

1.状态标记

2.单例double check

class Singleton{
     private volatile static Singleton instance = null ;
 
     private Singleton() {
 
     }
 
     public static Singleton getInstance() {
         if (instance== null ) {
             synchronized (Singleton. class ) {
                 if (instance== null )
                     instance = new Singleton();
             }
         }
         return instance;
     }
}
instance = new Singleton();在jvm中经历了:
1)为instance分配内存
2)初始化成员变量
3)将instance对象指向分配的内存空间
在jvm的即时编译器中存在指令排序的优化,有可能出现1-3-2的顺序,当3执行完毕,另一个线程直接判断instance不为空而直接返回并在使用中发生错误。