搜尋
首頁Javajava教程Java基礎之volatile應用實例分析

Java基礎之volatile應用實例分析

問:請談談你對volatile的理解?
答:volatile是Java虛擬機器提供的輕量級的同步機制,它有3個特性:
1)保證可見性
2)不保證原子性
3)禁止指令重排

剛學完java基礎,如果有人問你什麼是volatile?它有什麼作用的話,相信一定非常懵逼…
可能看了答案,也完全不明白,什麼是同步機制?什麼是可見性?什麼是原子性?什麼是指令重排?

1、volatile保證可見性

1.1、什麼是JMM模型?

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

JMM(Java記憶體模型,Java Memory Model)本身就是一種抽象的概念,並非真實存在。它描述的是一組規則或規範,透過這組規範,定了程式中各個變數的存取方法。 JMM關於同步的規定:
1)線程解鎖前,必須把共享變數的值刷新回主記憶體;
2)線程加鎖前,必須讀取主記憶體的最新值到自己的工作記憶體;
3)加上鎖解鎖是同一把鎖;

由於JVM運行程式的實體是線程,創建每個線程時,JMM會為其建立一個工作記憶體(有些地方稱為堆疊空間) ,工作記憶體是每個執行緒的私有資料區域。

Java記憶體模型規定所有變數都儲存在主內存,主記憶體是共享記憶體區域,所有執行緒都可以存取。

但執行緒對變數的操作(讀取、賦值等)必須在工作記憶體中進行。首先需要將變數從主內存複製到工作內存,進行操作後再寫回主內存。

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

1)如下圖,此時賣票系統後端只剩下1張票,並已讀入主內存:ticketNum=1。
2)此時網路上有多個使用者都在搶票,那麼此時就有多個線程同時都在進行買票服務,假設此時有3個線程都讀入了目前的票數:ticketNum =1,那麼接著就會買票。
3)假設線程1先搶占到cpu的資源,先買好票,並在自己的工作內存中將ticketNum的值改為0:ticketNum=0,然後再寫回到主內存中。

此時,線程1的用戶已經買到票了,那麼線程2,線程3此時應該不能再繼續買票了,因此需要係統通知線程2,線程3,ticketNum此時已經等於0了:ticketNum=0。如果有這樣的通知操作,你就可以理解為就具有可見性。

Java基礎之volatile應用實例分析

透過上面對JMM的介紹和舉例,可以簡單總結下。

JMM記憶體模型的可見性是指,多執行緒存取主記憶體的某一個資源時,如果某一個執行緒在自己的工作記憶體中修改了該資源,並寫回主內存,那麼JMM記憶體模型應該要通知其他執行緒來從新取得最新的資源,來確保最新資源的可見性。

1.2、volatile保證可見性的程式碼驗證

在第1.1節中,我們基本上了解可見性的定義,現在我們可以使用程式碼驗證這個定義。經實踐證明,使用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 正在执行");
                    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后,如果有可见性的话,那么就会跳出循环;
            // 如果没有可见性的话,就会一直在循环里执行
        }

        System.out.println("具有可见性!");
    }}

運行結果如下圖,可以看到雖然執行緒0已經將number的值改為了10,但是主執行緒還是在循環中,因為此時number不具有可見性,系統不會主動通知。
Java基礎之volatile應用實例分析

1.2.1、volatile保證可見性驗證

在上面程式碼的第7行給變數number加入volatile後再次測試,如下圖,此時主線程成功退出了循環,因為JMM主動通知了主執行緒更新number的值了,number已經不為0了。
Java基礎之volatile應用實例分析

2、volatile不保證原子性

2.1 什麼是原子性?

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

原子性是指無法分割或打斷,維持完整性的特性。換句話說,當一個執行緒正在執行某個操作時,它不能被任何因素中斷。要嘛同時成功,要嘛同時失敗。

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

如下圖,創建了一個測試原子性的類別:TestPragma。編譯後的程式碼表明,add方法內對n的增加是透過三個指令來完成的。

因此可能存在线程1正在执行第1个指令,紧接着线程2也正在执行第1个指令,这样当线程1和线程2都执行完3个指令之后,很容易理解,此时n的值只加了1,而实际是有2个线程加了2次,因此这种情况就是不保证原子性。
Java基礎之volatile應用實例分析

2.2 不保证原子性的代码验证

在2.1中已经进行了举例,可能存在2个线程执行n++的操作,但是最终n的值却只加了1的情况,接下来对这种情况再用代码进行演示下。

首先给MyData类添加一个add方法

