夯實(shí)Java基礎(chǔ)系列4:一文了解final關(guān)鍵字的特性、使用方法,以及實(shí)現(xiàn)原理

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請(qǐng)到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點(diǎn)下Star哈

文章首發(fā)于我的個(gè)人博客:

www.how2playlife.com

本文是微信公眾號(hào)【Java技術(shù)江湖】的《夯實(shí)Java基礎(chǔ)系列博文》其中一篇,本文部分內(nèi)容來源于網(wǎng)絡(luò),為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯(cuò)的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán),請(qǐng)聯(lián)系作者。

該系列博文會(huì)告訴你如何從入門到進(jìn)階,一步步地學(xué)習(xí)Java基礎(chǔ)知識(shí),并上手進(jìn)行實(shí)戰(zhàn),接著了解每個(gè)Java知識(shí)點(diǎn)背后的實(shí)現(xiàn)原理,更完整地了解整個(gè)Java技術(shù)體系,形成自己的知識(shí)框架。為了更好地總結(jié)和檢驗(yàn)?zāi)愕膶W(xué)習(xí)成果,本系列文章也會(huì)提供部分知識(shí)點(diǎn)對(duì)應(yīng)的面試題以及參考答案。

如果對(duì)本系列文章有什么建議,或者是有什么疑問的話,也可以關(guān)注公眾號(hào)【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。

final關(guān)鍵字特性

final關(guān)鍵字在java中使用非常廣泛,可以申明成員變量、方法、類、本地變量。一旦將引用聲明為final,將無法再改變這個(gè)引用。final關(guān)鍵字還能保證內(nèi)存同步,本博客將會(huì)從final關(guān)鍵字的特性到從java內(nèi)存層面保證同步講解。這個(gè)內(nèi)容在面試中也有可能會(huì)出現(xiàn)。

final使用

final變量

final變量有成員變量或者是本地變量(方法內(nèi)的局部變量),在類成員中final經(jīng)常和static一起使用,作為類常量使用。其中類常量必須在聲明時(shí)初始化,final成員常量可以在構(gòu)造函數(shù)初始化。

public class Main {
    public static final int i; //報(bào)錯(cuò),必須初始化 因?yàn)槌A吭诔A砍刂芯痛嬖诹?,調(diào)用時(shí)不需要類的初始化,所以必須在聲明時(shí)初始化
    public static final int j;
    Main() {
        i = 2;
        j = 3;
    }
}

就如上所說的,對(duì)于類常量,JVM會(huì)緩存在常量池中,在讀取該變量時(shí)不會(huì)加載這個(gè)類。


public class Main {
    public static final int i = 2;
    Main() {
        System.out.println("調(diào)用構(gòu)造函數(shù)"); // 該方法不會(huì)調(diào)用
    }
    public static void main(String[] args) {
        System.out.println(Main.i);
    }
}

final修飾基本數(shù)據(jù)類型變量和引用

@Test
public void final修飾基本類型變量和引用() {
    final int a = 1;
    final int[] b = {1};
    final int[] c = {1};
//  b = c;報(bào)錯(cuò)
    b[0] = 1;
    final String aa = "a";
    final Fi f = new Fi();
    //aa = "b";報(bào)錯(cuò)
    // f = null;//報(bào)錯(cuò)
    f.a = 1;
}

final方法表示該方法不能被子類的方法重寫,將方法聲明為final,在編譯的時(shí)候就已經(jīng)靜態(tài)綁定了,不需要在運(yùn)行時(shí)動(dòng)態(tài)綁定。final方法調(diào)用時(shí)使用的是invokespecial指令。

class PersonalLoan{
    public final String getName(){
        return"personal loan”;
    }
}

class CheapPersonalLoan extends PersonalLoan{
    @Override
    public final String getName(){
        return"cheap personal loan";//編譯錯(cuò)誤,無法被重載
    }

    public String test() {
        return getName(); //可以調(diào)用,因?yàn)槭莗ublic方法
    }
}

final類

final類不能被繼承,final類中的方法默認(rèn)也會(huì)是final類型的,java中的String類和Integer類都是final類型的。

