欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java基礎(chǔ)面試題之volatile詳解

 更新時間:2022年06月20日 10:01:38   作者:koping_wu  
Volatile可以看做是輕量級的 Synchronized,它只保證了共享變量的可見性,下面這篇文章主要給大家介紹了關(guān)于Java基礎(chǔ)面試題之volatile的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下

問:請談談你對volatile的理解?

答:volatile是Java虛擬機提供的輕量級的同步機制,它有3個特性:

1)保證可見性

2)不保證原子性

3)禁止指令重排

剛學完java基礎(chǔ),如果有人問你什么是volatile?它有什么作用的話,相信一定非常懵逼…

可能看了答案,也完全不明白,什么是同步機制?什么是可見性?什么是原子性?什么是指令重排?

1、volatile保證可見性

1.1、什么是JMM模型?

要想理解什么是可見性,首先要先理解JMM。

JMM(Java內(nèi)存模型,Java Memory Model)本身是一種抽象的概念,并不真實存在。它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范,定了程序中各個變量的訪問方法。JMM關(guān)于同步的規(guī)定:

1)線程解鎖前,必須把共享變量的值刷新回主內(nèi)存;

2)線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存;

3)加鎖解鎖是同一把鎖;

由于JVM運行程序的實體是線程,創(chuàng)建每個線程時,JMM會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為棧空間),工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域。

Java內(nèi)存模型規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問。

但線程對變量的操作(讀取、賦值等)必須在工作內(nèi)存中進行。因此首先要將變量從主內(nèi)存拷貝到自己的工作內(nèi)存,然后對變量進行操作,操作完成后再將變量寫會主內(nèi)存中。

看了上面對JMM的介紹,可能還是優(yōu)點懵,接下來用一個賣票系統(tǒng)來進行舉例:

1)如下圖,此時賣票系統(tǒng)后端只剩下1張票,并已讀入主內(nèi)存中:ticketNum=1。

2)此時網(wǎng)絡上有多個用戶都在搶票,那么此時就有多個線程同時都在進行買票服務,假設(shè)此時有3個線程都讀入了目前的票數(shù):ticketNum=1,那么接著就會買票。

3)假設(shè)線程1先搶占到cpu的資源,先買好票,并在自己的工作內(nèi)存中將ticketNum的值改為0:ticketNum=0,然后再寫回到主內(nèi)存中。

此時,線程1的用戶已經(jīng)買到票了,那么線程2,線程3此時應該不能再繼續(xù)買票了,因此需要系統(tǒng)通知線程2,線程3,ticketNum此時已經(jīng)等于0了:ticketNum=0。如果有這樣的通知操作,你就可以理解為就具有可見性。

通過上面對JMM的介紹和舉例,可以簡單總結(jié)下。

JMM內(nèi)存模型的可見性是指,多線程訪問主內(nèi)存的某一個資源時,如果某一個線程在自己的工作內(nèi)存中修改了該資源,并寫回主內(nèi)存,那么JMM內(nèi)存模型應該要通知其他線程來從新獲取最新的資源,來保證最新資源的可見性。

1.2、volatile保證可見性的代碼驗證

在1.1中,已經(jīng)基本理解了可見性的含義,接下來用代碼來驗證一下,volatile確實可以保證可見性。

1.2.1、無可見性代碼驗證

首先先驗證下,不使用volatile,是不是就是沒有可見性。

package com.koping.test;

import java.util.concurrent.TimeUnit;

class MyData
{
    int number = 0;

    public void add10() {
        this.number += 10;
    }
}

public class VolatileVisibilityDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 啟動一個線程修改myData的number,將number的值加10
        new Thread(
                () -> {
                    System.out.println("線程" + Thread.currentThread().getName()+"\t 正在執(zhí)行");
                    try{
                        TimeUnit.SECONDS.sleep(3);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    myData.add10();
                    System.out.println("線程" + Thread.currentThread().getName()+"\t 更新后,number的值為" + myData.number);
                }
        ).start();

        // 看一下主線程能否保持可見性
        while (myData.number == 0) {
            // 當上面的線程將number加10后,如果有可見性的話,那么就會跳出循環(huán);
            // 如果沒有可見性的話,就會一直在循環(huán)里執(zhí)行
        }
        System.out.println("具有可見性!");
    }
}

運行結(jié)果如下圖,可以看到雖然線程0已經(jīng)將number的值改為了10,但是主線程還是在循環(huán)中,因為此時number不具有可見性,系統(tǒng)不會主動通知。

