Location>code7788 >text

Understand the role of the final keyword in the field of concurrent programming in Java?

Popularity:369 ℃/2024-10-08 18:29:29

In the field of concurrent programming in Java, thefinalKeywords play a crucial role. While many students are familiar withfinalis used to modify the basic usage of variables, methods, and classes, but its application and principles in concurrent environments are often overlooked.finalA keyword is more than a simple modifier; it ensures visibility and invariance of object state in multithreaded programming, which is essential for building thread-safe applications. In this article, we'll take a deeper look at thefinalThe role of keywords, revealing their importance and implementation principles in the field of concurrent programming in Java.

reordering rules for final fields

Java memory model in order to allow the processor and compiler layer to play their maximum advantage, the bottom of the constraints on the bottom of the very little, that is to say, for the bottom of the Java memory model is a weak memory data model. At the same time, the processor and compiler reorder sequences of instructions for performance optimization by the compiler and processor.

And final guarantees that when an object is created, the use of the final keyword prevents another thread from accessing the object in a "partially created" state, which might otherwise happen. This is because final has the following semantics when used as a property of an object:

When the constructor ends, the values of the final type are guaranteed to be visible when other threads access the object

For final domains, compilers and processors are subject to two reordering rules.

  • There can be 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.

  • There can be no reordering between the initial reading of a reference to an object containing a final field, and the subsequent initial reading of that final field.

Why it's necessary.

The use of final is a form of so-called safe publication, where publication means that it is created in one thread and another thread can refer to the newly created object at some point later. The following scenario exists: when the JVM calls the object's constructor, it must assign values to the members and store a pointer to the object; but like any other normal data write, this can be out of order.

The final prevents this from happening: if a member is final, the JVM specification makes the following explicit guarantee: once an object reference is visible to other threads, its final member must also be correctly assigned.

The final field is a basic type

Let's start with a sample piece of code:

public class FinalDemo {
    private int a; //ordinary domain (computing)
    private final int b; //finaldomain (taxonomy)
    private static FinalDemo finalDemo;

    public FinalDemo() {
        a = 1; // 1. writeordinary domain (computing)
        b = 2; // 2. writefinaldomain (taxonomy)
    }

    public static void writer() {//threadingAfulfillment
        finalDemo = new FinalDemo();
    }

    public static void reader() {//threadingBfulfillment
        FinalDemo demo = finalDemo; // 3.Reading Object References
        int a = ; //4.phrase marked by pauseordinary domain (computing)
        int b = ; //5.phrase marked by pausefinaldomain (taxonomy)
    }
}

Suppose thread A is executing the writer() method and thread B is executing the reader() method.

Write final field reordering rules

Reordering rule for writing final fields The implementation of the rule that prohibits reordering writes to final fields outside of the constructor consists of two main aspects:

  • The JMM prohibits the compiler from reordering writes to the final field outside of the constructor;

  • The compiler inserts a storestore barrier after the final field write and before the constructor return. This barrier prohibits the processor from reordering final field writes outside of the constructor.

Analyzing the writer method again, it's only one line of code, but it actually does two things:

  • Constructs a FinalDemo object;

  • Assign this object to the member variable finalDemo.

One possible execution timing diagram that exists is as follows:

Since there is no data dependency between a,b, the ordinary domain (ordinary variable) a may be reordered outside the constructor, and thread B may read the value before the initialization of the ordinary variable a (zero value), which may result in an error. On the other hand, the final domain variable b, according to the reordering rule, will prohibit the reordering of the final modified variable b outside of the constructor, so that b can be correctly assigned a value, and thread B will be able to read the value of the final variable after initialization.

Thus, writing reordering rules for final domains ensures that: before an object reference is visible to any thread, the final domain of the object has been properly initialized, whereas ordinary domains do not have this guarantee. For example, in the above example, it is possible that thread B is an object finalDemo that has not been properly initialized.

Read the final field reordering rules

The read final field reordering rule is that the initial read of an object reference and the initial read of the final field contained in that object in a thread will disable reordering for both operations by the JMM. (Note that this rule is only for the processor.) The processor inserts a LoadLoad barrier in front of the read final domain operation. In fact, there is an indirect dependency between reading a reference to an object and reading the final domain of that object, and generally processors do not reorder these two operations. However, there are some processors that do reorder, so this rule against reordering is specific to those processors.

The read() method contains three main operations:

  • Initial read reference variable finalDemo.

  • Initial read references the common domain a of the variable finalDemo.

  • Initial read reference to the final domain b of the variable finalDemo.

Assuming that the Thread A write process is not reordered, the possible execution timings for Thread A and Thread B with one of these are shown below:

Read the object's ordinary field is reordered to read the object reference before the thread B will not have read the object reference is reading the object's ordinary field variables, which is obviously the wrong operation. The read operation of the final domain "qualifies" in the reading of the final domain variable before the object has been read before the reference, so you can avoid this situation.

The reordering rule for reading final fields ensures that: before reading the final field of an object, a reference to the object that contains this final field will always be read first.

The final field is a reference type

Write operations on member fields of final-modified objects

For reference datatypes, final field writes add the constraint that compiler and processor reordering cannot reorder writes to the member fields of a final-modified object within the constructor and subsequent assignments of references to the constructed object to a reference variable outside the constructor. Note that the word "add" means that the previous reordering rules for final basic datatypes are still in use here. This sentence is more difficult to understand, the following examples to see.

