Java memory model JMM five final memory semantics

 

In addition to volatile and synchronized to achieve visibility, the final keyword can also achieve visibility (but the reference to which the final field belongs cannot "escape" from the constructor). The visibility of synchronized synchronized blocks is obtained by the locking rules of happens-before. Let's study the memory semantics of the final keyword in detail.

 

For final variables, both the compiler and the processor obey two reordering rules:

 

Write final field rules : There is no reordering between the writing of a final field within a constructor and the subsequent assignment of a reference to the constructed object to a reference variable.

        This rule prohibits reordering the write operations of the final field outside the constructor, because executing the constructor to instantiate the object, the bottom layer can be divided into three operations: allocate memory, initialize member variables on the memory, and point the object instance reference to the memory . These three operations may be reordered, that is, first point the reference to memory, and then initialize member variables.

        This rule is also implemented by memory barriers: the compiler will insert a StoreStore barrier after the final field is written and before the constructor returns. This prevents the processor from reordering writes to final fields outside of the constructor.

        This rule guarantees that final variables of an object are properly initialized before the object reference becomes visible to any thread (but the reference cannot "escape" from the constructor), whereas ordinary variables do not have this guarantee.

  

Rules for reading final fields: The first read of a reference to an object containing a final field and the subsequent initial read of the final field cannot be reordered.

        This rule prohibits reordering reads of a final field before reading the object reference to which the final field belongs, which is not guaranteed for ordinary variables.

        This rule is also implemented through memory barriers: the compiler inserts a LoadLoad barrier before reads of final fields. So that to read the final field, you must first read the reference to which the final field belongs.

        This rule guarantees that before reading an object's final variables, the object's reference must be read. If the read reference is not empty, it means that its object reference has been visible to the current reading thread. According to the above writing final domain rules, it means that the final variable of the object must be initialized and the correct variable value can be read.

 

public class FinalExample {
    int i; //ordinary variable
    final int j; //final variable
    static FinalExample obj;

    public void FinalExample () { //Constructor
        i = 1; //write normal field
        j = 2; //write final field
    }

    public static void writer () { //write thread A executes
        obj = new FinalExample ();
    }

    public static void reader () { //Read thread B executes
        FinalExample object = obj; //read object reference
        int a = object.i; //read ordinary fields
        int b = object.j; //read final field
    }
}

 

 For the above sample code, it is assumed that a thread A executes the writer() method first, simulating the execution of a write operation, and then thread B executes the reader() method.

  According to the rules of reading final fields : it is entirely possible that the read operation of thread B on ordinary fields will be reordered before the operation of reading object references, thus forming an erroneous read operation, while the operations on final fields are due to the rules of reading final fields. For the guarantee, the reference of the object containing this final field must be read first. In this example, if the reference is not empty, then its final field must have been initialized by thread A, so the variable b must be 2.

 

  According to the rules for writing final fields : it is entirely possible that thread A's writes to ordinary fields will be reordered outside the constructor, but writes to final fields will not. So (assuming that thread B's read object reference operation and the normal field of the read object are not reordered): When thread B performs the read operation, it must be able to read the correctly initialized value 2 of the final field, but it may not be able to The value 1 is read after the normal field is initialized, but the value 0 before initialization may be read.

 

 final fields of reference types

 The above examples are all final fields of basic types. If it is a final field of reference type, in addition to the above rules for reading and writing final fields, the reordering rules for writing final fields add the following constraints to the compiler and processor :

 Additional rules for writing final fields : The writing to a field of a final referenced object within the constructor and the subsequent assignment of the reference to the constructed object to a reference variable outside the constructor cannot be separated. Reorder.

          This rule ensures that final member variables of an object are properly initialized before they are visible to other threads.

 

public class FinalReferenceExample {
    final int[] intArray;
    static FinalReferenceExample obj;
 
    public FinalReferenceExample() {
        intArray = new int[1];// 1
        intArray[0] = 1;// 2
    }
 
    public static void writerOne() {// A线程执行
        obj = new FinalReferenceExample(); // 3
    }

    public static void writerTwo () {          //写线程B执行
        obj.intArray[0] = 2;                 //4
    }
 
    public static void reader() {// 读线程 C 执行
        if (obj != null) { // 5
            int temp1 = obj.intArray[0]; // 6
        }
    }
}
 

 

 针对上面的示例代码,这里假设一个线程A执行writerOne()方法,执行完后线程 B 执行writerTwo()方法,执行完后线程 C 执行reader ()方法,根据普通final域读写规则,操作1和操作3不能重排序,根据引用类型final域的写规则,操作2和操作3也不能重排序,JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。而写线程B对数组元素的写入,读线程C可能看的到,也可能看不到。JMM不能保证线程B的写入对读线程C可见,因为写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。

 

如果想要确保读线程C看到写线程B对数组元素的写入,写线程B和读线程C之间需要使用同步原语(lock或volatile)来确保内存可见性。

 

 避免对象引用在构造函数当中溢出

写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了即对其他线程可见。这就是在文章开头final关键字带来的可见性实现。

但是要得到这个效果,有一个前提条件:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。

 

public class FinalReferenceEscapeExample {
    final int i;
    int j;
    static FinalReferenceEscapeExample obj;
 
    public FinalReferenceEscapeExample() {
        i = 1;                    // 1
        j = 2;                    // 2
        obj = this;               // 3 避免怎么做!!!对象引用逸出。
    }
 
    public static void writer() {
        new FinalReferenceEscapeExample();
    }
 
    public static void reader() {
        if (obj != null) {        // 4
            int a = obj.i;     // 5
            int b = obj.j;     //6
        }
    }
}
 

 

针对上面的this引用逸出构造函数的示例代码,假设一个线程A执行writer()方法,另一个线程B执行reader()方法。这里的操作3使得对象还未完成构造前引用就为线程B可见。即使这里的操作3是构造函数的最后一步,且即使在程序中操作3排在操作1和操作2后面,执行read()方法的线程B仍然可能无法看到final域以及普通域被初始化后的值,因为这里的操作1和操作2、操作3之间可能被重排序。

 

因此,在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的 final 域可能还没有被初始化,如果对象引用提前逸出,将破坏final关键字的语义,也就是说,final关键字将不能保障原有的可见性。

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326488512&siteId=291194637