1.2.1、volatile保證可見性驗證

在上面代碼的第7行給變量number添加volatile后再次測試,如下圖,此時主線程成功退出了循環(huán),因為JMM主動通知了主線程更新number的值了,number已經(jīng)不為0了。

2、volatile不保證原子性

2.1 什么是原子性?

理解了上面說的可見性之后,再來理解下什么叫原子性?

原子性是指不可分隔,完整性,即某個線程正在做某個業(yè)務時,中間不能被分割。要么同時成功,要么同時失敗。

還是有點抽象,接下來舉個例子。

如下圖,創(chuàng)建了一個測試原子性的類:TestPragma。在add方法中將n加1,通過查看編譯后的代碼可以看到,n++被拆分為3個指令進行執(zhí)行。

因此可能存在線程1正在執(zhí)行第1個指令,緊接著線程2也正在執(zhí)行第1個指令,這樣當線程1和線程2都執(zhí)行完3個指令之后,很容易理解,此時n的值只加了1,而實際是有2個線程加了2次,因此這種情況就是不保證原子性。

2.2 不保證原子性的代碼驗證

在2.1中已經(jīng)進行了舉例,可能存在2個線程執(zhí)行n++的操作,但是最終n的值卻只加了1的情況,接下來對這種情況再用代碼進行演示下。

首先給MyData類添加一個add方法

package com.koping.test;

class MyData {
    volatile int number = 0;

    public void add() {
        number++;
    }
}

然后創(chuàng)建測試原子性的類:TestPragmaDemo。測試下20個線程給number各加1000次之后,number的值是否是20000。

package com.koping.test;

public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 啟動20個線程,每個線程將myData的number值加1000次,那么理論上number值最終是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序運行時,模型會有主線程和守護線程。如果超過2個,那就說明上面的20個線程還有沒執(zhí)行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println("number值加了20000次,此時number的實際值是:" + myData.number);
    }
}

運行結(jié)果如下圖,最終number的值僅為18410。

可以看到即使加了volatile,依然不保證有原子性。

2.3 volatile不保證原子性的解決方法

上面介紹并證明了volatile不保證原子性,那如果希望保證原子性,怎么辦呢?以下提供了2種方法

2.3.1 方法1:使用synchronized

方法1是在add方法上添加synchronized,這樣每次只有1個線程能執(zhí)行add方法。

結(jié)果如下圖,最終確實可以使number的值為20000,保證了原子性。

但是,實際業(yè)務邏輯方法中不可能只有只有number++這1行代碼,上面可能還有n行代碼邏輯。現(xiàn)在為了保證number的值是20000,就把整個方法都加鎖了(其實另外那n行代碼,完全可以由多線程同時執(zhí)行的)。所以就優(yōu)點殺雞用牛刀,高射炮打蚊子,小題大做了。

package com.koping.test;

class MyData {
    volatile int number = 0;

    public synchronized void add() {
      // 在n++上面可能還有n行代碼進行邏輯處理
        number++;
    }
}

2.3.2 方法1:使用JUC包下的AtomicInteger

給MyData新曾一個原子整型類型的變量num,初始值為0。

package com.koping.test;

import java.util.concurrent.atomic.AtomicInteger;

class MyData {
    volatile int number = 0;

    volatile AtomicInteger num = new AtomicInteger();

    public void add() {
        // 在n++上面可能還有n行代碼進行邏輯處理
        number++;
        num.getAndIncrement();
    }
}

讓num也同步加20000次。結(jié)果如下圖,可以看到,使用原子整型的num可以保證原子性,也就是number++的時候不會被搶斷。

package com.koping.test;

public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 啟動20個線程,每個線程將myData的number值加1000次,那么理論上number值最終是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序運行時,模型會有主線程和守護線程。如果超過2個,那就說明上面的20個線程還有沒執(zhí)行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此時number的實際值是:" + myData.number);
        System.out.println("num值加了20000次,此時number的實際值是:" + myData.num);
    }
}

3、volatile禁止指令重排

3.1 什么是指令重排?

在第2節(jié)中理解了什么是原子性,現(xiàn)在要理解下什么是指令重排?

計算機在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令進行重排:
源代碼–>編譯器優(yōu)化重排–>指令并行重排–>內(nèi)存系統(tǒng)重排–>最終執(zhí)行指令

處理器在進行重排時,必須要考慮指令之間的數(shù)據(jù)依賴性。

單線程環(huán)境中,可以確保最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致。

