volatile、final内存含义以及happen-before规则

volatile和final在线程同步时起到很大的作用,那么在Java内存中这两个关键字是如何和线程同步关联起来呢,以及线程的happen-before规则又是怎么定义的呢?

volatile的内存含义

volatile用来修饰变量,可以保证变量每次都是从主内存中读取,但是它不保证原子性,先来看看具体的例子:

public class VolatileExample {
    private static volatile long v1 = 10L;
    public static void setV1(long l){
        v1 = l;
    }
   public static long getV1(){
        return v1;
    }
 
    public static void getAndIncrement() {
        v1++;
    }
 
    public static void main(String[] args){
        for (int i = 0;i < 1000;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    getAndIncrement();
                }
            }).start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: "+v1);
    }
}

上面getAndIncrement()等同于下面这个:

   
   /**
    * 这里有三个操作,读取V1的值,对V1进行+1操作,写V1的值,如果线程1进行到第二步时,
    * 此时线程2去读取V1的值,还是V1没有+1前的值,所以此时取的值就不对了,因为线程1还没
    * 有来得及将V1的值刷新到主内存,所以说明volatile能够保证每次的操作能够被其他线程及时
    * 由于对操作不具有原子性,当一个操作分步执行时,其他线程就有可能取到脏数据。
    * */
  public static void getAndIncrement() {
        v1++;
  }

volatile禁止重排序规则

看下下面的例子:

class VolatileExample{
   int a = 0;
   volatile boolean flag = false;  //volatile修饰变量
   
   public void writer(){
      a = 1; 	//	语句1
      flag = true; //	语句2
    }
    
    public void reader(){
       if(flag){	//	语句3
       	int i = a;	//	语句4
       	...
      }

存在问题
若flag没有被volatile修饰,那么上面的语句1,2,3,4就存在重排序的可能,那么假设线程1执行writer()函数,线程2执行reader(),若线程1先执行了语句2,此时线程2执行语句3,发现flag 为true,那么就执行语句4,此时线程1还没有执行语句1,那么语句4获得的a值就不是1了。虚拟机的重排序只是保证同一个线程内的语句不会被重排序影响,但是线程间的执行顺序并不能保证,如果对flag使用了volatile修饰,就能避免上述问题,volatile对于重排序有如下规则:

volatile重排序规则:
1.当第二个操作是volatile写操作时,不管第一个操作是什么,禁止重排序,所以语句1和2不能重排序
2.当第一个操作是volatile读操作时,不管第二个操作是什么,禁止重排序,所以语句3和4不能重排序
3.当第一个操作是volatile写,第二个操作是volatile读时,禁止重排序

所以,有了volatile修饰,上述语句1和2不能进行重排序,语句3和4也不会重排序,那么在两个线程中执行上述reader()和writer()就不会出现上述问题。

写final域的重排序规则

final对于重排序规则有如下的限制:
final域的写排序不能被重排序到构造函数之外,即是实例对象在被任意线程可见之前,它的final域一定已经被初始化过了,但是若普通域就不具有这个保证,Java内存模型会在构造函数返回之前,加入一个StoreStore屏障,即:

写final域;
StoreStore屏障;
构造函数return

看看以下代码:

public class FinalExample{
   static FinalExample obj;
   int i;	//普通变量
   final int j ;	//final变量
   
   public FinalExample(){
   		i = 10;	 //构造函数返回和对i写值这两个操作可能重排序
   		j = 20;  //构造函数返回之前保证j已经写入20
   	}
   	
   	public void writer(){
   	/**
   	 * 在执行构造函数时,会保障j在构造函数return前初始化,但是不保障普通域i在构造函数返回
   	 * 前初始化
   	 * */
   		obj = new FinalExample();	
    }
    
    public void reader(){
    	FinalExample object = obj;
    	/**
    	 *  若线程B执行这个reader函数,由于重排序,可能obj已经初始化到实例了,但是i = 10还没
    	 * 有执行,所以获取到的i值是原始值。
    	 * */
    	int a = obj.i;  
    	/**
    	  * 但由于final域的写后面加入了内存屏障StoreStore,禁止处理器把final域的写重排序到构造
    	  * 函数之外,所以这里获取到的j值肯定是正确的。
    	  * */
    	int b = obj.j;   
   } 			

读final域的重排序规则

在一个线程中,初次读对象引用与初次读对象包含的final域,Java 内存模型禁止这两个操作重排序,编译器会在读final域前插入一个LoadLoad屏障,即:

读对象引用 ;
LoadLoad屏障;
读final域;

fianl域不能从构造函数“溢出”

例子如下:

public class FinalExample {
    private final int i;
    private static FinalExample obj;

    public FinalExample(){
        i = 2;   //语句1
        obj = this; //语句2,这两个语句可能重排序,而且这个语句造成了对象引用溢出
    }
    public static void writer(){
        new FinalExample(); //线程A执行writer()
    }
    public static void reader(){
        if(obj != null){    //线程B执行reader(),若语句2先执行,此时obj不为空,但是事实上i还没有初始化,造成final域在构造函数溢出。
            int temp = obj.i;
        }
    }
}

happen-before规则

定义

happen-before指定两个操作之间的执行顺序,这两个操作可以是一个线程之内,也可以是不同线程之间,JMM(Java内存模型)通过happen-before规则向程序猿提供跨线程的内存可见性,即如果线程A的写操作a happen-before于线程B的读操作,尽管a操作和b操作在不同的线程中执行,但是JMM向程序猿保证a的结果对b操作可见。

happen-before规则定义

1.程序顺序执行规则: 一个线程中的任意操作,happen-before于该线程的任意后续操作

2.监视器锁规则: 对一个锁的解锁,happen-before于后续对该锁的加锁

3.volatile规则: 对一个volatile变量的写操作,happen-before于后续对该volatile的读操作

4.传递性: 如果A happen-before于 B,B happen-before C,那么A happen-before C

5.start()规则 如果线程A执行ThreadB.start()启动线程B,那么A的ThreadB.start()操作happen-before于线程B的任意操作

6.join()规则 如果线程A执行ThreadB.join()并成功返回,那么线程B的任意操作happen-before于线程A的ThreadB.join().

总结

  1. 程序猿在编写代码时,都是按顺序编写的,先操作1,再操作2,那么也会期待操作1先于操作2执行,但是实际上,JVM不一定会保证操作1就一定先于操作2执行,但是它会保证操作1的结果对操作2的结果可见,什么时候会重排序呢?操作1和2没有数据以来关系时,这是第一种happen-before规则;同时很容易理解,happen-before具有传递性;
  2. 另外的一些规则例如监视器锁规则,对一个锁的解锁,happen-before后续对该锁的加锁也是很好理解的;
  3. volatile规则,即是我们定义一个变量为volatile时,即使后续在不同线程内对该变量进行读写操作,JMM也能够保证读操作的结果对于写操作可见;
  4. 另外关于线程的start()、join(),即使我们没有对于线程的启动、join()加入任何的锁,但是JMM会保证线程的启动先于线程内部操作执行,线程的内部操作执行先于下一个线程的join()的后续操作。

总结下来,其实happen-before就是JMM对于程序员的一个保证,以及对于编译器和处理器执行重排序时的约束。

猜你喜欢

转载自blog.csdn.net/qq_26984087/article/details/87885670
今日推荐