成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Java面試題,深入理解final關(guān)鍵字

Michael_Ding / 2524人閱讀

摘要:寫域重排序規(guī)則寫域的重排序規(guī)則禁止對域的寫重排序到構(gòu)造函數(shù)之外,這個規(guī)則的實現(xiàn)主要包含了兩個方面禁止編譯器把域的寫重排序到構(gòu)造函數(shù)之外編譯器會在域?qū)懼?,?gòu)造函數(shù)之前,插入一個屏障。結(jié)論只有當構(gòu)造函數(shù)返回時,引用才應該從線程中逸出。

final關(guān)鍵字final的簡介

final可以修飾變量,方法和類,用于表示所修飾的內(nèi)容一旦賦值之后就不會再被改變,比如String類就是一個final類型的類。

final的具體使用場景

final能夠修飾變量,方法和類,也就是final使用范圍基本涵蓋了java每個地方, 下面就分別以鎖修飾的位置:變量,方法和類分別介紹。

final修飾成員變量
public class FinalExample {    //聲明變量的時候,就進行初始化
    private final int num=6;    //類變量必須要在靜態(tài)初始化塊中指定初始值或者聲明該類變量時指定初始值
    // private final String str; //編譯錯誤:因為非靜態(tài)變量不可以在靜態(tài)初始化快中賦初值
    private final static String name;    private final double score;    private final char ch;    //private final char ch2;//編譯錯誤:TODO:因為沒有在構(gòu)造器、初始化代碼塊和聲明時賦值
    
    {        //實例變量在初始化代碼塊賦初值
        ch="a";
    }    
    static {
        name="aaaaa";
    }    
    public FinalExample(){        //num=1;編譯錯誤:已經(jīng)賦值后,就不能再修改了
        score=90.0;
    }    
    public void ch2(){        //ch2="c";//編譯錯誤:實例方法無法給final變量賦值
    }
}

類變量:必須要在靜態(tài)初始化塊中指定初始值或者聲明該類變量時指定初始值,而且只能在這兩個地方之一進行指定

實例變量:必要要在非靜態(tài)初始化塊,聲明該實例變量或者在構(gòu)造器中指定初始值,而且只能在這三個地方進行指定

final修飾局部變量

final局部變量由程序員進行顯式初始化, 如果final局部變量已經(jīng)進行了初始化則后面就不能再次進行更改, 如果final變量未進行初始化,可以進行賦值,當且僅有一次賦值,一旦賦值之后再次賦值就會出錯。

public void test(){    final int a=1;    //a=2;//編譯錯誤:final局部變量已經(jīng)進行了初始化則后面就不能再次進行更改}

final基本數(shù)據(jù)類型 VS final引用數(shù)據(jù)類型

如果final修飾的是一個基本數(shù)據(jù)類型的數(shù)據(jù),一旦賦值后就不能再次更改, 那么,如果final是引用數(shù)據(jù)類型了?這個引用的對象能夠改變嗎?

public class FinalExample2 {    private static class Person {        private String name;        private int age;        public void setName(String name) {            this.name = name;
        }        public String getName() {            return name;
        }        public void setAge(int age) {            this.age = age;
        }        public int getAge() {            return age;
        }        public Person(String name, int age) {            this.name=name;            this.age = age;
        }        @Override
        public String toString() {            StringBuilder res=new StringBuilder();
            res.append("[").append("name="+name+",age="+age).append("]");            return res.toString();
        }
    }    private static final Person person=new Person("小李子",23);    public static void main(String[] args) {        System.out.println(person);
        person.setAge(24);        System.out.println(person);
    }
}

輸出結(jié)果:

[name=小李子,age=23]
[name=小李子,age=24]

當我們對final修飾的引用數(shù)據(jù)類型變量person的屬性改成24,是可以成功操作的。 通過這個實驗我們就可以看出來當final修飾基本數(shù)據(jù)類型變量時,不能對基本數(shù)據(jù)類型變量重新賦值, 因此基本數(shù)據(jù)類型變量不能被改變。 而對于引用類型變量而言,它僅僅保存的是一個引用,final只保證這個引用類型變量所引用的地址不會發(fā)生改變, 即一直引用這個對象,但這個對象屬性是可以改變的。

宏變量

利用final變量的不可更改性,在滿足以下三個條件時,該變量就會成為一個“宏變量”,即是一個常量。

使用final修飾符修飾

在定義該final變量時就指定了初始值;

該初始值在編譯時就能夠唯一確定

注意:當程序中其他地方使用該宏變量的地方,編譯器會直接替換成該變量的值。

final修飾方法

重寫(Override)

被final修飾的方法不能夠被子類所重寫。 比如在Object中,getClass()方法就是final的,我們就不能重寫該方法, 但是hashCode()方法就不是被final所修飾的,我們就可以重寫hashCode()方法。

重載(Overload)

被final修飾的方法是可以重載的

final修飾類

當一個類被final修飾時,該類是不能被子類繼承的。 子類繼承往往可以重寫父類的方法和改變父類屬性,會帶來一定的安全隱患, 因此,當一個類不希望被繼承時就可以使用final修飾。

不可變類

final經(jīng)常會被用作不變類上。我們先來看看什么是不可變類:

使用private和final修飾符來修飾該類的成員變量

提供帶參的構(gòu)造器用于初始化類的成員變量

僅為該類的成員變量提供getter方法,不提供setter方法,因為普通方法無法修改fina修飾的成員變量

如果有必要就重寫Object類的hashCode()和equals()方法,應該保證用equals()判斷相同的兩個對象其Hashcode值也是相等的。

JDK中提供的八個包裝類和String類都是不可變類。

final域重排序規(guī)則 final為基本類型
public class FinalDemo {    private int a;  //普通域
    private final int b; //final域-->int基本類型
    private static FinalDemo finalDemo;//引用類型,但不是final修飾的

    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域
    }
}