但是多線程環(huán)境中,線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在,兩個線程使用的變量能否保持一致性是無法確定的,結(jié)果無法預測。

看了上面的文字性表達,然后看一個很簡單的例子。

比如下面的mySort方法,在系統(tǒng)指令重排后,可能存在以下3種語句的執(zhí)行情況:

1)1234

2)2134

3)1324

以上這3種重排結(jié)果,對最后程序的結(jié)果都不會有影響,也考慮了指令之間的數(shù)據(jù)依賴性。

public void mySort() {
    int x = 1;  // 語句1
    int y = 2;  // 語句2
    x = x + 3;  // 語句3
    y = x * x;  // 語句4
}

3.2 單線程單例模式

看完指令重排的簡單介紹后,然后來看下單例模式的代碼。

package com.koping.test;
public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 執(zhí)行構(gòu)造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 單線程測試
        System.out.println("單線程的情況測試開始");
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println("單線程的情況測試結(jié)束\n");
    }
}

首先是在單線程情況下進行測試,結(jié)果如下圖??梢钥吹?,構(gòu)造方法只執(zhí)行了一次,是沒有問題的。

3.3 多線程單例模式

接下來在多線程情況下進行測試,代碼如下。

package com.koping.test;

public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 執(zhí)行構(gòu)造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }

        // DCL(Double Check Lock雙端檢索機制)
