乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      面試???--- volatile

       貪挽懶月 2022-06-20 發(fā)布于廣東

      一、volatile簡(jiǎn)介

      在單線程環(huán)境中,我們幾乎用不到這個(gè)關(guān)鍵詞,但是多線程環(huán)境中,這個(gè)關(guān)鍵詞隨處可見。而且也是面試的常客??偟膩碚f,volatile有以下三個(gè)特性:

      • 保證可見性;

      • 不保證原子性;

      • 禁止指令重排。

      下面就來詳細(xì)的說說這三個(gè)特性。

      二、保證可見性

      1、什么是可見性?
      在說volatile保證可見性之前,先來說說什么叫可見性。談到可見性,又不得不說JMM(java memory model)內(nèi)存模型。JMM內(nèi)存模型是邏輯上的劃分,并不是真實(shí)存在。Java線程之間的通信就由JMM控制。JMM的抽象示意圖如下:

      JMM內(nèi)存模型

      如上圖所示,我們定義的共享變量,是存儲(chǔ)在主內(nèi)存中的,也就是計(jì)算機(jī)的內(nèi)存條中。線程A去操作共享變量的時(shí)候,并不能直接操作主內(nèi)存中的值,而是將主內(nèi)存中的值拷貝回自己的工作內(nèi)存中,在工作內(nèi)存中做修改。修改好后,再將值刷回到主內(nèi)存中。

      假設(shè)現(xiàn)在new 一個(gè) student , age為 18,這個(gè)18是存儲(chǔ)在主內(nèi)存中的?,F(xiàn)在兩個(gè)線程先將18拷貝回自己的工作內(nèi)存中。這時(shí),A線程將18改為了20,刷回到主內(nèi)存中。也就是說,現(xiàn)在主內(nèi)存中的值變?yōu)榱?0。可是,B線程并不知道現(xiàn)在主內(nèi)存中的值變了,因?yàn)锳線程所做的操作對(duì)B是不可見的。我們需要一種機(jī)制,即一旦主內(nèi)存中的值發(fā)生改變,就及時(shí)地通知所有的線程,保證他們對(duì)這個(gè)變化可見。這就是可見性。我們通常用happen - before(先行發(fā)生原則),來闡述操作之間內(nèi)存的可見性。也就是前一個(gè)的操作結(jié)果對(duì)后一個(gè)操作可見,那么這兩個(gè)操作就存在 happen - before 規(guī)則。

      2、為什么volatile能保證可見性?
      先來說一說內(nèi)存屏障(memory barrier),這是一條CPU指令,可以影響數(shù)據(jù)的可見性。當(dāng)變量用volatile修飾時(shí),將會(huì)在寫操作的后面加一條屏障指令,在讀操作的前面加一條屏障指令。這樣的話,一旦你寫入完成,可以保證其他線程讀到最新值,也就保證了可見性。

      3、驗(yàn)證volatile保證可見性。
      驗(yàn)證volatile可見性和不保證原子性的代碼:

       1// 驗(yàn)證可見性
      2class MyData {
      3    //int number = 0; // 沒加volatile關(guān)鍵字
      4    volatile int number = 0;
      5    int changeNumber({
      6        return this.number = 60;
      7    }
      8}
      9
      10public class VolatileTest {
      11    // 驗(yàn)證可見性
      12    public static void main(String[] args{
      13        MyData myData = new MyData();
      14        new Thread("AAA") {
      15            public void run({
      16                try {
      17                    Thread.sleep(3000);
      18                    // 睡3秒后調(diào)用changeNumber方法將number改為60
      19                    System.err.println(Thread.currentThread().getName() 
      20                                        +  " update number to " + myData.changeNumber());
      21                } catch (InterruptedException e) {
      22                    e.printStackTrace();
      23                }
      24            };
      25        }.start();
      26        // 主線程
      27        while (myData.number == 0) {
      28        }
      29        // 如果主線程讀取到的一直都是最開始的0,
      30        //將造成死循環(huán),這句話將無法輸出
      31        System.err.println(Thread.currentThread().getName() 
      32                          + " get number value is " + myData.number);
      33    }
      34}

      上面這段代碼很簡(jiǎn)單,定義了一個(gè)MyData類,初始一個(gè)number,值為0。然后在main方法中創(chuàng)建另一個(gè)線程,將其值改為60。但是,這個(gè)線程對(duì)number所作的操作對(duì)main線程是不可見的,所以main線程以為number還是0,因此,將會(huì)造成死循環(huán)。如果number加了volatile修飾,main線程就可以獲取到主內(nèi)存中的最新值,就不會(huì)死循環(huán)。這就驗(yàn)證了volatile可以保證可見性。

      三、不保證原子性

      1、什么叫原子性?
      所謂原子性,就是說一個(gè)操作不可被分割或加塞,要么全部執(zhí)行,要么全不執(zhí)行。

      2、volatile不保證原子性解析
      java程序在運(yùn)行時(shí),JVM將java文件編譯成了class文件。我們使用javap命令對(duì)class文件進(jìn)行反匯編,就可以查看到j(luò)ava編譯器生成的字節(jié)碼。最常見的 i++ 問題,其實(shí) 反匯編后是分三步進(jìn)行的。

      • 第一步:將i的初始值裝載進(jìn)工作內(nèi)存;

      • 第二步:在自己的工資內(nèi)存中進(jìn)行自增操作;

      • 第三步:將自己工作內(nèi)存的值刷回到主內(nèi)存。

      我們知道線程的執(zhí)行具有隨機(jī)性,假設(shè)現(xiàn)在i的初始值為0,有A和B兩個(gè)線程對(duì)其進(jìn)行++操作。首先兩個(gè)線程將0拷貝到自己工作內(nèi)存,當(dāng)線程A在自己工作內(nèi)存中進(jìn)行了自增變成了1,還沒來得及把1刷回到主內(nèi)存,這是B線程搶到CPU執(zhí)行權(quán)了。B將自己工作內(nèi)存中的0進(jìn)行自增,也變成了1。然后線程A將1刷回主內(nèi)存,主內(nèi)存此時(shí)變成了1,然后B也將1刷回主內(nèi)存,主內(nèi)存中的值還是1。本來A和B都對(duì)i進(jìn)行了一次自增,此時(shí)主內(nèi)存中的值應(yīng)該是2,而結(jié)果是1,出現(xiàn)了寫丟失的情況。這是因?yàn)閕++本應(yīng)該是一個(gè)原子操作,但是卻被加塞了其他操作。所以說volatile不保證原子性。

      3、volatile不保證原子性驗(yàn)證

       1    // 驗(yàn)證volatile不保證原子性
      2    void  addPlusPlus({
      3        this.number++;
      4    }
      5    // 驗(yàn)證volatile不保證原子性
      6    public static void main(String[] args{
      7        MyData mydata2 = new MyData();
      8        for(int i = 0; i < 20; i ++ ) { // 創(chuàng)建20個(gè)線程
      9            new Thread("線程" + i) {
      10                public void run({
      11                    try {
      12                        for(int j = 0; j < 1000; j++) {
      13                            mydata2.addPlusPlus();// 每個(gè)線程執(zhí)行1000次number++
      14                        }
      15                    } catch (Exception e) {
      16                        e.printStackTrace();
      17                    }
      18                };
      19            }.start();
      20        }
      21        // 保證上面的線程執(zhí)行完main線程再輸出結(jié)果。 大于2,因?yàn)槟J(rèn)有main線程和gc線程
      22        while(Thread.activeCount() > 2) {
      23            Thread.yield();
      24        }
      25        System.err.println(Thread.currentThread().getName() + " obtain the number is " + mydata2.number);
      26    }

      同樣是上面的MyData類,有一個(gè)volatile修飾的number變量初始值為0?,F(xiàn)在有20個(gè)線程,每個(gè)線程對(duì)其執(zhí)行1000次++操作。理論上執(zhí)行完后,main線程輸出的結(jié)果是20000,但是運(yùn)行之后會(huì)發(fā)現(xiàn),每次的運(yùn)行結(jié)果都會(huì)小于20000,這就是因?yàn)槌霈F(xiàn)了寫丟失的情況。
      解決辦法:

      • 第一種:可以在addPlusPlus方法中加synchronized;

      • 第二種:可以使用原子包裝類AtomicInteger。

      第一種辦法不太好,因?yàn)閟ynchronized太重量級(jí)了,整個(gè)操作都加鎖了。第二種辦法更好。但是為什么AtomicInteger就可以保證原子性呢?因?yàn)樗褂昧薈AS算法。什么是CAS?后續(xù)我再專門寫一篇介紹CAS的文章。

      三、禁止指令重排

      1、什么叫指令重排?
      上面說了,使用javap命令可以對(duì)class文件進(jìn)行反匯編,查看到程序底層到底是如何執(zhí)行的。像 i++ 這樣一個(gè)簡(jiǎn)單的操作,底層就分三步執(zhí)行。在多線程情況下,計(jì)算機(jī)為了提高執(zhí)行效率,就會(huì)對(duì)這些步驟進(jìn)行重排序,這就叫指令重排。比如現(xiàn)有如下代碼:

      1int x = 1;
      2int y = 2;
      3x = x + 3;
      4y = x - 4;

      這四條語句,正常的執(zhí)行順序是從上往下1234這樣執(zhí)行,x的結(jié)果應(yīng)該是4,y的結(jié)果應(yīng)該是0。但是在多線程環(huán)境中,編譯器指令重排后,執(zhí)行順序可能就變成了1243,這樣得出的x就是4,y就是-3,這結(jié)果顯然就不正確了。不過編譯器在重排的時(shí)候也會(huì)考慮數(shù)據(jù)的依賴性,比如執(zhí)行順序不可能為2413,因?yàn)榈?條語句的執(zhí)行是依賴x的。使用volatile修飾,就可以禁止指令重排。

      四、你在哪些地方使用過volatile?

      最經(jīng)典的就是單例模式。

      • 最簡(jiǎn)版單例模式:

       1public class SingletonDemo {
      2    private static SingletonDemo  singletonDemo = null;
      3    private SingletonDemo(){
      4        System.err.println("構(gòu)造方法被執(zhí)行");
      5    }
      6    public static SingletonDemo getInstance(){
      7        if (singletonDemo == null){
      8            singletonDemo = new SingletonDemo();
      9        }
      10        return singletonDemo;
      11    }
      12}

      這是我們最開始學(xué)的時(shí)候?qū)懙膯卫J?。看似很完美。其?shí)多線程環(huán)境中就會(huì)出問題。測(cè)試一下:

      1public static void main(String[] args){
      2        for (int i = 0; i <= 10; i++){
      3           new Thread(() -> SingletonDemo.getInstance()).start();
      4        }
      5}

      10個(gè)線程去執(zhí)行這個(gè)單例,看結(jié)果:

      運(yùn)行結(jié)果

      構(gòu)造方法被執(zhí)行這句話打印了兩次,說明創(chuàng)建了兩次對(duì)象。所以在多線程環(huán)境中這個(gè)單例模式是有問題的??梢栽趃etInstance方法上加synchronized,但是,這樣就把一整個(gè)方法都鎖了,這樣不太好。下面介紹另一種方式。
      • DCL版單例模式:
        DCL,是double check lock 的縮寫,中文名叫雙端檢索機(jī)制。所謂雙端檢索,就是在加鎖前和加鎖后都用進(jìn)行一次判斷。代碼如下:

       1public class SingletonDemo {
      2    private static SingletonDemo  singletonDemo = null;
      3    private SingletonDemo(){
      4        System.err.println("構(gòu)造方法被執(zhí)行");
      5    }
      6    public static SingletonDemo getInstance(){
      7        if (singletonDemo == null){ // 第一次check
      8            synchronized (SingletonDemo.class){
      9                if (singletonDemo == null// 第二次check
      10                    singletonDemo = new SingletonDemo();
      11            }
      12        }
      13        return singletonDemo;
      14    }
      15 }

      用synchronized只鎖住創(chuàng)建實(shí)例那部分代碼,而不是整個(gè)方法。在加鎖前和加鎖后都進(jìn)行判斷,這就叫雙端檢索機(jī)制。經(jīng)測(cè)試,這樣確實(shí)只創(chuàng)建了一個(gè)對(duì)象。但是,這也并非絕對(duì)安全。new 一個(gè)對(duì)象也是分三步的:

      • 1.分配對(duì)象內(nèi)存空間;(這個(gè)房間有人訂了)

      • 2.初始化對(duì)象;(打掃好房間)

      • 3.將對(duì)象指向分配的內(nèi)存地址,此時(shí)這個(gè)對(duì)象不為null。(訂房間的人入住)

      步驟二和步驟三不存在數(shù)據(jù)依賴,因此編譯器優(yōu)化時(shí)允許這兩句顛倒順序。當(dāng)指令重拍后,多線程去訪問也會(huì)出問題。所以便有了如下的最終版單例模式。

      • 最終版單例模式:
        既然說到DCL版可能會(huì)出現(xiàn)指令重排的現(xiàn)象,所以最終版就是加上volatile。

       1public class SingletonDemo {
      2    private static volatile SingletonDemo  singletonDemo = null;
      3    private SingletonDemo(){
      4        System.err.println("構(gòu)造方法被執(zhí)行");
      5    }
      6    public static SingletonDemo getInstance(){
      7        if (singletonDemo == null){ // 第一次check
      8            synchronized (SingletonDemo.class){
      9                if (singletonDemo == null// 第二次check
      10                    singletonDemo = new SingletonDemo();
      11            }
      12        }
      13        return singletonDemo;
      14    }
      15}

      總結(jié):

      1、volatile特性:

      • 可見性

      • 不保證原子性

      • 禁止指令重排

      2、volatile的應(yīng)用:
      最經(jīng)典的就是單例模式。

        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評(píng)論

        發(fā)表

        請(qǐng)遵守用戶 評(píng)論公約

        類似文章 更多