final这个词实现原理介绍final关键字的实际含义只有一句话,无法更改。什么是不可变的?即初始化完成后,不能再做任何修改。修改成员变量时,成员变量变为常量;修改方法时,不允许重写方法;修改类时,不允许继承类;修改参数使用列表时,不能更改作为参数输入的对象。这是不可变的。不管是引用一个新对象,重写还是继承,都是一种变化的方法,而final就是挡住这种变化的方式。final修饰变量final成员变量表示一个常量,只能赋值一次。赋值后该值不会改变(final要求地址值不能改变)。当final修饰一个基本数据类型时,意味着这个基本数据类型的值一旦被初始化就不能再改变;如果final修饰的是一个引用类型,那么在它初始化之后,就不能再指向其他对象,但是这个引用指向的对象的内容是可以改变的。本质上是一样的东西,因为引用的值是一个地址,而final需要一个值,即地址的值不变。final修饰一个成员变量(属性),必须显式初始化。这里有两种初始化方法。一种是在声明变量时对其进行初始化。第二种方法是在声明变量的时候不赋初值,而是在变量所在类的所有构造函数中都给这个变量赋初值。final修改后的方法使用final方法有两个原因。第一个原因是锁定方法,防止任何继承的类修改其含义,不能被覆盖;第二个原因是效率,final方法比非final方法更快,因为它们在编译时静态绑定,不需要在运行时动态绑定。注意:类的私有方法会被隐式指定为final方法。当一个类被final修饰时,表明该类不能被继承。final类中的成员变量可以根据需要设置为final,但需要注意的是final类中的所有成员方法都会被隐式指定为final方法。在使用final修饰一个类时,注意慎重选择,除非以后真的不用这个类做继承或者出于安全考虑,尽量不要将该类设计成final类。final关键字的好处final关键字可以提高性能。JVM和Java应用程序都缓存最终变量。final变量可以在多线程环境中安全地共享,而无需额外的同步开销。使用final关键字,JVM将优化方法、变量和类。注意final关键字可用于成员变量、局部变量、方法和类。final成员变量必须在构造函数中声明或初始化时进行初始化,否则会报编译错误。您不能重新分配最终变量。局部变量在声明时必须赋值。匿名类中的所有变量都必须是最终变量。final方法不能被覆盖。final类不能被继承。final关键字不同于finally关键字,后者用于异常处理。final关键字很容易与finalize()方法混淆,finalize()方法是定义在Object类中的方法,在垃圾回收之前由JVM调用。在接口中声明的所有变量本身都是最终的。final和abstract这两个关键字是反相关的,final类不能是abstract。final方法在编译时绑定,称为静态绑定。声明时没有初始化的final变量称为空白final变量(blankfinalvariable),必须在构造函数中初始化,或者调用this()初始化。如果不这样做,编译器会报错“finalvariable(变量名)需要初始化”。将类、方法和变量声明为final可以提高性能,让JVM有机会进行预估然后优化。根据Java代码约定,final变量是常量,常量名称通常大写。将一个集合对象声明为final意味着引用不能被改变,但是你可以添加、删除或改变它的内容。内存语义原理写入内存语义确保在对象引用对任何线程可见之前初始化最终字段。读取内存语义确保如果对象引用不为空,则最终字段已被初始化。总之,final字段的内存语义提供了初始化安全保证。写入内存语义:写入构造函数中的最终字段,以及随后将对象引用分配给引用变量,不能重新排序。读取内存语义:对包含final字段的对象的引用的初始读取,以及对该final字段的后续初始读取,不能重新排序。写入final字段的重新排序规则写入final字段的重新排序规则禁止在构造函数之外重新排序写入final字段。该规则的实现包括以下两个方面:JMM禁止编译器在构造函数之外对final字段的写入进行重新排序。编译器将在写入最终字段之后和构造函数返回之前插入一个StoreStore屏障。此屏障可防止处理器对构造函数之外的final字段的写入重新排序。现在让我们来分析writer()方法。writer()方法只包含一行代码:finalExample=newFinalExample()。这行代码包括两个步骤:构造一个FinalExample类型的对象;将此对象的引用分配给引用变量obj。假设线程B读取对象引用和读取对象字段之间没有重新排序(为什么需要这个假设将在稍后解释),下图是一个可能的执行顺序:上图中,写入公共字段的操作被重新排序被编译器整理出构造函数,读取线程B在初始化之前错误地读取了普通变量i的值。但是,写final字段的操作在构造函数内部被写final字段的重排序规则“限制”,读线程B在初始化后正确读取final变量的值。为final字段编写重新排序规则可确保在对象引用对任何线程可见之前正确初始化对象的final字段,而普通字段则没有此保证。以上图为例,当读线程B“看到”引用obj的对象时,很可能obj对象还没有构造(对公共字段i的写操作在构造函数外重新排序,初始化此时还没有写入值2公共域i)。读取final字段的重排序规则读取final字段的重排序规则如下:在一个线程中,第一次读取对象引用和第一次读取对象包含的final字段,JMM禁止处理器对这两个操作进行重排序(请注意,此规则仅适用于处理器)。编译器会在读final字段操作前插入一个LoadLoad屏障。对象引用的初始读取与该对象中包含的最终字段的初始读取之间存在间接依赖关系。由于编译器尊重间接依赖关系,因此编译器不会对这两个操作进行重新排序。大多数处理器也会遵守间接依赖关系,并且大多数处理器不会对这两个操作进行重新排序。但是,有一些处理器允许对具有间接依赖性的操作进行重新排序(例如alpha处理器),并且此规则是专门为此类处理器设计的。reader()方法包括三个操作:初始化引用变量obj;初始读取指向对象公共域j的引用变量obj。初始读取的引用变量obj指向对象的final字段i。现在我们假设写线程A没有任何重排序,程序在不服从间接依赖的处理器上执行。以下是一个可能的执行顺序。在上图中,处理器在读取对象引用之前对对象的普通字段进行的读取操作被重新排序。读取一个公共字段时,该字段还没有被写线程A写入,属于读操作错误。读取final字段的重排序规则会“限制”在读取对象引用后读取对象final字段的操作。此时final字段已经被A线程初始化,是正确的读操作。读取final字段的重排序规则可以保证:在读取对象的final字段之前,必须先读取包含final字段的对象的引用。在这个示例程序中,如果引用不为null,那么被引用对象的final域肯定已经被线程A初始化过了。如果final字段是引用类型上面我们看到的final字段是一个基本数据类型,那么让我们看看如果final字段是引用类型会怎么样呢?请看下面的示例代码:COPYpublicclassFinalReferenceExample{finalint[]intArray;//final是一个引用类型staticFinalReferenceExampleobj;publicFinalReferenceExample(){//构造函数intArray=newint[1];//1整数数组[0]=1;//2}publicstaticvoidwriterOne(){//Writer线程A执行obj=newFinalReferenceExample();//3}publicstaticvoidwriterTwo(){//Writer线程B执行obj.intArray[0]=2;//4}publicstaticvoidreader(){//读取线程C执行if(obj!=null){//5inttemp1=obj.intArray[0];//6}}}这里的final字段作为引用类型,指的是一个int类型的数组对象。对于引用类型,写入final字段的重新排序规则对编译器和处理器施加了以下约束:在构造函数内写入final引用对象的字段与在构造函数外写入构造对象的字段不同不能在这两个操作之间对引用变量重新排序。对于上面的示例程序,我们假设线程A先执行writerOne()方法,然后线程B执行完后执行writerTwo()方法,线程C执行完后执行reader()方法。下面是一个可能的线程执行时序:上图中1是写final字段,2是写这个final字段引用的对象的字段,3是将构造对象的引用赋值给一个引用多变的。除了前面提到的1不能与3重新排序外,2和3也不能重新排序。JMM可以保证读线程C至少能在构造函数中看到写线程A对最终引用对象字段的写入。也就是说,C至少可以看到数组下标0的值为1。在写入线程B写入数组元素的同时,读取线程C可能看到也可能看不到。JMM不保证线程B的写对读线程C可见,因为写线程B和读线程C之间存在数据竞争,此时的执行结果是不可预测的。如果要确保读取线程C看到写入线程B对数组元素的写入,则需要在写入线程B和读取线程C之间使用同步原语(lock或volatile)来确保内存可见性。为什么最终引用不能从构造函数中“逃脱”?前面我们提到,final字段的重排序规则可以保证:在引用变量对任何线程可见之前,引用变量指向的对象的final字段已经在构造函数中。已经正确初始化。其实要实现这个效果,需要一个保证:在构造函数内部,不能让构造对象的引用对其他线程可见,即对象引用不能在构造函数中“逃逸”。为了说明问题,让我们看下面的示例代码:COPYpublicclassFinalReferenceEscapeExample{finalinti;静态FinalReferenceEscapeExample对象;公共FinalReferenceEscapeExample(){i=1;//1写入最终字段obj=this;//2这里引用了“escape”}publicstaticvoidwriter(){newFinalReferenceEscapeExample();}publicstaticvoidreader{if(obj!=null){//3inttemp=obj.i;//4}}}假设一个线程A执行writer()方法,另一个线程B执行reader()方法。这里的操作2使对象在构造完成前对线程B可见。即使这里的操作2是构造函数的最后一步,即使程序中操作2安排在操作1之后,执行read()方法的线程也不一定能看到final字段初始化后的值,因为此处的操作1和操作2可能会重新排序。实际执行时序可能如下图所示:从上图我们可以看出,在构造函数返回之前,构造对象的引用是其他线程看不到的,因为此时final字段可能还没有初始化时间。构造函数返回后,保证任何线程都能看到final字段的正确初始化值。最终语义在处理器中的实现下面我们以x86处理器为例,说明最终语义在处理器中的具体实现。我们在上面提到,写入final字段的重新排序规则将要求编译器在写入final字段之后和从构造函数返回之前插入一个StoreStore屏障。读取final字段的重新排序规则要求编译器在读取final字段的操作之前插入一个LoadLoad屏障。由于x86处理器不会重新排序写入操作,因此在x86处理器上省略了写入最终字段所需的StoreStore屏障。此外,由于x86处理器不会对具有间接依赖性的操作进行重新排序,因此在x86处理器上也省略了读取最终字段所需的LoadLoad屏障。也就是说,在x86处理器中,final字段的读写不会插入任何内存屏障!为什么要增强Final的语义旧Java内存模型中最严重的缺陷之一是线程可能会看到final字段更改的值。例如,一个线程当前看到一个整型final字段的值为0(初始化前的默认值),过了一段时间,当线程读取final字段的值时,发现该值变成了1(通过线程初始化后的某个值)。最常见的例子是,在旧的Java内存模型中,String的值可能会发生变化。为了修复这个漏洞,JSR-133专家组增强了final的语义。通过为final字段添加写入和读取重排序规则,可以为java程序员提供初始化安全保证:只要正确构造对象(构造对象的引用没有在构造函数中“逃逸”),那么就没有需要使用synchronization(指lock和volatile的使用),可以保证任何线程在构造函数中初始化后都能看到final字段的值。final、finally、finalize的区别final可以用来修饰类、方法、变量,各有不同的含义。final修饰的类,意味着不能被继承和扩展。final变量不能修改,final方法不能重复。写入(覆盖)。最后是Java保证关键代码一定要执行的机制。我们可以使用try-finally或try-catch-finally来执行关闭JDBC连接和确保解锁锁等操作。finalize是基础类java.lang.Object的一个方法,其设计目的是保证对象在被垃圾回收前完成特定资源的回收。finalize机制现已弃用,自JDK9起已被标记为弃用。本文由传智教育博学谷狂野架构师教研团队发布。如果本文对您有帮助,请关注并点赞;有什么建议也可以留言或私信。您的支持是我坚持创作的动力。转载请注明出处!