//        if (instance == null) {
//            synchronized (SingletonDemo.class) {
//                if (instance == null) {
//                    instance = new SingletonDemo();
//                }
//            }
//        }
        return instance;
    }

    public static void main(String[] args) {
        // 單線程測試
//        System.out.println("單線程的情況測試開始");
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println("單線程的情況測試結(jié)束\n");

        // 多線程測試
        System.out.println("多線程的情況測試開始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

在多線程情況下的運行結(jié)果如下圖??梢钥吹?,多線程情況下,出現(xiàn)了構(gòu)造方法執(zhí)行了2次的情況。

3.4 多線程單例模式改進:DCL

在3.3中的多線程單里模式下,構(gòu)造方法執(zhí)行了兩次,因此需要進行改進,這里使用雙端檢鎖機制:Double Check Lock, DCL。即加鎖之前和之后都進行檢查。

package com.koping.test;

public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 執(zhí)行構(gòu)造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
//        if (instance == null) {
//            instance = new SingletonDemo();
//        }

        // DCL(Double Check Lock雙端檢鎖機制)
        if (instance == null) {  // a行
            synchronized (SingletonDemo.class) {
                if (instance == null) {  // b行
                    instance = new SingletonDemo();  // c行
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        // 單線程測試
//        System.out.println("單線程的情況測試開始");
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println("單線程的情況測試結(jié)束\n");

        // 多線程測試
        System.out.println("多線程的情況測試開始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

在多次運行后,可以看到,在多線程情況下,此時構(gòu)造方法也只執(zhí)行1次了。

3.5 多線程單例模式改進,DCL版存在的問題

需要注意的是3.4中的DCL版的單例模式依然不是100%準確的?。。?/p>

是不是不太明白為什么3.4DCL版單例模式不是100%準確的原因?

是不是不太明白在3.1講完指令重排的簡單理解后,為什么突然要講多線程的單例模式?

因為3.4DCL版單例模式可能會由于指令重排而導致問題,雖然該問題出現(xiàn)的可能性可能是千萬分之一,但是該代碼依然不是100%準確的。如果要保證100%準確,那么需要添加volatile關(guān)鍵字,添加volatile可以禁止指令重排。

接下來分析下,為什么3.4DCL版單例模式不是100%準確?

查看instance = new SingletonDemo();編譯后的指令,可以分為以下3步:

1)分配對象內(nèi)存空間:memory = allocate();

2)初始化對象:instance(memory);

3)設(shè)置instance指向分配的內(nèi)存地址:instance = memory;

由于步驟2和步驟3不存在數(shù)據(jù)依賴關(guān)系,因此可能出現(xiàn)執(zhí)行132步驟的情況。

比如線程1執(zhí)行了步驟13,還沒有執(zhí)行步驟2,此時instance!=null,但是對象還沒有初始化完成;

如果此時線程2搶占到cpu,然后發(fā)現(xiàn)instance!=null,然后直接返回使用,就會發(fā)現(xiàn)instance為空,就會出現(xiàn)異常。

這就是指令重排可能導致的問題,因此要想保證程序100%正確就需要加volatile禁止指令重排。

3.6 volatile保證禁止指令重排的原理

在3.1中簡單介紹了下執(zhí)行重排的含義,然后通過3.2-3.5,借助單例模式來舉例說明多線程情況下,為什么要使用volatile的原因,因為可能存在指令重排導致程序異常。

接下來就介紹下volatile能保證禁止指令重排的原理。

首先要了解一個概念:內(nèi)存屏障(Memory Barrier),又稱為內(nèi)存柵欄。它是一個CPU指令,有2個作用:

1)保證特定操作的執(zhí)行順序;

2)保證某些變量的內(nèi)存可見性;

由于編譯器和處理器都能執(zhí)行指令重排。如果在指令之間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說,通過插入內(nèi)存屏障,禁止在內(nèi)存屏障前后的指令執(zhí)行重排需優(yōu)化。

內(nèi)存屏障的另一個作用是強制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。

總結(jié)

到此這篇關(guān)于Java基礎(chǔ)面試題之volatile詳解的文章就介紹到這了,更多相關(guān)Java volatile詳解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 淺談Java的LinkedHashSet源碼

    淺談Java的LinkedHashSet源碼

    這篇文章主要介紹了淺談Java的LinkedHashSet源碼,底層是鏈表實現(xiàn)的,是set集合中唯一一個能保證怎么存就怎么取的集合對象
    因為是HashSet的子類,所以也是保證元素唯一的,與HashSet的原理一樣,需要的朋友可以參考下
    2023-09-09
  • idea工具欄如何添加快捷圖標的操作

    idea工具欄如何添加快捷圖標的操作

    這篇文章主要介紹了idea工具欄如何添加快捷圖標的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • MyBatis中常見的SQL執(zhí)行方式及其使用方法

    MyBatis中常見的SQL執(zhí)行方式及其使用方法

    MyBatis可能很多人都一直在用,但是MyBatis的SQL執(zhí)行流程可能并不是所有人都清楚了,下面這篇文章主要給大家介紹了關(guān)于MyBatis中常見的SQL執(zhí)行方式及其使用的相關(guān)資料,需要的朋友可以參考下
    2023-09-09
  • 指定springboot的jar運行內(nèi)存方式

    指定springboot的jar運行內(nèi)存方式

    這篇文章主要介紹了指定springboot的jar運行內(nèi)存方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-02-02
  • SpringBoot集成RocketMQ的使用示例

    SpringBoot集成RocketMQ的使用示例

    RocketMQ是阿里巴巴開源的一款消息中間件,性能優(yōu)秀,功能齊全,被廣泛應用在各種業(yè)務場景,本文就來介紹一下SpringBoot集成RocketMQ的使用示例,感興趣的可以了解一下
    2023-11-11
  • 基于jenkins發(fā)布編譯后的class文件

    基于jenkins發(fā)布編譯后的class文件

    這篇文章主要介紹了基于jenkins發(fā)布編譯后的class文件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-07-07
  • 簡述Java中throw-throws異常拋出

    簡述Java中throw-throws異常拋出

    任何Java代碼都可以拋出異常,本文主要介紹了Java中throw-throws異常拋出,具有一定的參考價值,感興趣的可以了解一下
    2021-08-08
  • windows下jar包開機自動重啟的步驟

    windows下jar包開機自動重啟的步驟

    這篇文章主要給大家介紹了關(guān)于windows下jar包開機自動重啟的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-11-11
  • SpringBoot快速整合通用Mapper的示例代碼

    SpringBoot快速整合通用Mapper的示例代碼

    后端業(yè)務開發(fā),每個表都要用到單表的?增刪改查?等通用方法,而配置了通用Mapper可以極大的方便使用Mybatis單表的增刪改查操作,這篇文章主要介紹了SpringBoot快速整合通用Mapper,需要的朋友可以參考下
    2022-07-07
  • SpringBoot?整合?Elasticsearch?實現(xiàn)海量級數(shù)據(jù)搜索功能

    SpringBoot?整合?Elasticsearch?實現(xiàn)海量級數(shù)據(jù)搜索功能

    這篇文章主要介紹了SpringBoot?整合?Elasticsearch?實現(xiàn)海量級數(shù)據(jù)搜索,本文主要圍繞?SpringBoot?整合?ElasticSearch?接受數(shù)據(jù)的插入和搜索使用技巧,在實際的使用過程中,版本號尤其的重要,不同版本的?es,對應的?api?是不一樣,需要的朋友可以參考下
    2022-07-07

最新評論