JUC Lecture 7: Detailed explanation of keyword final

JUC Lecture 7: Detailed explanation of keyword final

The final keyword seems simple, but it can be said that very few people really understand it deeply. This article is the seventh lecture of JUC: Detailed explanation of the keyword final. It simplifies the conventional usage and proposes some usage and in-depth thinking.

1. Use the interview questions from major BAT companies to understand the final

Please continue with these questions, which will greatly help you understand final better.

  • Are all final-modified fields compile-time constants ? No
  • How to understand that the method modified by private is implicitly final?
  • Let’s talk about how to expand the final type of class ? For example, String is a final type. We want to write a MyString that reuses all the methods in String and adds a new toMyString() method. How should we do it? Combination
  • Can final methods be overloaded? Yes
  • Can the final method of the parent class be overridden by the subclass? No
  • Tell us about the final field reordering rules?
  • Tell us about the principle of final?
  • What are the restrictions and limitations of using final?

2. Basic use of final

2.1. Modification class

When the overall definition of a class is final, it indicates that you cannot intend to inherit the class, and others are not allowed to do so . That is, this class cannot have subclasses.

Note: All methods in a final class are implicitly final because they cannot be overridden , so it makes no sense to add the final keyword to any method in a final class.

By the way, let’s talk about how to expand the final type of class? For example, String is a final type. We want to write a MyString that reuses all the methods in String and adds a new toMyString() method. How should we do this?

The two most important relationships in design patterns are inheritance/implementation and the other is composition . Therefore, when you encounter a class that cannot be inherited (final modified class) , you should consider using combination. The following code roughly means writing a combination implementation:

class MyString{
    
    
    private String innerString;

    // ...init & other methods

    // 支持老的方法
    public int length(){
    
    
        return innerString.length(); // 通过innerString调用老的方法
    }

    // 添加新方法
    public String toMyString(){
    
    
        //...
    }
}

2.2. Modification method

I won’t talk about the regular use, but here it is:

  • private methods are implicitly final
  • final methods can be overloaded
1、private final

All private methods in a class are implicitly designated as final. Since the private method cannot be accessed, it cannot be overridden. You can add the final keyword to the private method, but there is no benefit in doing so. Take a look at the following example:

public class Base {
    
    
    private void test() {
    
    
    }
}

public class Son extends Base {
    
    
    public void test() {
    
    
    }
    public static void main(String[] args) {
    
    
        Son son = new Son();
        Base father = son;
        //father.test();
    }
}

Both Base and Son have the method test(), but this is not an override, because the method modified by private is implicitly final, that is, it cannot be inherited, so it is not to mention an override. In the test() in Son ) method is just a new member of Son . Son performs upward transformation to obtain father, but father.test() is not executable because the test method in Base is private and cannot be accessed.

2. Final methods can be overloaded

We know that the final method of the parent class cannot be overridden by the subclass, so can the final method be overloaded? The answer is yes, the following code is correct.

public class FinalExampleParent {
    
    
    public final void test() {
    
    
    }

    public final void test(String str) {
    
    
    }
}

2.3. Modification parameters

Java allows declaratively specifying parameters as final in the parameter list , which means that you cannot change the object pointed to by the parameter reference in the method. This feature is mainly used to pass data to anonymous inner classes.

2.4. Modify variables

The conventional usage is relatively simple, and is further explained through the following three questions.

1. Are all final-modified fields compile-time constants?

Now let’s look at compile-time constants and non-compile-time constants, such as:

public class Test {
    
    
    // 编译期常量
    final int i = 1;
    final static int J = 1;
    final int[] a = {
    
    1,2,3,4};
    // 非编译期常量
    Random r = new Random();
    final int k = r.nextInt();

    public static void main(String[] args) {
    
    

    }
}

The value of k is determined by the random number object, so not all final-modified fields are compile-time constants, but the value of k cannot be changed after being initialized .

2、static final

A field that is both static and final only occupies a storage space that cannot be changed. It must be assigned a value when it is defined, otherwise the compiler will reject it.

import java.util.Random;
public class Test {
    
    
    static Random r = new Random();
    final int k = r.nextInt(10);
    static final int k2 = r.nextInt(10); 
    public static void main(String[] args) {
    
    
        Test t1 = new Test();
        System.out.println("k="+t1.k+" k2="+t1.k2);
        Test t2 = new Test();
        System.out.println("k="+t2.k+" k2="+t2.k2);
    }
}

The output of the above code is as follows:

k=2 k2=7
k=8 k2=7

We can find that the value of k is different for different objects, but the value of k2 is the same. Why is this? Because the field modified by the static keyword does not belong to an object, but to this class. It can also be simply understood that the fields modified by static final only occupy a space in the memory and will not be changed once initialized .