假設線程A在執(zhí)行writer()方法,線程B執(zhí)行reader()方法。

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

寫final域的重排序規(guī)則:禁止對final域的寫重排序到構(gòu)造函數(shù)之外,這個規(guī)則的實現(xiàn)主要包含了兩個方面:

JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外

編譯器會在final域?qū)懼螅瑯?gòu)造函數(shù)return之前,插入一個StoreStore屏障。 這個屏障可以禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外。 (參見 StoreStore Barriers的說明:在Store1;Store2之間插入StoreStore,確保Store1對 其他處理器可見(刷新內(nèi)存)先于Store2及所有后續(xù)存儲指令的存儲)

writer方法中,實際上做了兩件事:

構(gòu)造了一個FinalDemo對象

把這個對象賦值給成員變量finalDemo

可能的執(zhí)行時序圖如下:


a,b之間沒有數(shù)據(jù)依賴性,普通域(普通變量)a可能會被重排序到構(gòu)造函數(shù)之外, 線程B就有可能讀到的是普通變量a初始化之前的值(零值),這樣就可能出現(xiàn)錯誤。

final域變量b,根據(jù)重排序規(guī)則,會禁止final修飾的變量b重排序到構(gòu)造函數(shù)之外,從而b能夠正確賦值, 線程B就能夠讀到final變量初始化后的值。

因此,寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前,對象的final域已經(jīng)被正確初始化過了。 普通域不具有這個保障,比如在上例,線程B有可能就是一個未正確初始化的對象finalDemo。

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

讀final域重排序規(guī)則:在一個線程中,初次讀對象引用和初次讀該對象包含的final域,JMM會禁止這兩個操作的重排序。 (注意,這個規(guī)則僅僅是針對處理器), 處理器會在讀final域操作的前面插入一個LoadLoad屏障。 實際上,讀對象的引用和讀該對象的final域存在間接依賴性,一般處理器不會重排序這兩個操作。 但是有一些處理器會重排序,因此,這條禁止重排序規(guī)則就是針對這些處理器而設定的。

read方法主要包含了三個操作:

初次讀引用變量finalDemo

初次讀引用變量finalDemo的普通域a

初次讀引用變量finalDemo的final域b

假設線程A寫過程沒有重排序,那么線程A和線程B有一種的可能執(zhí)行時序如下:


讀對象的普通域被重排序到了讀對象引用的前面就會出現(xiàn)線程B還未讀到對象引用就在讀取該對象的普通域變量,這顯然是錯誤的操作。

final域的讀操作就“限定”了在讀final域變量前已經(jīng)讀到了該對象的引用,從而就可以避免這種情況。

因此,讀final域的重排序規(guī)則可以確保:在讀一個對象的final域之前,一定會先讀這個包含這個final域的對象的引用。

