Java基础5-一文了解final关键字的特性、使用方法,以及实现原理(二)

简介: Java基础5-一文了解final关键字的特性、使用方法,以及实现原理(二)

Java基础5-一文了解final关键字的特性、使用方法,以及实现原理(一):https://developer.aliyun.com/article/1535637

如何保证数组内部不被修改

那可能有的同学就会问了,加上final关键字不能保证数组不会被外部修改,那有什么方法能够保证呢?答案就是降低访问级别,把数组设为private。这样的话,就解决了数组在外部被修改的不安全性,但也产生了另一个问题,那就是这个数组要被外部使用的。 
复制代码

解决这个问题见代码:

import java.util.AbstractList;   
import java.util.List;   

public class Final   
{   
    public static void main(String[] args)   
    {   
        for (String color : Color.color)   
            System.out.print(color + " ");   
        Color.color.set(3, "white");   
    }   
}   
  
class Color   
{   
    private static String[] _color = { "red", "blue", "yellow", "black" };   
    public static List<String> color = new AbstractList<String>()   
    {   
        @Override  
        public String get(int index)   
        {   
            return _color[index];   
        }   
        @Override  
        public String set(int index, String value)   
        {   
            throw new RuntimeException("为了代码安全,不能修改数组");   
        }   
        @Override  
        public int size()   
        {   
            return _color.length;   
        }   
    };  
复制代码
}
复制代码

final方法的三条规则

规则1:final修饰的方法不可以被重写。

规则2:final修饰的方法仅仅是不能重写,但它完全可以被重载。

规则3:父类中private final方法,子类可以重新定义,这种情况不是重写。

代码示例

规则1代码

public class FinalMethodTest
{
    public final void test(){}
}
class Sub extends FinalMethodTest
{
    // 下面方法定义将出现编译错误,不能重写final方法
    public void test(){}
}

规则2代码

public class Finaloverload {
    //final 修饰的方法只是不能重写,完全可以重载
    public final void test(){}
    public final void test(String arg){}
}

规则3代码

public class PrivateFinalMethodTest
{
    private final void test(){}
}
class Sub extends PrivateFinalMethodTest
{
    // 下面方法定义将不会出现问题
    public void test(){}
}
复制代码

final 和 jvm的关系

与前面介绍的锁和 volatile 相比较,对 final 域的读和写更像是普通的变量访问。对于 final 域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。

下面,我们通过一些示例性的代码来分别说明这两个规则:

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 域 
}
复制代码

}

这里假设一个线程 A 执行 writer () 方法,随后另一个线程 B 执行 reader () 方法。下面我们通过这两个线程的交互来说明这两个规则。

写 final 域的重排序规则

写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面 2 个方面:

  • JMM 禁止编译器把 final 域的写重排序到构造函数之外。
  • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。

现在让我们分析 writer () 方法。writer () 方法只包含一行代码:finalExample = new FinalExample ()。这行代码包含两个步骤:

  1. 构造一个 FinalExample 类型的对象;
  2. 把这个对象的引用赋值给引用变量 obj。

假设线程 B 读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序:

在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程 B 错误的读取了普通变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规则“限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。

写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程 B“看到”对象引用 obj 时,很可能 obj 对象还没有构造完成(对普通域 i 的写操作被重排序到构造函数外,此时初始值 2 还没有写入普通域 i)。

读 final 域的重排序规则

读 final 域的重排序规则如下:

  • 在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。

初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器。

reader() 方法包含三个操作:

  1. 初次读引用变量 obj;
  2. 初次读引用变量 obj 指向对象的普通域 j。
  3. 初次读引用变量 obj 指向对象的 final 域 i。

现在我们假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:

在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程 A 写入,这是一个错误的读取操作。而读 final 域的重排序规则会把读对象 final 域的操作“限定”在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。

读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。在这个示例程序中,如果该引用不为 null,那么引用对象的 final 域一定已经被 A 线程初始化过了。

如果 final 域是引用类型

上面我们看到的 final 域是基础数据类型,下面让我们看看如果 final 域是引用类型,将会有什么效果?

请看下列示例代码:

public class FinalReferenceExample {final int[] intArray;                     //final 是引用类型static FinalReferenceExample obj;
public FinalReferenceExample () {        // 构造函数intArray = new int[1];              //1intArray[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) {                    //5int temp1 = obj.intArray[0];       //6}}}

这里 final 域为一个引用类型,它引用一个 int 型的数组对象。对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:

  1. 在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

对上面的示例程序,我们假设首先线程 A 执行 writerOne() 方法,执行完后线程 B 执行 writerTwo() 方法,执行完后线程 C 执行 reader () 方法。下面是一种可能的线程执行时序:

在上图中,1 是对 final 域的写入,2 是对这个 final 域引用的对象的成员域的写入,3 是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。

JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。即 C 至少能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入,读线程 C 可能看的到,也可能看不到。JMM 不保证线程 B 的写入对读线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行结果不可预知。

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


目录
相关文章
|
1天前
|
并行计算 Java API
【Java】Java18的新特性
【Java】Java18的新特性
9 3
|
1天前
|
存储 Java 程序员
Java中的static关键字
Java中的static关键字
11 3
Java中的static关键字
|
1天前
|
Java
Java中的内置锁synchronized关键字和wait()、notifyAll()方法
【6月更文挑战第17天】Java的synchronized和wait/notify实现顺序打印ALI:共享volatile变量`count`,三个线程分别检查`count`值,匹配时打印并减1,未匹配时等待。每个`print`方法加锁,确保互斥访问。代码示例展示了线程同步机制。考虑异常处理及实际场景的扩展需求。
32 3
|
2天前
|
存储 缓存 算法
滚雪球学Java(62):HashSet的底层实现原理解析
【6月更文挑战第16天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
11 3
滚雪球学Java(62):HashSet的底层实现原理解析
|
3天前
|
算法 Java
Java关键字与保留字:如何正确使用,让你的代码“飞”起来!
【6月更文挑战第15天】Java编程中,关键字如&quot;class&quot;、&quot;int&quot;用于特定语法,保留字可能未来成为关键字。理解其含义和用法至关重要,避免用作标识符以防止未来冲突。正确使用如&quot;for&quot;控制循环,优化代码能提升效率,使程序运行更流畅。避免保留字,如&quot;goto&quot;、&quot;const&quot;,查阅文档确保合规性。通过代码优化,让程序效率更高,代码飞行在技术的云端。
|
3天前
|
Java 程序员
Java关键字:不只是简单的词汇,更是编程的“魔法咒语”!
【6月更文挑战第15天】Java关键字是编程的基石,如&quot;class&quot;定义类,&quot;new&quot;创建对象,&quot;if/else&quot;控制流程,&quot;for/while&quot;实现循环,&quot;public/private&quot;设置访问权限。示例展示了如何使用这些关键字来定义类、条件判断和循环,强调掌握关键字对提升代码效率至关重要。
|
6月前
|
Java
【零基础学Java】—final关键字与四种用法(二十九)
【零基础学Java】—final关键字与四种用法(二十九)
|
1月前
|
Java
Java中final关键字(看这篇就够了)
Java中final关键字(看这篇就够了)
31 0
|
5天前
|
缓存 安全 Java
Java基础5-一文了解final关键字的特性、使用方法,以及实现原理(一)
Java基础5-一文了解final关键字的特性、使用方法,以及实现原理(一)
15 0
|
1月前
|
Java
【JAVA面试题】final关键字的作用有哪些
【JAVA面试题】final关键字的作用有哪些