class Si{
    //一般情況下final修飾的變量一定要被初始化。
    //只有下面這種情況例外,要求該變量必須在構(gòu)造方法中被初始化。
    //并且不能有空參數(shù)的構(gòu)造方法。
    //這樣就可以讓每個(gè)實(shí)例都有一個(gè)不同的變量,并且這個(gè)變量在每個(gè)實(shí)例中只會(huì)被初始化一次
    //于是這個(gè)變量在單個(gè)實(shí)例里就是常量了。
    final int s ;
    Si(int s) {
        this.s = s;
    }
}
class Bi {
    final int a = 1;
    final void go() {
        //final修飾方法無法被繼承
    }
}
class Ci extends Bi {
    final int a = 1;
//        void go() {
//            //final修飾方法無法被繼承
//        }
}
final char[]a = {'a'};
final int[]b = {1};
final class PersonalLoan{}

class CheapPersonalLoan extends PersonalLoan {  //編譯錯(cuò)誤,無法被繼承 
}

@Test
public void final修飾類() {
    //引用沒有被final修飾,所以是可變的。
    //final只修飾了Fi類型,即Fi實(shí)例化的對(duì)象在堆中內(nèi)存地址是不可變的。
    //雖然內(nèi)存地址不可變,但是可以對(duì)內(nèi)部的數(shù)據(jù)做改變。
    Fi f = new Fi();
    f.a = 1;
    System.out.println(f);
    f.a = 2;
    System.out.println(f);
    //改變實(shí)例中的值并不改變內(nèi)存地址。

    Fi ff = f;
    //讓引用指向新的Fi對(duì)象,原來的f對(duì)象由新的引用ff持有。
    //引用的指向改變也不會(huì)改變原來對(duì)象的地址
    f = new Fi();
    System.out.println(f);
    System.out.println(ff);
}

final關(guān)鍵字的知識(shí)點(diǎn)

  1. final成員變量必須在聲明的時(shí)候初始化或者在構(gòu)造器中初始化,否則就會(huì)報(bào)編譯錯(cuò)誤。final變量一旦被初始化后不能再次賦值。
  2. 本地變量必須在聲明時(shí)賦值。 因?yàn)闆]有初始化的過程
  3. 在匿名類中所有變量都必須是final變量。
  4. final方法不能被重寫, final類不能被繼承
  5. 接口中聲明的所有變量本身是final的。類似于匿名類
  6. final和abstract這兩個(gè)關(guān)鍵字是反相關(guān)的,final類就不可能是abstract的。
  7. final方法在編譯階段綁定,稱為靜態(tài)綁定(static binding)。
  8. 將類、方法、變量聲明為final能夠提高性能,這樣JVM就有機(jī)會(huì)進(jìn)行估計(jì),然后優(yōu)化。

final方法的好處:

  1. 提高了性能,JVM在常量池中會(huì)緩存final變量
  2. final變量在多線程中并發(fā)安全,無需額外的同步開銷
  3. final方法是靜態(tài)編譯的,提高了調(diào)用速度
  4. final類創(chuàng)建的對(duì)象是只可讀的,在多線程可以安全共享

final關(guān)鍵字的最佳實(shí)踐

final的用法

1、final 對(duì)于常量來說,意味著值不能改變,例如 final int i=100。這個(gè)i的值永遠(yuǎn)都是100。
但是對(duì)于變量來說又不一樣,只是標(biāo)識(shí)這個(gè)引用不可被改變,例如 final File f=new File("c:\test.txt");

那么這個(gè)f一定是不能被改變的,如果f本身有方法修改其中的成員變量,例如是否可讀,是允許修改的。有個(gè)形象的比喻:一個(gè)女子定義了一個(gè)final的老公,這個(gè)老公的職業(yè)和收入都是允許改變的,只是這個(gè)女人不會(huì)換老公而已。

關(guān)于空白final

final修飾的變量有三種:靜態(tài)變量、實(shí)例變量和局部變量,分別表示三種類型的常量。
 另外,final變量定義的時(shí)候,可以先聲明,而不給初值,這中變量也稱為final空白,無論什么情況,編譯器都確??瞻譮inal在使用之前必須被初始化。
 
但是,final空白在final關(guān)鍵字final的使用上提供了更大的靈活性,為此,一個(gè)類中的final數(shù)據(jù)成員就可以實(shí)現(xiàn)依對(duì)象而有所不同,卻有保持其恒定不變的特征。

