並發和多線程-說說面試長提平時少用的volatile


說到volatile,一些參加過面試的同學對此肯定不陌生。

它是面試官口中的常客,但是平時的編碼卻很少打照面(起碼,我是這樣的)。

最近的面試,我也經常會問到volatile相關的問題,比如volatile和sychronized的區別volatile的使用場景volatile的實現原理等等。

問這些問題其實主要還是考察多線程、鎖等方便的知識儲備。雖然volatile在我們日常編碼使用不多,但是他的實現思想以及背后牽扯的一些概念還是值得我們學習和思考的。

舉例

首先我們來看一個例子


public class VolatileExample extends Thread {
    
    private static boolean flag = false;
    
    public void run() {
        while (!flag) {
        }
    }

    public static void main(String[] args) throws Exception {
        new VolatileExample().start();
        Thread.sleep(100);
        flag = true;
    }
}

這段代碼並不長。

  • 聲明了一個布爾類型的靜態變量flag,初始值為false;

  • main函數中啟動了一個線程VolatileExample,集成自Thread類;

  • 除去VolatileExample線程,當然還有一個主線程main;

  • 主線程sleep 100毫秒,並在其后置flag為true;

那么,你覺得運行main會怎樣

  • 順利跑完所有代碼,結束?

  • 程序一直在等待,不會結束?

我們看下運行的結果

從結果可以發現,主線程運行結束后,界面中左邊的紅點並沒有消失,表示程序還沒有結束。

這是因為啟動的VolatileExample線程讀取到的flag一直都是false。

雖然主線程中flag已經被賦值為true,但是VolatileExample線程卻視而不見,這是為什么呢,這就涉及到一個可見性的問題。

可見性

這里假設大家都是對線程以及鎖等知識有一定的了解

sychronized我們應該都用過或者了解過,這是為了在多線程環境下對共享資源添加一個標識,用於鎖住共享資源,防止多線程同時進入對數據操作產生不一致的現象。

我們平時說的加鎖其實表面上是為了達到一種互斥的效果,也就是對於同時存在的線程A和線程B,如果這時候線程A或者鎖,則線程B只能在旁邊乖乖的等,這就是一種互斥,有你沒我,有我沒你!

但是加鎖的本質是解決了可見性的問題。具體先不說這個問題,我們先結合上面的例子來說說可見性的問題。后面在結合可見性可能更好理解加鎖的本質。

在java內存模型中,是分為主內存工作內存兩塊的。

主內存,主要是存儲各個線程都會用到共享變量等。

對於每個線程都有自己的一個存儲變量的地方,就是工作內存。各個線程之間的工作內存是相互獨立的,不可見的。

到這里,你可能已經知道上面例子的程序為什么一直出於運行的狀態沒有終止了。沒錯,VolatileExample線程讀取的flag值是自己線程中存儲在工作內存中的值,主線程中的flag值雖然更新了,但是對於VolatileExample是不可見、無感知的。

所以,這時候volatile關鍵字就排上用場了。我們在flag變量錢添加volatile關鍵字修飾。再次運行程序

效果顯而易見,主線程和VolatileExample線程一同結束。

這是因為使用volatile關鍵字修飾,該變量是強制要求從主內存讀以及往主內存寫,這樣保證各個線程在操作這個變量的值時,都是最新鮮的數據。

這時候,我們再來回顧剛剛說到加鎖的本質是解決可見性問題。

volatile是強制讓所有線程都從一個地方操作共享資源,使得原來是從每個線程的副本中讀取變量變為從主內存讀取變量,從而解決了可見性問題。

而sychronized也是解決了可見性問題,它不允許同一時間有兩個線程操作同一共享資源,因為其無法保證可見性。所以其通過獨占互斥的方式,保證自己執行完之后才會有下一個執行,這樣每次只有一個線程占有資源,也是間接的解決了可見性的問題。

說到這,就不得不提另外一個問題——原子性

原子性


private int num = 0;

num++;

上面的代碼符合原子性么?

顯然不符合,假如現在有線程A和線程B兩個線程,

線程A讀取num=0,准備執行num++的操作,

但是還沒有執行“++”操作之前,線程B讀取num的值,此時值為0,之后也執行num++的操作,

最后A和B兩個線程最終得到的值都是1,

這是不符合原子性的。

通過如下的sychronized的修飾就符合原子性了


sychronized(this) {

    num++;

}

其道理還是因為sychronized的獨占性。

那么volatile是否可以保證原子性呢

答案是否定的。道理和上面沒有加sychronized的描述是一樣的。

線程A還有執行完num++后,線程B也來訪問num值,得到的是0,然后執行num++,最終還是兩個線程得到的值都是1。

那么volatile有哪些使用場景呢。

volatile的適用場景

  • volatile是在synchronized性能低下的時候提出的。如今synchronized的效率已經大幅提升,所以volatile存在的意義不大。

  • 如今非volatile的共享變量,在訪問不是超級頻繁的情況下,已經和volatile修飾的變量有同樣的效果了。

  • volatile不能保證原子性,這點是大家沒太搞清楚的,所以很容易出錯。

  • volatile可以禁止重排序(sychronized也是可以的)。

其實與之相關概念還有重排序、happens-before,as-if-serial等等,限於篇幅,不再詳述。

如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM