volatile synchronized visibility multi-thread 多執行緒

Volatile in Java -- 用volatile解決可視性問題

郭彥廷 2019/12/30 11:00:00
16019

Overview

volatile是基礎但有時會遭誤解的關鍵字。本篇文章將簡述其在Java語言中的特性、介紹其和同樣用來處理執行緒安全的關鍵字synchronized有何不同,並佐以簡易的範例code做說明。

 

When to use volatile

Java裡,每個執行緒有各自的記憶體空間(working memory),當執行完一段操作後,執行緒會再將剛才使用到的變數的值更新到主記憶體(main memory)裡。其他執行緒則可從主記憶體讀取到變數的最新值。

上述特性加快了程式處理的效率,但在多執行緒環境裡卻可能為我們帶來變數可視性(visibility)的問題。即當一個變數的讀取和寫入發生在不同的執行緒時,讀取變數的執行緒有時無法及時看到變數的值的改變(被其他執行緒寫入),導致資料不一致。

此時,可在變數前加上volatile此變數會改為不使用各執行緒的working memory,永遠從主記憶體做存取與讀寫。

 

example:

底下的範例程式碼將比較有volatile和沒volatile的差別。

public class VisibilityProblem {
    static int num;
    
    public static void main (String[] args) {

        Thread readerThread = new Thread(() -> {
            int temp = 0;
            while (true) {
                if (temp != num) {
                    temp = num;
                    System.out.println("reader: value of num = " + num);
                }
            }
        });

        Thread writerThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                num++;
                System.out.println("writer: changed value to = " + num);
                
             // 進入睡眠,以讓readerThread有足夠時間讀到int num的改變(因為num++非具原子性的操作,readerThread仍有一定機會讀到錯誤的值)
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
            //離開程式,否則readerThread會一直等待變數int num的值改變
            System.exit(0);
        });

        readerThread.start();
        writerThread.start();
    }
}

上段程式碼裡,int numreaderThreadwriterThread的共用變數,未加volatile修飾的output如下:

沒加volatilereaderThread只讀到變數numdefault01的變化。

但將變數num加上volatile修飾子後。即:

static volatile int num;

    output變成:

 

    可見加上volatile後,變數num值的變化可及時被readerThread讀取到。

 

volatile vs synchronized

撰寫多執行緒應用程式時,確保資料一致性的兩大原則:

l   Mutual Exclusion - critical section裡的程式碼一次只能被一個執行緒執行

l   Visibility – 共享資料的值被某執行緒更改時,其他執行緒可及時看見

 

  使用volatile可確保Visibility,但不具Mutual Exclusion

  使用synchronized,則可保證以上兩項特性,代價則是更差的效能。

 

  用volatile可以幫助我們寫出更簡潔的code。相較用synchronized鎖住某個區塊,因為用volatile像是將同步責任交給JVM,會比我們自己處理更不容易出錯。但如果宣告為volatile的變數經常被使用的話,可能導致程式的效能不如鎖住整個區塊。

volatile in Java vs C/C++

  Javavolatile是告訴編譯器變數的值不快取到working memory,讀寫永遠透過main memory

 

  C/C++volatile則是告訴編譯器不要優化我們所撰寫的code,和我們文章上方所討論的同步問題無關係。

 

Conclusion:

  由上可知,在某些需要可視性,且沒有資料競速(race condition)的情境,像是:我們在某個執行緒裡寫了個變數,用來做旗標;在別的執行緒查看那個變數,並且變數值的寫入並不依據當前的值做更新。

  或者,我們其實並不那麼在意資料競速可能帶來的誤差,用synchronized顯得殺雞用牛刀時,我們可以使用volatile來達到我們的目的。

 

  簡言之,當我們需要更多的visibility時,我們可以標記變數為volatile,但它不具備鎖的功能,所以使用上仍須有心理準備可能錯過某些更新。

 

資料來源:

geeksforgeeks:volatile keyword in Java

baeldung:Guide to the Volatile Keyword in Java

javamex:When to use volatile?

logicbig:Java Memory Model - Visibility problem, fixing with volatile variable

jenkov:Java Volatile Keyword

volatile應用在設計模式:

geeksforgeeks:builder pattern in java

geeksforgeeks:singleton design pattern

 

郭彥廷