public class FinalTest { 
final int p; 
final int q=3; 
FinalTest(){ 
p=1; 
} 
FinalTest(int i){ 
p=i;//可以賦值,相當(dāng)于直接定義p 
q=i;//不能為一個(gè)final變量賦值 
} 
} 

final內(nèi)存分配

剛提到了內(nèi)嵌機(jī)制,現(xiàn)在詳細(xì)展開。
要知道調(diào)用一個(gè)函數(shù)除了函數(shù)本身的執(zhí)行時(shí)間之外,還需要額外的時(shí)間去尋找這個(gè)函數(shù)(類內(nèi)部有一個(gè)函數(shù)簽名和函數(shù)地址的映射表)。所以減少函數(shù)調(diào)用次數(shù)就等于降低了性能消耗。

final修飾的函數(shù)會(huì)被編譯器優(yōu)化,優(yōu)化的結(jié)果是減少了函數(shù)調(diào)用的次數(shù)。如何實(shí)現(xiàn)的,舉個(gè)例子給你看:

public class Test{ 
final void func(){System.out.println("g");}; 
public void main(String[] args){ 
for(int j=0;j<1000;j++)   
func(); 
}} 
經(jīng)過編譯器優(yōu)化之后,這個(gè)類變成了相當(dāng)于這樣寫: 
public class Test{ 
final void func(){System.out.println("g");}; 
public void main(String[] args){ 
for(int j=0;j<1000;j++)  
{System.out.println("g");} 
}} 

看出來區(qū)別了吧?編譯器直接將func的函數(shù)體內(nèi)嵌到了調(diào)用函數(shù)的地方,這樣的結(jié)果是節(jié)省了1000次函數(shù)調(diào)用,當(dāng)然編譯器處理成字節(jié)碼,只是我們可以想象成這樣,看個(gè)明白。

不過,當(dāng)函數(shù)體太長的話,用final可能適得其反,因?yàn)榻?jīng)過編譯器內(nèi)嵌之后代碼長度大大增加,于是就增加了jvm解釋字節(jié)碼的時(shí)間。

在使用final修飾方法的時(shí)候,編譯器會(huì)將被final修飾過的方法插入到調(diào)用者代碼處,提高運(yùn)行速度和效率,但被final修飾的方法體不能過大,編譯器可能會(huì)放棄內(nèi)聯(lián),但究竟多大的方法會(huì)放棄,我還沒有做測試來計(jì)算過。

下面這些內(nèi)容是通過兩個(gè)疑問來繼續(xù)闡述的

使用final修飾方法會(huì)提高速度和效率嗎

見下面的測試代碼,我會(huì)執(zhí)行五次:

public class Test   
{   
    public static void getJava()   
    {   
        String str1 = "Java ";   
        String str2 = "final ";   
        for (int i = 0; i < 10000; i++)   
        {   
            str1 += str2;   
        }   
    }   
    public static final void getJava_Final()   
    {   
        String str1 = "Java ";   
        String str2 = "final ";   
        for (int i = 0; i < 10000; i++)   
        {   
            str1 += str2;   
        }   
    }   
    public static void main(String[] args)   
    {   
        long start = System.currentTimeMillis();   
        getJava();   
        System.out.println("調(diào)用不帶final修飾的方法執(zhí)行時(shí)間為:" + (System.currentTimeMillis() - start) + "毫秒時(shí)間");   
        start = System.currentTimeMillis();   
        String str1 = "Java ";   
        String str2 = "final ";   
        for (int i = 0; i < 10000; i++)   
        {   
            str1 += str2;   
        }   
        System.out.println("正常的執(zhí)行時(shí)間為:" + (System.currentTimeMillis() - start) + "毫秒時(shí)間");   
        start = System.currentTimeMillis();   
        getJava_Final();   
        System.out.println("調(diào)用final修飾的方法執(zhí)行時(shí)間為:" + (System.currentTimeMillis() - start) + "毫秒時(shí)間");   
    }   
}  