public class FinalReferenceDemo {
    final int[] arrays;
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo() {
        arrays = new int[1];  //1
        arrays[0] = 1;        //2
    }

    public void writerOne() {
        finalReferenceDemo = new FinalReferenceDemo(); //3
    }

    public void writerTwo() {
        arrays[0] = 2;  //4
    }

    public void reader() {
        if (finalReferenceDemo != null) {  //5
            int temp = [0];  //6
        }
    }
}

For the example program above, Thread A executes the wirterOne method, after which Thread B executes the writerTwo method, and then Thread C executes the reader method. The following figure discusses one scenario that occurs with this execution timing

Since writes to final domains are prohibited from being reordered outside of the constructor method, 1 and 3 cannot be reordered. Since member field writes to a referenced object with a final field cannot be reordered with the subsequent assignment of this constructed object to a referenced variable, 2 and 3 cannot be reordered.

Member field read operations on final-modified objects

The JMM ensures that thread C can at least see the write of thread A to the member field of the object referenced by final, i.e., it can see that arrays[0] = 1, whereas the write of thread B to the array elements may or may not be visible.The JMM does not guarantee that thread B's writes will be visible to thread C. There is a data contention between thread B and thread C, at which point the result is unpredictable. If visible, locks or volatile can be used.

Summary on final reordering

Categorized by the datatype of the final modifier:

  • Basic data types.

    • Final field writes: disable final field writes and constructor reordering, i.e., disable final field writes from being reordered outside of the constructor, thus ensuring that all of the object's final fields have been initialized by the time the object is visible to all threads.

    • FINAL FIELD READ: Disable the reordering of the initial read of a reference to an object with the read of the final field contained in that object.

  • References data types:

    • Add additional constraints: prohibit writing to the member fields of a final-modified object in the constructor and subsequently assigning a reference to the constructed object to a reference variable Reordering

How final is implemented

Writing the final domain would require the compiler to insert a StoreStore barrier after the final domain is written and before the constructor returns. The reordering rules for reading final domains would require the compiler to insert a LoadLoad barrier before the operation that reads the final domain.

It is interesting to note that if we take X86 processing as an example, X86 does not reorder write-write, so the StoreStore barrier can be omitted. Since there is no reordering of operations with indirect dependencies, the LoadLoad barrier required to read the final field is also omitted in X86 processors. That is, in the case of X86, the memory barriers for reading/writing final fields are omitted! Whether it is inserted or not depends on the processor

Reordering problems due to "overflow"

The above reordering rule for the final domain ensures that the final domain of an object has been initialized in the constructor when the reference is used. But there is a prerequisite: the constructor must not make the constructed object visible to other threads, i.e., the object reference must not "overflow" in the constructor. Take the following example:

As shown below, the overflow means that the constructor FinalReferenceEscapeDemo() has not yet finished executing, and as a result of the execution of 2, the referenceDemo is not null anymore

public class FinalReferenceEscapeDemo {
    private final int a;
    private FinalReferenceEscapeDemo referenceDemo;

    public FinalReferenceEscapeDemo() {
        a = 1;  //1
        referenceDemo = this; //2
    }

    public void writer() {
        new FinalReferenceEscapeDemo();
    }

    public void reader() {
        if (referenceDemo != null) {  //3
            int temp = ; //4
        }
    }
}

The possible execution timings are shown in Fig:

Suppose a thread A executes the writer method and another thread executes the reader method. Because there is no data dependency between operations 1 and 2 in the constructor, 1 and 2 can be reordered, and 2 is executed first, and the reference object referenceDemo is an object that is not fully initialized at this time, and when thread B goes to read the object it will be an error. Although still meet the final domain write reordering rules: in the reference object visible to all threads, its final domain has been fully initialized successfully. However, the reference object "this" escapes, and the code is still thread-safe.

Constraints and limitations of using final

When a final member is declared, its value must be set before the constructor exits.

public class MyClass {
  private final int myField = 1;
  public MyClass() {
    ...
  }
}

or

public class MyClass {
  private final int myField;
  public MyClass() {
    ...
    myField = 1;
    ...
  }
}

Declaring a member of an object as final only makes the reference immutable, not the object it refers to.

The following method still modifies the list.

private final List myList = new ArrayList();
("Hello");

A declaration of final ensures that the following operations are not legal

myList = new ArrayList();
myList = someOtherList;

If an object will be accessed in multiple threads and you have not declared its members as final, you must provide some other way to ensure thread safety.

"Other ways" may include declaring a member volatile and controlling all access to that member using synchronized or explicit Lock.

final byte


byte b2=3.
byte b3=b1+b2;//when the program executes to this line will be an error, because b1, b2 can be automatically converted to int type variable, the operation of the java virtual machine on it to convert, the result is to assign an int to byte----- error!

If you add final to b1 b2, you don't get an error.


final byte b2=3;
byte b3=b1+b2;//there will be no error, I'm sure you know why after reading the explanation above.

About the Author.

From the first-line programmer Seven's exploration and practice, continuous learning iteration in the~

This article is included in my personal blog:https://

Public number: seven97, welcome to follow~