final為引用類型
public class FinalReferenceDemo {    final int[] arrays; //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
        }
    }
}

對final修飾的對象的成員域進行寫操作

針對引用數(shù)據(jù)類型,final域?qū)戓槍幾g器和處理器重排序增加了這樣的約束: 在構(gòu)造函數(shù)內(nèi)對一個final修飾的對象的成員域的寫入,與隨后在構(gòu)造函數(shù)之外把這個被構(gòu)造的對象的引用賦給一個引用變量,這兩個操作是不能被重排序的。 注意這里的是“增加”也就說前面對final基本數(shù)據(jù)類型的重排序規(guī)則在這里還是使用。

線程線程A執(zhí)行wirterOne方法,執(zhí)行完后線程B執(zhí)行writerTwo方法,線程C執(zhí)行reader方法。 下圖就以這種執(zhí)行時序出現(xiàn)的一種情況來討論:


對final域的寫禁止重排序到構(gòu)造方法外,因此1和3不能被重排序。 由于一個final域的引用對象的成員域?qū)懭氩荒芘c在構(gòu)造函數(shù)之外將這個被構(gòu)造出來的對象賦給引用變量重排序, 因此2和3不能重排序。

對final修飾的對象的成員域進行讀操作

JMM可以確保線程C至少能看到寫線程A對final引用的對象的成員域的寫入,即能看到arrays[0] = 1,而 寫線程B對數(shù)組元素的寫入可能看到可能看不到。 JMM不保證線程B的寫入對線程C可見,線程B和線程C之間存在數(shù)據(jù)競爭,此時的結(jié)果是不可預知的。 如果想要可見,可使用鎖或者volatile。

final重排序的總結(jié)


final寫final域讀
基本數(shù)據(jù)類型禁止final域?qū)懪c構(gòu)造方法重排序,即禁止final域?qū)懼嘏判虻綐?gòu)造方法之外,從而保證該對象對所有線程可見時,該對象的final域全部已經(jīng)初始化過禁止初次讀對象的引用與讀該對象包含的final域的重排序,保證了在讀一個對象的final域之前,一定會先讀這個包含這個final域的對象的引用
引用數(shù)據(jù)類型額外增加約束:構(gòu)造函數(shù)內(nèi)對一個final修飾的對象的成員域的寫入,與隨后在構(gòu)造函數(shù)之外把這個被構(gòu)造的對象的引用賦給一個引用變量,這兩個操作是不能被重排序的
對象溢出

對象溢出:一種錯誤的發(fā)布,當一個對象還沒有構(gòu)造完成時,就使它被其他線程所見。

/*** 對象溢出示例*/public class ThisEscape {
  public ThisEscape(EventSource source) {
    source.registerListener(new EventListener() {
      public void onEvent(Event e) {
        doSomething(e);
      }
    });
  }
 
  void doSomething(Event e) {
  }
}

這將導致this逸出,所謂逸出,就是在不該發(fā)布的時候發(fā)布了一個引用。

在這個例子里面,當我們實例化ThisEscape對象時,會調(diào)用source的registerListener方法, 這時便啟動了一個線程,而且這個線程持有了ThisEscape對象(調(diào)用了對象的doSomething方法), 但此時ThisEscape對象卻沒有實例化完成(還沒有返回一個引用),所以我們說, 此時造成了一個this引用逸出,即還沒有完成的實例化ThisEscape對象的動作,卻已經(jīng)暴露了對象的引用。

正確構(gòu)造過程:

public class SafeListener {
  private final EventListener listener;
 
  private SafeListener() {
    listener = new EventListener() {
      public void onEvent(Event e) {
        doSomething(e);
      }
    };
  }
 
  public static SafeListener newInstance(EventSource source) {
    SafeListener safe = new SafeListener();
    source.registerListener(safe.listener);
    return safe;
  }
 
  void doSomething(Event e) {
  }

當構(gòu)造好了SafeListener對象(通過構(gòu)造器構(gòu)造)之后, 我們才啟動了監(jiān)聽線程,也就確保了SafeListener對象是構(gòu)造完成之后再使用的SafeListener對象。

結(jié)論:

只有當構(gòu)造函數(shù)返回時,this引用才應該從線程中逸出。

構(gòu)造函數(shù)可以將this引用保存到某個地方,只要其他線程不會在構(gòu)造函數(shù)完成之前使用它。

final的實現(xiàn)原理

寫final域會要求編譯器在final域?qū)懼?,?gòu)造函數(shù)返回前插入一個StoreStore屏障。