結(jié)果為: 
第一次: 
調(diào)用不帶final修飾的方法執(zhí)行時(shí)間為:1732毫秒時(shí)間 
正常的執(zhí)行時(shí)間為:1498毫秒時(shí)間 
調(diào)用final修飾的方法執(zhí)行時(shí)間為:1593毫秒時(shí)間 
第二次: 
調(diào)用不帶final修飾的方法執(zhí)行時(shí)間為:1217毫秒時(shí)間 
正常的執(zhí)行時(shí)間為:1031毫秒時(shí)間 
調(diào)用final修飾的方法執(zhí)行時(shí)間為:1124毫秒時(shí)間 
第三次: 
調(diào)用不帶final修飾的方法執(zhí)行時(shí)間為:1154毫秒時(shí)間 
正常的執(zhí)行時(shí)間為:1140毫秒時(shí)間 
調(diào)用final修飾的方法執(zhí)行時(shí)間為:1202毫秒時(shí)間 
第四次: 
調(diào)用不帶final修飾的方法執(zhí)行時(shí)間為:1139毫秒時(shí)間 
正常的執(zhí)行時(shí)間為:999毫秒時(shí)間 
調(diào)用final修飾的方法執(zhí)行時(shí)間為:1092毫秒時(shí)間 
第五次: 
調(diào)用不帶final修飾的方法執(zhí)行時(shí)間為:1186毫秒時(shí)間 
正常的執(zhí)行時(shí)間為:1030毫秒時(shí)間 
調(diào)用final修飾的方法執(zhí)行時(shí)間為:1109毫秒時(shí)間 

由以上運(yùn)行結(jié)果不難看出,執(zhí)行最快的是“正常的執(zhí)行”即代碼直接編寫,而使用final修飾的方法,不像有些書上或者文章上所說的那樣,速度與效率與“正常的執(zhí)行”無異,而是位于第二位,最差的是調(diào)用不加final修飾的方法。 

觀點(diǎn):加了比不加好一點(diǎn)。

使用final修飾變量會(huì)讓變量的值不能被改變嗎;

見代碼:

public class Final   
{   
    public static void main(String[] args)   
    {   
        Color.color[3] = "white";   
        for (String color : Color.color)   
            System.out.print(color+" ");   
    }   
}   
  
class Color   
{   
    public static final String[] color = { "red", "blue", "yellow", "black" };   
}  


執(zhí)行結(jié)果: 
red blue yellow white 
看!,黑色變成了白色。 


在使用findbugs插件時(shí),就會(huì)提示public static String[] color = { "red", "blue", "yellow", "black" };這行代碼不安全,但加上final修飾,這行代碼仍然是不安全的,因?yàn)閒inal沒有做到保證變量的值不會(huì)被修改!

原因是:final關(guān)鍵字只能保證變量本身不能被賦與新值,而不能保證變量的內(nèi)部結(jié)構(gòu)不被修改。例如在main方法有如下代碼Color.color = new String[]{""};就會(huì)報(bào)錯(cuò)了。

如何保證數(shù)組內(nèi)部不被修改

那可能有的同學(xué)就會(huì)問了,加上final關(guān)鍵字不能保證數(shù)組不會(huì)被外部修改,那有什么方法能夠保證呢?答案就是降低訪問級(jí)別,把數(shù)組設(shè)為private。這樣的話,就解決了數(shù)組在外部被修改的不安全性,但也產(chǎn)生了另一個(gè)問題,那就是這個(gè)數(shù)組要被外部使用的。 

解決這個(gè)問題見代碼:

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("為了代碼安全,不能修改數(shù)組");   
        }   
        @Override  
        public int size()   
        {   
            return _color.length;   
        }   
    };  


}

這樣就OK了,既保證了代碼安全,又能讓數(shù)組中的元素被訪問了。

final方法的三條規(guī)則

規(guī)則1:final修飾的方法不可以被重寫。

規(guī)則2:final修飾的方法僅僅是不能重寫,但它完全可以被重載。

規(guī)則3:父類中private final方法,子類可以重新定義,這種情況不是重寫。

代碼示例

規(guī)則1代碼

public class FinalMethodTest
{
    public final void test(){}
}
class Sub extends FinalMethodTest
{
    // 下面方法定義將出現(xiàn)編譯錯(cuò)誤,不能重寫final方法
    public void test(){}
}

