Compared to the lock and volatile described earlier, the read and write to the final domain is more like normal variable access. For the final domain, the compiler and processor are subject to two reordering rules:
- Writing to a final field within the constructor, and then assigning a reference to the constructed object to a reference variable, cannot be reordered between the two operations.
- The first time you read a reference to an object that contains a final field, and the final field is then first read, the two operations cannot be reordered.
Below, we illustrate these two rules separately with some exemplary code:
Public classFinalexample {intI//Common Variables Final intJ//Final Variable StaticFinalexample obj; Public voidFinalexample () {//constructor Functioni = 1;//write normal fieldsj = 2;//Write final field } Public Static voidWriter () {//Write thread A executesobj =Newfinalexample (); } Public Static voidReader () {//Read thread B executionFinalexample object = obj;//Read Object reference intA = OBJECT.I;//Read normal domain intb = OBJECT.J;//Read final field }}
This assumes that a 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 final field reordering rules
A reorder rule that writes a final field prevents the final field from being sorted out of the constructor. The implementation of this rule consists of the following 2 aspects:
- JMM prevents the compiler from ordering the final field's write-back to the constructor.
- The compiler inserts a storestore barrier before the constructor return after the final field is written. This barrier prohibits the processor from ordering the final domain's write-back to the constructor.
Now let's analyze the writer () method. The writer () method contains only one line of code: Finalexample = new Finalexample (). This line of code contains two steps:
- Constructs an object of type finalexample;
- Assigns a reference to this object to the reference variable, obj.
Assuming there is no reordering between the thread B-read object reference and the member domain of the Read object (which immediately explains why this assumption is required), it is a possible execution sequence:
In, the operations that write the normal domain are sorted out of the constructor by the compiler, and read thread B incorrectly reads the value before the normal variable i is initialized. In the final field, the reordering rule of the final field is "qualified" within the constructor, and read thread B correctly reads the value after the final variable is initialized.
The collation of the final field ensures that the final domain of the object has been properly initialized before the object reference is visible to any thread, and the normal domain does not have this guarantee. For example, when read thread B "sees" an object referencing obj, it is likely that the Obj object has not yet been constructed (the write to normal domain i is reordered to the constructor, at which point the initial value of 2 has not been written to the normal domain i).
Read the Reorder rules for final fields
The reordering rules for the Read final field are as follows:
- In one thread, the first read object reference and the first read of the object contains the final domain, JMM prohibit the processor reordering these two operations (note that this rule is only for the processor). The compiler inserts a loadload barrier before the read final domain operation.
An indirect dependency exists between the first-read object reference and the first-read final domain that the object contains. Because the compiler adheres to indirect dependencies, the compiler does not reorder the two operations. Most processors also adhere to indirect dependencies, and most processors do not reorder the two operations. But there are a handful of processors that allow reordering of operations that have indirect dependencies (such as an Alpha processor), which is specifically intended for this processor.
The reader () method consists of three actions:
- First read reference variable obj;
- The first read reference variable, obj, points to the normal domain J of the object.
- The first read reference variable, obj, points to the final field I of the object.
Now let's assume that write thread A does not have any reordering, and the program executes on a processor that does not obey the indirect dependencies, here is a possible execution timing:
In, the operation of the normal domain of the Read object is reordered by the processor before the Read object reference. When reading a normal domain, the domain has not been written by write thread A, which is an incorrect read operation. The re-collation of the final field will "qualify" the operation of the final field of the Read object after the Read object reference, and the final domain has been initialized by a thread, which is the correct read operation.
Reading the collation of the final field ensures that a reference to the object containing the final field must be read before the final field of an object is read. In this example program, if the reference is not NULL, then the final domain of the referencing object must have been initialized by a thread.
If the final field is a reference type
The final field we see above is the underlying data type, so let's see what happens if the final field is a reference type.
Take a look at the following sample code:
Public classFinalreferenceexample {Final int[] intarray;//Final is a reference typeStaticfinalreferenceexample obj; PublicFinalreferenceexample () {//constructor FunctionIntarray =New int[1];//1Intarray[0] = 1;//2} Public Static voidWriterone () {//Write thread A executesobj =NewFinalreferenceexample ();//3} Public Static voidWritertwo () {//Write thread B executionObj.intarray[0] = 2;//4} Public Static voidReader () {//read thread C execution if(obj! =NULL) {//5 intTemp1 = obj.intarray[0];//6 }}}
Here the final field is a reference type that references an array object of type int. For reference types, the reorder rules for the write final domain add the following constraints to the compiler and processor:
- Writes to a member field of a final referenced object within a constructor, and then assigns a reference to a reference variable outside of the constructor to the constructed object, and cannot be reordered between the two operations.
For the above example program, we assume that thread A executes the Writerone () method first, and thread B executes the Writertwo () method after execution, after the thread C executes the reader () method. The following is a possible sequence of thread execution:
In, 1 is the write to the final field, 2 is the write to the member domain of the object referenced by this final field, and 3 is the assignment of a reference to a reference variable to the referenced object. In addition to the previously mentioned 1 cannot be and 3 reorder, 2 and 3 can not be re-ordered.
JMM ensures that read thread C can at least see write thread a writes to the member domain of the final reference object in the constructor. That is, C can see at least the value of the array subscript 0 is 1. While writing thread B writes to an array element, read thread C may or may not be seen. JMM does not guarantee that the write to thread B is visible to read thread C because there is data contention between write thread B and read thread C, where the execution result is unpredictable.
If you want to make sure that read thread C sees write thread B write to the array element, write thread B and read thread C need to use synchronization primitives (lock or volatile) to ensure memory visibility.
Why a final reference cannot "escape" from within a constructor
As we mentioned earlier, the collation of the final field ensures that the final domain of the object to which the reference variable points is already properly initialized in the constructor before the reference variable is visible to any thread. In fact, to get this effect, you also need a guarantee: Inside the constructor, you cannot let the reference of the constructed object be visible to other threads, that is, the object reference cannot be "escaped" in the constructor. To illustrate the problem, let's look at the following sample code:
Public classFinalreferenceescapeexample {Final inti;Staticfinalreferenceescapeexample obj; Publicfinalreferenceescapeexample () {i= 1;//1 Write final fieldobj = This;//2 This reference in this "escape"} Public Static voidwriter () {Newfinalreferenceescapeexample ();} Public Static voidReader {if(obj! =NULL) {//3 inttemp = OBJ.I;//4 }}}
Suppose one thread a executes the writer () method and another thread B executes the reader () method. Operation 2 here makes it visible to thread B before the object is constructed. Even if Operation 2 here is the last step of the constructor, and even after Operation 2 in the program, the thread that executes the read () method may still not see the value after the final domain is initialized, because the operation 1 and the Operation 2 may be reordered. The actual execution timing may look like the following:
As we can see, the reference to the constructed object cannot be visible to other threads until the constructor returns, because the final domain may not have been initialized at this time. After the constructor returns, any thread will be guaranteed to see the values after the final domain is initialized correctly.
The implementation of final semantics in the processor
Now we take the x86 processor as an example to illustrate the concrete implementation of final semantics in the processor.
As we mentioned above, the reordering rules for final fields require the translator to insert a storestore screen before the constructor return after the final field is written. The re-collation of the Read final field requires the compiler to insert a loadload barrier before the operation of the final domain.
Because the x86 processor does not reorder write-write operations, the Storestore screen that is required to write the final domain in the x86 processor is omitted. Similarly, because the x86 processor does not reorder operations that have indirect dependencies, the loadload barriers required to read the final domain are omitted from the x86 processor. That is, in the x86 processor, the final domain read/write does not insert any memory barrier!
JSR-133 why to enhance final semantics
One of the most serious flaws in the old Java memory model is that the thread might see the final domain value change. For example, a thread currently sees a value of 0 for the Shaping final field (before the default value has not been initialized), and after a period of time the thread then reads the value of the final field, but finds that the value becomes 1 (the value after the initialization of a thread). The most common example of this is that in the old Java memory model, the value of String may change (see reference 2 for a specific example, the reader of interest can refer to it, this is not discussed here).
To remedy this vulnerability, the JSR-133 Expert Group Enhanced final semantics. By adding write and read reordering rules for final fields, you can provide initialization security assurances for Java programmers: As long as the object is properly constructed (references to constructed objects do not "escape" in the constructor), then synchronization is not required (for lock and volatile use). You can ensure that any thread can see the value of this final field after it has been initialized in the constructor.
Java Memory Model-final