package com.koping.test;class MyData {
    volatile int number = 0;

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

然后创建测试原子性的类:TestPragmaDemo。验证number的值是否为20000,需要测试通过20个线程分别对其加1000次后的结果。

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个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);

    }}

运行结果如下图,最终number的值仅为18410。
可以看到即使加了volatile,依然不保证有原子性。
Java基礎之volatile應用實例分析

2.3 volatile不保证原子性的解决方法

上面介绍并证明了volatile不保证原子性,那如果希望保证原子性,怎么办呢?以下提供了2种方法

2.3.1 方法1:使用synchronized

方法1是在add方法上添加synchronized,这样每次只有1个线程能执行add方法。

结果如下图,最终确实可以使number的值为20000,保证了原子性。

但在实际业务逻辑方法中,很少只有一个类似于number++的单行代码,通常会包含其他n行代码逻辑。现在为了保证number的值是20000,就把整个方法都加锁了(其实另外那n行代码,完全可以由多线程同时执行的)。所以就优点杀鸡用牛刀,高射炮打蚊子,小题大做了。

package com.koping.test;class MyData {
    volatile int number = 0;

    public synchronized void add() {
      // 在n++上面可能还有n行代码进行逻辑处理
        number++;
    }}

Java基礎之volatile應用實例分析

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次。可以将原句重写为:使用原子整型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个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
        System.out.println("num值加了20000次,此时number的实际值是:" + myData.num);

    }}

Java基礎之volatile應用實例分析

3、volatile禁止指令重排

3.1 什么是指令重排?

在第2节中理解了什么是原子性,现在要理解下什么是指令重排?

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:
源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

处理器在进行重排时,必须要考虑指令之间的数据依赖性。

单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。

但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

看了上面的文字性表达,然后看一个很简单的例子。
比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:
1)1234
2)2134
3)1324
以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

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 执行构造方法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("单线程的情况测试结束\n");
    }}

首先是在单线程情况下进行测试,结果如下图。可以看到,构造方法只执行了一次,是没有问题的。
Java基礎之volatile應用實例分析

3.3 多线程单例模式

接下来在多线程情况下进行测试,代码如下。

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

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法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("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多线程情况下的运行结果如下图。可以看到,多线程情况下,出现了构造方法执行了2次的情况。
Java基礎之volatile應用實例分析

3.4 多线程单例模式改进:DCL

在3.3中的多线程单里模式下,构造方法执行了两次,因此需要进行改进,这里使用双端检锁机制: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 执行构造方法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("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多次运行后,可以看到,在多线程情况下,此时构造方法也只执行1次了。
Java基礎之volatile應用實例分析

3.5 多執行緒單例模式改進,DCL版存在的問題

要注意的是3.4中的DCL版的單例模式依然不是100%準確的! ! !

是不是不太明白為什麼3.4DCL版單例模式不是100%準確的原因
是不是不太明白在3.1講完指令重排的簡單理解後,為什麼突然要講多執行緒的單例模式

因為3.4DCL版單例模式可能會因為指令重排而導致問題,雖然該問題出現的可能性可能是千萬分之一,但是該程式碼依然不是100%準確的。 如果要保證100%準確,那麼需要加入volatile關鍵字,增加volatile可以禁止指令重排

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

檢視instance = new SingletonDemo();編譯後的指令,可以分為以下3步:
1)分配物件記憶體空間:memory = allocate();
2)初始化物件: instance(memory);
3)設定instance指向分配的記憶體位址:instance = memory;

由於步驟2和步驟3不存在資料依賴關係,因此可能出現執行132步驟的情況。
例如執行緒1執行了步驟13,還沒有執行步驟2,此時instance!=null,但是物件還沒有初始化完成;
如果此時執行緒2搶占到cpu,然後發現instance!=null,然後直接回傳使用,就會發現instance為空,就會出現異常。

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

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

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

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

首先要了解一個概念:記憶體屏障(Memory Barrier),又稱為記憶體柵欄。它是CPU指令,有2個作用:
1)保證特定操作的執行順序;
2)保證某些變數的記憶體可見性;

由於編譯器和處理器都能執行指令重排。如果在指令之間插入一條Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說,透過插入記憶體屏障,禁止在記憶體屏障前後的指令執行重排需最佳化

記憶體屏障的另一個作用是強制刷出各種CPU的快取數據,因此任何CPU上的執行緒都能讀取到這些數據的最新版本

Java基礎之volatile應用實例分析

以上是Java基礎之volatile應用實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:亿速云。如有侵權,請聯絡admin@php.cn刪除
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具