3、blank final

Java allows the generation of blank finals, which means fields that are declared final but do not give a fixed value, but must be assigned a value before the field can be used. This gives us two options:

  • Assign value at the definition (this is not called blank final)
  • Assigning a value in the constructor ensures that the value is assigned before being used .

This enhances the flexibility of final.

Look at the code below:

public class Test {
    
    
    final int i1 = 1;
    final int i2;//空白final
    public Test() {
    
    
        i2 = 1;
    }
    public Test(int x) {
    
    
        this.i2 = x;
    }
}

You can see that the assignment of i2 is more flexible. However, please note that if a field is modified by static and final, it can only be assigned at the time of declaration or in a static code block after declaration, because the field does not belong to the object and belongs to this class .

2.5. Use of final in projects

Scenario 1: In the template mode, the default template is modified with final to prevent subclasses from reimplementing;

Scenario 2: Use final to modify constants and classes to prevent modifications

3. Final domain reordering rules

The use of final we talked about above should belong to the basic level of Java. After understanding these, can we really master final? Have you ever considered the use of final in multi-thread concurrency? In the java memory model, we know the java memory model In order to allow the processor and compiler to take full advantage of their bottom layer, there are very few constraints on the bottom layer. In other words, for the bottom layer, the Java memory model is a weak memory data model . At the same time, the processor and compiler will reorder the instruction sequence for performance optimization. So, in a multi-threaded situation, what kind of reordering will be performed by final? Will it cause thread safety issues? Next, let's take a look at the reordering of final.

3.1. The final field is a basic type

Let’s look at an example code first:

public class FinalDemo {
    
    
    private int a;  //普通域
    private final int b; //final域
    private static FinalDemo finalDemo;

    public FinalDemo() {
    
    
        a = 1; // 1. 写普通域
        b = 2; // 2. 写final域
    }

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

    public static void reader() {
    
    
        FinalDemo demo = finalDemo; // 3.读对象引用
        int a = demo.a;    //4.读普通域
        int b = demo.b;    //5.读final域
    }
}

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

1. Write final domain reordering rules

The reordering rule for writing final fields prohibits the reordering of writing to final fields outside the constructor. The implementation of this rule mainly includes two aspects:

  • JMM prohibits the compiler from reordering writes to final fields outside of constructors ;
  • The compiler will insert a store barrier after the final field is written and before the constructor returns . This barrier prevents the processor from reordering writes to final fields outside the constructor.

Let's analyze the writer method again. Although there is only one line of code, it actually does two things:

  • Constructed a FinalDemo object;
  • Assign this object to the member variable finalDemo.

Let’s draw a possible execution timing diagram, as follows:

  • img

Since there is no data dependency between a and b, the ordinary field (ordinary variable) a may be reordered outside the constructor, and thread B may read the value (zero value) of the ordinary variable a before it is initialized. Errors may occur. As for the final domain variable b, according to the reordering rules, the final-modified variable b will be prohibited from being reordered outside the constructor , so that b can be correctly assigned and thread B can read the initialized value of the final variable.

Therefore, writing reordering rules for final fields can ensure that the object's final field has been correctly initialized before the object reference is visible to any thread, while ordinary fields do not have this guarantee . For example, in the above example, thread B may be an incorrectly initialized object finalDemo.

2. Read the final field reordering rules

The reordering rules for reading final fields are: in a thread, JMM will prohibit the reordering of these two operations when reading an object reference for the first time and reading the final field contained in the object for the first time . (Note that this rule is only for the processor), the processor will insert a LoadLoad barrier in front of the read final field operation. In fact, there is an indirect dependence between reading the reference of the object and reading the final field of the object. Generally, the processor will not reorder these two operations. However, some processors will reorder, so this prohibition of reordering rule is set for these processors.

The read() method mainly includes three operations:

  • Read the reference variable finalDemo for the first time;
  • First read the ordinary field a that refers to the variable finalDemo;
  • Read the final field b of the reference variable finalDemo for the first time;

Assuming that the writing process of thread A is not reordered, then the possible execution timing of thread A and thread B is as follows:

  • img

If the common field of the read object is reordered to the front of the read object reference, thread B will read the common field variable of the object before reading the object reference. This is obviously a wrong operation. The read operation of the final field "limits" the reference to the object that has been read before reading the final field variable , thus avoiding this situation.

The reordering rules for reading final fields ensure that before reading the final field of an object, the reference to the object containing the final field must be read first.

3.2. The final field is a reference type

We already know what the reordering rules are when the final field is a basic data type? What if it is a reference data type? Let's continue to discuss it.

1. Write operations to the member fields of final-modified objects

