12. 并发终结之final/static语义

在前面说到安全发布的时候我们提到了final和static。

如何保证对象安全的发布?
1.对象的引用定义成volatile类型或者AtomicReference
2.将对象的引用保存到一个锁的保护域中
3.static静态初始化函数中初始化一个对象引用
4.将对象引用保存到final类型域中

static

static关键字在多线程环境下有特殊的含义,它能够保证一个线程在未使用其他同步机制的情况下也总是能读取到一个类的静态变量的初始值(不是默认值),但是这种保证仅限于线程初次读取该变量,也就是说如果这个静态变量在初始化完毕之后被其他线程更新,那么这个线程在读取这个静态变量就需要借助锁或者volatile关键字等同步手段。
对于引用型变量,static关键字还能保证一个线程读取到该变量初始值时,这个值所指向(引用)的对象已经初始化完毕。
我们再回忆一下,static静态变量在JVM中的存储位置,JDK7之前静方法区存放虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据这些,JDK7的时候就将静态变量,类Class对象信息,字符串常量池都移到了堆内存;JDK8则将方法区剩下的内容(主要是类型信息)放到元数据空间
在new Object()的过程分为“加载”,“验证”,“准备”,“解析”,“”初始化,“使用”,“卸载”。而与static静态变量相关的就是“准备”,“初始化”阶段。

  • 准备阶段:正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初 始值的阶段。
public static int value = 123; 

变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把 value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值 为123的动作要到类的初始化阶段才会被执行。

public static final int value = 123;

这个加了final,那么类字段的字段属性表中存在ConstantValue属性,编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置 将value赋值为123

  • 初始化阶段:初始化阶段就是执行类构造器()方法的过程,()是javac自动编译生成的,()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块(static{}块)中的 语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的(注意()方法区别于类的构造函数(即在虚拟机视角中的实例构造器()方法不同)。

Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同 时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其他线程都需要阻塞等 待,直到活动线程执行完毕()方法。

这句话就解释了“它能够保证一个线程在未使用其他同步机制的情况下也总是能读取到一个类的静态变量的初始值(不是默认值)”。

final

前面我们提到多次DCL单例模式如果不加volatile会出现拿到对象引用,但是对象还未初始化完毕的现象。
先谈谈final在多线程中的语义:当一个对象发布到其他线程时,该对象的所有final字段(实例变量)都是初始化完毕的;对于引用型变量,final关键字还进一步确保该字段所引用的对象也初始化完毕。

public class Test {
    final int x;
    int y;

    public Test() {
        this.x = 1;
        this.y = 2;
    }
}

先看这个基本类型的final变量,在new Test()的时候伪代码如下

1 objRef = allocate(Test.class);//对象分配空间
2 objRef.x = 1;//对象初始化
3 objRef.y = 2;//对象初始化
4 instance = objRef//将对象引用写入共享变量

其中操作3(非final字段初始化)可能被JIT优化重排序到操作4之后,因此当其他线程通过instance变量访问对象时,对象的y可能还没初始化。而操作2对应final字段,处理器会禁止将操作2重排序。
且注意final关键字只保证有序性,即保障一个对象对外可见的时候,该对象的final字段一定是初始化完毕的,但是并不保证final字段的对象引用自身对其他线程的可见性。

image.png

另外补充一个知识点:为什么匿名内部类要求引用的局部变量要是final的?
匿名内部类可以使你的代码更加简洁,你可以在定义一个类的同时对其进行实例化。它与局部类很相似,不同的是它没有类名,如果某个局部类你只需要用一次,那么你就可以使用匿名内部类。
下面的代码里使用了匿名内部类创建了Runnable实例。

public class Test {
    public static void main(String[] args) {
        String test = "";
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(test);
            }
        }).start();
    }
}

经过Javac生成class文件,可以看到Test$1.class的类给内部类。匿名内部类实质上会定义一个构造函数将局部变量当做构造器参数传递进入匿名内部类(因为方法局部变量在栈上,匿名内部类在堆上,生命周期不一致,如果不传到匿名内部类,会导致方法结束,成员变量被回收,而匿名内部类访问不存在的成员变量,所以这里需要复制成员变量到匿名内部类),并且以成员变量形式存在于内部类,内部了则使用自己的成员变量,这里涉及到值传递和引用传递。
对于值传递和引用传递,值传递,外部变量和内部变量没有联系;引用传递,外部变量和内部变量都指向同一地址,但也可以说是值传递,因为内部变量可以重新赋值新对象。这么说既然内外不能同步,如果想要保证匿名内部类和外部变量保持一致性,那就不许大家改外围的局部变量。那么为了保证数据一致性,用final避免了当匿名内部类拿到了成员变量的地址,而后成员变量发生变化,那么程序运行结果与预期不符合。
更简单的解释:为了防止在匿名内部类中的方法执行之前改变外部类局部变量的值,避免一些不可预测(奇怪)的问题,必须把外部类的局部变量声明为final。

final class Test$1 implements Runnable {
    Test$1(String var1) {
        this.val$test = var1;
    }
    public void run() {
        System.out.println(this.val$test);
    }
}

你可能感兴趣的:(12. 并发终结之final/static语义)