讀final域的重排序規(guī)則會要求編譯器在讀final域的操作前插入一個LoadLoad屏障。

如果以X86處理為例,X86不會對寫-寫重排序,所以StoreStore屏障可以省略。 由于不會對有間接依賴性的操作重排序,所以在X86處理器中,讀final域需要的LoadLoad屏障也會被省略掉。 也就是說,以X86為例的話,對final域的讀/寫的內(nèi)存屏障都會被省略!具體是否插入還是得看是什么處理器。

注意:

上面對final域?qū)懼嘏判蛞?guī)則可以確保我們在使用一個對象引用的時候該對象的final域已經(jīng)在構(gòu)造函數(shù)被初始化過了。 但是這里其實是有一個前提條件: 在構(gòu)造函數(shù),不能讓這個被構(gòu)造的對象被其他線程可見,也就是說該對象引用不能在構(gòu)造函數(shù)中“溢出”。

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
        }
    }
}


假設一個線程A執(zhí)行writer方法另一個線程執(zhí)行reader方法。因為構(gòu)造函數(shù)中操作1和2之間沒有數(shù)據(jù)依賴性,1和2可以重排序,先執(zhí)行了2,這個時候引用對象referenceDemo是個沒有完全初始化的對象,而當線程B去讀取該對象時就會出錯。盡管依然滿足了final域?qū)懼嘏判蛞?guī)則:在引用對象對所有線程可見時,其final域已經(jīng)完全初始化成功。 但是,引用對象“this”逸出,該代碼依然存在線程安全的問題。



文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/6937.html

相關(guān)文章

  • java 基礎 - 收藏集 - 掘金

    摘要:基礎知識復習后端掘金的作用表示靜態(tài)修飾符,使用修飾的變量,在中分配內(nèi)存后一直存在,直到程序退出才釋放空間。將對象編碼為字節(jié)流稱之為序列化,反之將字節(jié)流重建成對象稱之為反序列化。 Java 學習過程|完整思維導圖 - 后端 - 掘金JVM 1. 內(nèi)存模型( 內(nèi)存分為幾部分? 堆溢出、棧溢出原因及實例?線上如何排查?) 2. 類加載機制 3. 垃圾回收 Java基礎 什么是接口?什么是抽象...

    makeFoxPlay 評論0 收藏0
  • Java面試,深入理解final關(guān)鍵字

    摘要:寫域重排序規(guī)則寫域的重排序規(guī)則禁止對域的寫重排序到構(gòu)造函數(shù)之外,這個規(guī)則的實現(xiàn)主要包含了兩個方面禁止編譯器把域的寫重排序到構(gòu)造函數(shù)之外編譯器會在域?qū)懼?,?gòu)造函數(shù)之前,插入一個屏障。結(jié)論只有當構(gòu)造函數(shù)返回時,引用才應該從線程中逸出。final關(guān)鍵字final的簡介final可以修飾變量,方法和類,用于表示所修飾的內(nèi)容一旦賦值之后就不會再被改變,比如String類就是一個final類型的類。f...

    番茄西紅柿 評論0 收藏0
  • Java面試深入理解final關(guān)鍵字

    摘要:寫域重排序規(guī)則寫域的重排序規(guī)則禁止對域的寫重排序到構(gòu)造函數(shù)之外,這個規(guī)則的實現(xiàn)主要包含了兩個方面禁止編譯器把域的寫重排序到構(gòu)造函數(shù)之外編譯器會在域?qū)懼?,?gòu)造函數(shù)之前,插入一個屏障。結(jié)論只有當構(gòu)造函數(shù)返回時,引用才應該從線程中逸出。final關(guān)鍵字final的簡介final可以修飾變量,方法和類,用于表示所修飾的內(nèi)容一旦賦值之后就不會再被改變,比如String類就是一個final類型的類。f...

    番茄西紅柿 評論0 收藏0
  • 后臺開發(fā)常問面試集錦(問搬運工,附鏈接)

    摘要:基礎問題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線抽象關(guān)鍵字修飾符知識點總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機制解讀抽象類與三大特征時間和時間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類對象鎖和類鎖的區(qū)別,,優(yōu)缺點及比較提高篇八詳解內(nèi)部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...

    spacewander 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<