規(guī)則2代碼

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

規(guī)則3代碼

public class PrivateFinalMethodTest
{
    private final void test(){}
}
class Sub extends PrivateFinalMethodTest
{
    // 下面方法定義將不會(huì)出現(xiàn)問題
    public void test(){}
}

final 和 jvm的關(guān)系

與前面介紹的鎖和 volatile 相比較,對(duì) final 域的讀和寫更像是普通的變量訪問。對(duì)于 final 域,編譯器和處理器要遵守兩個(gè)重排序規(guī)則:

  1. 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè) final 域的寫入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
  2. 初次讀一個(gè)包含 final 域的對(duì)象的引用,與隨后初次讀這個(gè) final 域,這兩個(gè)操作之間不能重排序。

下面,我們通過一些示例性的代碼來分別說明這兩個(gè)規(guī)則:

<pre>public class FinalExample {
int i; // 普通變量
final int j; //final 變量
static FinalExample obj;

public void FinalExample () {     // 構(gòu)造函數(shù) 
    i = 1;                        // 寫普通域 
    j = 2;                        // 寫 final 域 
}

public static void writer () {    // 寫線程 A 執(zhí)行 
    obj = new FinalExample ();
}

public static void reader () {       // 讀線程 B 執(zhí)行 
    FinalExample object = obj;       // 讀對(duì)象引用 
    int a = object.i;                // 讀普通域 
    int b = object.j;                // 讀 final 域 
}

}
</pre>

這里假設(shè)一個(gè)線程 A 執(zhí)行 writer () 方法,隨后另一個(gè)線程 B 執(zhí)行 reader () 方法。下面我們通過這兩個(gè)線程的交互來說明這兩個(gè)規(guī)則。

寫 final 域的重排序規(guī)則

寫 final 域的重排序規(guī)則禁止把 final 域的寫重排序到構(gòu)造函數(shù)之外。這個(gè)規(guī)則的實(shí)現(xiàn)包含下面 2 個(gè)方面:

  • JMM 禁止編譯器把 final 域的寫重排序到構(gòu)造函數(shù)之外。
  • 編譯器會(huì)在 final 域的寫之后,構(gòu)造函數(shù) return 之前,插入一個(gè) StoreStore 屏障。這個(gè)屏障禁止處理器把 final 域的寫重排序到構(gòu)造函數(shù)之外。

現(xiàn)在讓我們分析 writer () 方法。writer () 方法只包含一行代碼:finalExample = new FinalExample ()。這行代碼包含兩個(gè)步驟:

  1. 構(gòu)造一個(gè) FinalExample 類型的對(duì)象;
  2. 把這個(gè)對(duì)象的引用賦值給引用變量 obj。

假設(shè)線程 B 讀對(duì)象引用與讀對(duì)象的成員域之間沒有重排序(馬上會(huì)說明為什么需要這個(gè)假設(shè)),下圖是一種可能的執(zhí)行時(shí)序:

在上圖中,寫普通域的操作被編譯器重排序到了構(gòu)造函數(shù)之外,讀線程 B 錯(cuò)誤的讀取了普通變量 i 初始化之前的值。而寫 final 域的操作,被寫 final 域的重排序規(guī)則“限定”在了構(gòu)造函數(shù)之內(nèi),讀線程 B 正確的讀取了 final 變量初始化之后的值。

寫 final 域的重排序規(guī)則可以確保:在對(duì)象引用為任意線程可見之前,對(duì)象的 final 域已經(jīng)被正確初始化過了,而普通域不具有這個(gè)保障。以上圖為例,在讀線程 B“看到”對(duì)象引用 obj 時(shí),很可能 obj 對(duì)象還沒有構(gòu)造完成(對(duì)普通域 i 的寫操作被重排序到構(gòu)造函數(shù)外,此時(shí)初始值 2 還沒有寫入普通域 i)。

讀 final 域的重排序規(guī)則

讀 final 域的重排序規(guī)則如下:

  • 在一個(gè)線程中,初次讀對(duì)象引用與初次讀該對(duì)象包含的 final 域,JMM 禁止處理器重排序這兩個(gè)操作(注意,這個(gè)規(guī)則僅僅針對(duì)處理器)。編譯器會(huì)在讀 final 域操作的前面插入一個(gè) LoadLoad 屏障。