For reference data types, final field writing adds the following constraints to compiler and processor reordering: writing to a member field of a final-modified object within the constructor, and subsequently writing the constructed field outside the constructor. The reference to the object is assigned to a reference variable, and these two operations cannot be reordered. Note that the word "increase" here means that the previous reordering rules for final basic data types are still used here. This sentence is quite difficult to pronounce, let’s look at it with examples below.

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 = finalReferenceDemo.arrays[0];  //6
        }
    }
}

For the above example program, thread A executes the writerOne method. After execution, thread B executes the writerTwo method, and then thread C executes the reader method. The figure below discusses a situation where this execution timing occurs.

  • img

Since writes to final fields prohibit reordering outside the constructor, 1 and 3 cannot be reordered. Since the member field writing of a final field reference object cannot be reordered with the subsequent assignment of the constructed object to the reference variable, 2 and 3 cannot be reordered.

2. Read the member fields of the final modified object

JMM can ensure that thread C can at least see writing thread A's writing to the member field of the object referenced by final, that is, it can see arrays[0] = 1, while writing thread B's writing to array elements may see Less than. JMM does not guarantee that thread B's writes are visible to thread C. There is data competition between thread B and thread C. The result at this time is unpredictable. If visible, use locks or volatile.

3.3. Summary on final reordering

Classification according to final modified data type:

  • Basic data types:
    • final域写: Prohibit final field writing and reordering of constructor methods, that is, prohibit final field writing and reordering outside of the constructor method, thereby ensuring that when the object is visible to all threads, all final fields of the object have been initialized.
    • final域读: Prohibits reordering of references to an object read for the first time and final fields contained in the object.
  • Reference data type:
    • 额外增加约束: Prohibits reordering when the constructor writes the member field of a final-modified object and subsequently assigns the reference of the constructed object to a reference variable.

4. Deeper understanding in final

4.1. Implementation principle of final

As we mentioned above, writing a final field requires the compiler to insert a StoreStore barrier after the final field is written and before the constructor returns. The reordering rules for reading final fields require the compiler to insert a LoadLoad barrier before reading the final field.

Interestingly, if we take X86 processing as an example, X86 does not reorder write-to-write, so the StoreStore barrier can be omitted. Since operations with indirect dependencies will not be reordered, the LoadLoad barrier required to read the final field will also be omitted in the X86 processor. In other words, taking X86 as an example, the memory barrier for reading/writing the final field will be omitted! Whether to plug it in or not depends on what kind of processor it is.

4.2. Why can’t final references “overflow” from the constructor?

There is another interesting question here: writing the reordering rules for the final field above can ensure that when we use an object reference, the object's final field has been initialized in the constructor. But there is actually a prerequisite here, that is: in the constructor, the constructed object cannot be made visible to other threads, which means that the object reference cannot "overflow" in the constructor. Take the following example:

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 = referenceDemo.a; //4
        }
    }
}

Possible execution timing is shown in the figure:

  • img

Assume that one 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. At this time, the reference object referenceDemo is an object that has not been fully initialized, and when thread B reads the object, it Something will go wrong. Although the final field write reordering rules are still met: when the reference object is visible to all threads, its final field has been fully initialized successfully. However, the reference object "this" escapes, and the code still has thread safety issues.

4.3. Restrictions and limitations of using final

When declaring a final member, 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 pointing to an object final only makes the reference, not the pointed object, immutable.

The following method can still modify the list.

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

Declaring it final ensures that the following operations are illegal

myList = new ArrayList();
myList = someOtherList;

If an object will be accessed from multiple threads and you do not declare its members final, you must provide another way to ensure thread safety.

"Other ways" could include declaring the member volatile, using synchronized or explicit Lock to control all access to the member.

Action: Read an interview question and think about an interesting phenomenon?

byte b1=1;
byte b2=3;
byte b3=b1+b2; //当程序执行到这一行的时候会出错,因为b1、b2可以自动转换成int类型的变量,运算时java虚拟机对它进行了转换,结果导致把一个int赋值给byte从而出错

If you add final to b1 b2, there will be no error

final byte b1=1;
final byte b2=3;
byte b3=b1+b2; //不会出错,不会被强制类型转换

Adding it finalis equivalent to forcing this type not to be converted and added directly. In javaorder to unify and speed up the instructions, the virtual machine converts the addition of these variables first and shortthen performs the operation. But adding reality will make this process impossible.byteintfinal

参考这篇文章:Is addition of byte converts to int because of java language rules or because of jvm?

5. Reference articles

  • "The Art of Concurrent Programming in Java"
  • "Crazy Java Lecture Notes"

Guess you like

Origin blog.csdn.net/qq_28959087/article/details/133169218