In-depth understanding of Java memory model (6)-final keyword

Compared with the lock and volatile mentioned earlier, the reading and writing of the final field are more like ordinary variable access. For the final domain, the compiler and processor must comply with two reordering rules:

  1. Writing to a final field in the constructor and subsequently assigning the reference of the constructed object to a reference variable cannot be reordered between these two operations.
  2. Reading a reference to an object containing a final field for the first time, and reading this final field for the first time thereafter, cannot be reordered between these two operations.

Below, we use some example codes to illustrate these two rules separately:

public class FinalExample {
    int i;                            // 普通变量 
    final int j;                      //final 变量 
    static FinalExample obj;

    public void FinalExample () {     // 构造函数 
        i = 1;                        // 写普通域 
        j = 2;                        // 写 final 域 
    }

    public static void writer () {    // 写线程 A 执行 
        obj = new FinalExample ();
    }

    public static void reader () {       // 读线程 B 执行 
        FinalExample object = obj;       // 读对象引用 
        int a = object.i;                // 读普通域 
        int b = object.j;                // 读 final 域 
    }

}

It is assumed that one thread A executes the writer () method, and then another thread B executes the reader () method. Below we illustrate these two rules through the interaction of these two threads.

 

Write reordering rules for final fields

The reordering rules for writing final fields prohibit reordering the writing of final fields outside of the constructor. The implementation of this rule includes the following two aspects:

  • JMM prohibits the compiler from reordering the writing of final fields outside of the constructor.
  • The compiler will insert a StoreStore barrier after the final domain is written and before the constructor return. This barrier prohibits the processor from reordering writes of final fields outside of the constructor.

Now let us analyze the writer () method. The writer () method contains only one line of code: finalExample = new FinalExample (). This line of code consists of two steps:

  1. Construct an object of type FinalExample;
  2. Assign the reference of this object to the reference variable obj.

Assuming that there is no reordering between thread B's read object reference and the member fields of the read object (we will explain why this assumption is needed soon), the following figure is a possible execution sequence:

In the above figure, the operation of writing the common field was reordered by the compiler outside the constructor, and the reader thread B read the value of the common variable i before the initialization by mistake. The operation of writing the final field is "limited" by the reordering rules of writing the final field within the constructor, and the reader thread B correctly reads the value after the final variable is initialized.

Writing the reordering rules for final fields can ensure that the final field of the object has been initialized correctly before the object reference is visible to any thread, and the ordinary field does not have this guarantee. The above figure is an example. When the reader thread B "sees" the object referencing obj, it is likely that the obj object has not been constructed yet (the write operation to the ordinary domain i is reordered outside the constructor, and the initial value 2 has not been written yet Ordinary domain i).

 

Reordering rules for reading final fields

The reordering rules for reading final fields are as follows:

  • In a thread, for the first time to read the object reference and the first time to read the final field contained in the object, JMM prohibits the processor from reordering these two operations (note that this rule is only for the processor). The compiler will insert a LoadLoad barrier before the read final domain operation.

There is an indirect dependency between the first reading of the object reference and the first reading of the final field contained in the object. Since the compiler observes indirect dependencies, the compiler will not reorder these two operations. Most processors will also comply with indirect dependencies, and most processors will not reorder these two operations. However, there are a few processors that allow reordering of operations that have indirect dependencies (such as alpha processors). This rule is specifically designed for this type of processor.

The reader() method contains three operations:

  1. Read the reference variable obj for the first time;
  2. The first read reference variable obj points to the normal field j of the object.
  3. The first read reference variable obj points to the final field i of the object.

Now we assume that no reordering occurs in the writer thread A, and the program is executed on a processor that does not comply with the indirect dependency. The following is a possible execution sequence:

In the above figure, the operation of reading the common field of the object is reordered by the processor before the reference of the reading object. When reading a normal field, the field has not been written by the writing thread A, which is an erroneous read operation. The reordering rules for reading the final field will "limit" the operation of reading the final field of the object after the reading object reference. At this time, the final field has been initialized by the A thread. This is a correct read operation.

The reordering rules for reading the final field can ensure that before reading the final field of an object, the reference of the object containing the final field must be read first. In this sample program, if the reference is not null, then the final field of the referenced object must have been initialized by the A thread.

 

If the final field is a reference type

The final field we saw above is a basic data type. Now let us see what effect will it have if the final field is a reference type?

Please see the following sample code:

public class FinalReferenceExample {

    final int[] intArray;                     //final 是引用类型 
    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
        }
    }    

}

Here the final field is a reference type, which refers to an array object of type int. For reference types, the reordering rules for writing final fields impose the following constraints on the compiler and processor:

  1. Writing to the member field of a final referenced object in the constructor, and subsequently assigning the reference of the constructed object to a reference variable outside the constructor, the two operations cannot be reordered.

For the example program above, we assume that thread A executes the writerOne() method first, then thread B executes the writerTwo() method after execution, and thread C executes the reader() method after execution. The following is a possible thread execution timing:

In the above figure, 1 is the write to the final field, 2 is the write to the member field of the object referenced by this final field, and 3 is to assign the reference of the constructed object to a reference variable. In addition to the aforementioned 1 cannot be reordered with 3, 2 and 3 cannot be reordered.

JMM can ensure that the reader thread C can at least see the write thread A to the member field of the final reference object in the constructor. That is, C can at least see the value 1 of the array subscript 0. The write thread B to the array elements, the reader thread C may or may not see it. JMM does not guarantee that thread B's writing is visible to reader thread C, because there is a data competition between writer thread B and reader thread C, and the execution result at this time is unpredictable.

If you want to ensure that the reader thread C sees the writing of the array elements by the writer thread B, a synchronization primitive (lock or volatile) needs to be used between the writer thread B and the reader thread C to ensure memory visibility.

 

Why can't final references "escape" from within the constructor

As we mentioned earlier, the reordering rules for writing final fields can ensure that the final field of the object pointed to by the reference variable has been correctly initialized in the constructor before the reference variable is visible to any thread. In fact, in order to get this effect, a guarantee is needed: inside the constructor, the reference of the constructed object cannot be made visible to other threads, that is, the object reference cannot "escape" in the constructor. To illustrate the problem, let us look at the following sample code:

public class FinalReferenceEscapeExample {

    final int i;
    static FinalReferenceEscapeExample obj;

    public FinalReferenceEscapeExample () {
        i = 1;                              //1 写 final 域 
        obj = this;                          //2 this 引用在此“逸出”
    }

    public static void writer() {
        new FinalReferenceEscapeExample ();
    }

    public static void reader {
        if (obj != null) {                     //3
            int temp = obj.i;                 //4
        }
    }

}

Suppose one thread A executes the writer() method, and another thread B executes the reader() method. Operation 2 here makes the object visible to thread B before it has completed its construction. Even if operation 2 here is the last step of the constructor, and even if operation 2 comes after operation 1 in the program, the thread executing the read() method may still not be able to see the initialized value of the final field, because operation 1 here And operation 2 may be reordered. The actual execution sequence may be as shown in the figure below:

From the above figure, we can see that the reference of the constructed object cannot be visible to other threads before the constructor returns, because the final field may not be initialized yet. After the constructor returns, any thread will be guaranteed to see the value after the final field is correctly initialized.

 

Realization of final semantics in the processor

Now we take the x86 processor as an example to illustrate the specific implementation of final semantics in the processor.

As we mentioned above, the reordering rules for writing final fields will require the translator to insert a StoreStore barrier after writing the final field and before the constructor return. The reordering rules for reading the final field require the compiler to insert a LoadLoad barrier before the operation of reading the final field.

Since the x86 processor does not reorder write-write operations, in the x86 processor, the StoreStore barrier screen required to write the final field will be omitted. Similarly, because the x86 processor does not reorder operations that have indirect dependencies, in the x86 processor, the LoadLoad barrier required to read the final field will also be omitted. That is to say, in the x86 processor, the read/write of the final domain will not insert any memory barrier!

 

Why JSR-133 should enhance the semantics of final

In the old Java memory model, one of the most serious flaws is that the thread may see the value of the final field change. For example, a thread currently sees the value of a plastic final field as 0 (the default value before initialization). After a period of time, when the thread reads the value of the final field, it finds that the value has changed to 1 (by some Value after initialization of each thread). The most common example is that in the old Java memory model, the value of String may change (there is a specific example in Reference 2, and interested readers can refer to it by themselves, so I won't repeat it here).

In order to fix this vulnerability, the JSR-133 expert group enhanced the semantics of final. By adding write and read reordering rules to the final domain, it can provide java programmers with initialization safety guarantee: as long as the object is correctly constructed (the reference of the constructed object is not "escaped" in the constructor), then there is no need to use synchronization (Referring to the use of lock and volatile), it can be guaranteed that any thread can see the value of this final field after it is initialized in the constructor.

Previous articleIn-     depth understanding of Java memory model (5)-lock
Next article In-    depth understanding of the Java memory model (7)-summary

Thanks to the author for his contribution to this article

Cheng Xiaoming, Java software engineer, nationally certified system analyst and information project manager. Focus on concurrent programming and work at Fujitsu Nanda. Personal email: [email protected].
---------------------
Author: World coding
Source: CSDN
Original: https://blog.csdn.net/dgxin_605/article/details/86183971
Copyright: This article is the original article of the blogger, please attach a link to the blog post if you reprint it!
 

 

Guess you like

Origin blog.csdn.net/dgxin_605/article/details/86183971