初次讀對(duì)象引用與初次讀該對(duì)象包含的 final 域,這兩個(gè)操作之間存在間接依賴關(guān)系。由于編譯器遵守間接依賴關(guān)系,因此編譯器不會(huì)重排序這兩個(gè)操作。大多數(shù)處理器也會(huì)遵守間接依賴,大多數(shù)處理器也不會(huì)重排序這兩個(gè)操作。但有少數(shù)處理器允許對(duì)存在間接依賴關(guān)系的操作做重排序(比如 alpha 處理器),這個(gè)規(guī)則就是專門用來針對(duì)這種處理器。

reader() 方法包含三個(gè)操作:

  1. 初次讀引用變量 obj;
  2. 初次讀引用變量 obj 指向?qū)ο蟮钠胀ㄓ?j。
  3. 初次讀引用變量 obj 指向?qū)ο蟮?final 域 i。

現(xiàn)在我們假設(shè)寫線程 A 沒有發(fā)生任何重排序,同時(shí)程序在不遵守間接依賴的處理器上執(zhí)行,下面是一種可能的執(zhí)行時(shí)序:

在上圖中,讀對(duì)象的普通域的操作被處理器重排序到讀對(duì)象引用之前。讀普通域時(shí),該域還沒有被寫線程 A 寫入,這是一個(gè)錯(cuò)誤的讀取操作。而讀 final 域的重排序規(guī)則會(huì)把讀對(duì)象 final 域的操作“限定”在讀對(duì)象引用之后,此時(shí)該 final 域已經(jīng)被 A 線程初始化過了,這是一個(gè)正確的讀取操作。

讀 final 域的重排序規(guī)則可以確保:在讀一個(gè)對(duì)象的 final 域之前,一定會(huì)先讀包含這個(gè) final 域的對(duì)象的引用。在這個(gè)示例程序中,如果該引用不為 null,那么引用對(duì)象的 final 域一定已經(jīng)被 A 線程初始化過了。

如果 final 域是引用類型

上面我們看到的 final 域是基礎(chǔ)數(shù)據(jù)類型,下面讓我們看看如果 final 域是引用類型,將會(huì)有什么效果?

請(qǐng)看下列示例代碼:

<pre>public class FinalReferenceExample {
final int[] intArray; //final 是引用類型
static FinalReferenceExample obj;

public FinalReferenceExample () { // 構(gòu)造函數(shù)
intArray = new int[1]; //1
intArray[0] = 1; //2
}

public static void writerOne () { // 寫線程 A 執(zhí)行
obj = new FinalReferenceExample (); //3
}

public static void writerTwo () { // 寫線程 B 執(zhí)行
obj.intArray[0] = 2; //4
}

public static void reader () { // 讀線程 C 執(zhí)行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}
</pre>

這里 final 域?yàn)橐粋€(gè)引用類型,它引用一個(gè) int 型的數(shù)組對(duì)象。對(duì)于引用類型,寫 final 域的重排序規(guī)則對(duì)編譯器和處理器增加了如下約束:

  1. 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè) final 引用的對(duì)象的成員域的寫入,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。

對(duì)上面的示例程序,我們假設(shè)首先線程 A 執(zhí)行 writerOne() 方法,執(zhí)行完后線程 B 執(zhí)行 writerTwo() 方法,執(zhí)行完后線程 C 執(zhí)行 reader () 方法。下面是一種可能的線程執(zhí)行時(shí)序:

在上圖中,1 是對(duì) final 域的寫入,2 是對(duì)這個(gè) final 域引用的對(duì)象的成員域的寫入,3 是把被構(gòu)造的對(duì)象的引用賦值給某個(gè)引用變量。這里除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。

JMM 可以確保讀線程 C 至少能看到寫線程 A 在構(gòu)造函數(shù)中對(duì) final 引用對(duì)象的成員域的寫入。即 C 至少能看到數(shù)組下標(biāo) 0 的值為 1。而寫線程 B 對(duì)數(shù)組元素的寫入,讀線程 C 可能看的到,也可能看不到。JMM 不保證線程 B 的寫入對(duì)讀線程 C 可見,因?yàn)閷懢€程 B 和讀線程 C 之間存在數(shù)據(jù)競爭,此時(shí)的執(zhí)行結(jié)果不可預(yù)知。

如果想要確保讀線程 C 看到寫線程 B 對(duì)數(shù)組元素的寫入,寫線程 B 和讀線程 C 之間需要使用同步原語(lock 或 volatile)來確保內(nèi)存可見性。

參考文章

https://www.infoq.cn/article/java-memory-model-6
http://m.itdecent.cn/p/067b6c89875a
http://m.itdecent.cn/p/f68d6ef2dcf0
https://www.cnblogs.com/xiaoxi/p/6392154.html
https://www.iteye.com/blog/cakin24-2334965
https://blog.csdn.net/chengqiuming/article/details/70139503
https://blog.csdn.net/hupuxiang/article/details/7362267

微信公眾號(hào)

個(gè)人公眾號(hào):黃小斜

黃小斜是跨考軟件工程的 985 碩士,自學(xué) Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術(shù)小白成長為阿里工程師。

作者專注于 JAVA 后端技術(shù)棧,熱衷于分享程序員干貨、學(xué)習(xí)經(jīng)驗(yàn)、求職心得和程序人生,目前黃小斜的CSDN博客有百萬+訪問量,知乎粉絲2W+,全網(wǎng)已有10W+讀者。

黃小斜是一個(gè)斜杠青年,堅(jiān)持學(xué)習(xí)和寫作,相信終身學(xué)習(xí)的力量,希望和更多的程序員交朋友,一起進(jìn)步和成長!

原創(chuàng)電子書:
關(guān)注微信公眾號(hào)【黃小斜】后回復(fù)【原創(chuàng)電子書】即可領(lǐng)取我原創(chuàng)的電子書《菜鳥程序員修煉手冊:從技術(shù)小白到阿里巴巴Java工程師》這份電子書總結(jié)了我2年的Java學(xué)習(xí)之路,包括學(xué)習(xí)方法、技術(shù)總結(jié)、求職經(jīng)驗(yàn)和面試技巧等內(nèi)容,已經(jīng)幫助很多的程序員拿到了心儀的offer!

程序員3T技術(shù)學(xué)習(xí)資源: 一些程序員學(xué)習(xí)技術(shù)的資源大禮包,關(guān)注公眾號(hào)后,后臺(tái)回復(fù)關(guān)鍵字 “資料” 即可免費(fèi)無套路獲取,包括Java、python、C++、大數(shù)據(jù)、機(jī)器學(xué)習(xí)、前端、移動(dòng)端等方向的技術(shù)資料。

技術(shù)公眾號(hào):Java技術(shù)江湖

如果大家想要實(shí)時(shí)關(guān)注我更新的文章以及分享的干貨的話,可以關(guān)注我的微信公眾號(hào)【Java技術(shù)江湖】

這是一位阿里 Java 工程師的技術(shù)小站。作者黃小斜,專注 Java 相關(guān)技術(shù):SSM、SpringBoot、MySQL、分布式、中間件、集群、Linux、網(wǎng)絡(luò)、多線程,偶爾講點(diǎn)Docker、ELK,同時(shí)也分享技術(shù)干貨和學(xué)習(xí)經(jīng)驗(yàn),致力于Java全棧開發(fā)!

(關(guān)注公眾號(hào)后回復(fù)”Java“即可領(lǐng)取 Java基礎(chǔ)、進(jìn)階、項(xiàng)目和架構(gòu)師等免費(fèi)學(xué)習(xí)資料,更有數(shù)據(jù)庫、分布式、微服務(wù)等熱門技術(shù)學(xué)習(xí)視頻,內(nèi)容豐富,兼顧原理和實(shí)踐,另外也將贈(zèng)送作者原創(chuàng)的Java學(xué)習(xí)指南、Java程序員面試指南等干貨資源)

Java工程師必備學(xué)習(xí)資源: 一些Java工程師常用學(xué)習(xí)資源,關(guān)注公眾號(hào)后,后臺(tái)回復(fù)關(guān)鍵字 “Java” 即可免費(fèi)無套路獲取。

我的公眾號(hào)

?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容