Java多線程進階(六):Volatile

在多線程併發編程中 synchronizedvolatile 都扮演着重要的角色,volatile 是輕量級的 synchronized,它在多處理器開發中保證了共享變量的“可見性”。

內存可見性
  • 內存可見性,指的是線程之間的可見性,當一個線程修改了共享變量時,另一個線程可以讀取到這個修改後的值。
  • 所謂內存可見性,指的是當一個線程對volatile修飾的變量進行寫操作(比如step 2)時,會立即把該線程對應的本地內存中的共享變量的值刷新到主內存;當一個線程對volatile修飾的變量進行讀操作(比如step 3)時,會把立即該線程對應的本地內存置爲無效,從主內存中讀取共享變量的值。
	public class VolatileDemo {
	    int a = 0;
	    volatile boolean flag = false;
	
	    public void writer() {
	        a = 1; // step 1
	        flag = true; // step 2
	    }
	
	    public void reader() {
	        if (flag) { // step 3
	            System.out.println(a); // step 4
	        }
	    }
	}

	public static void main(String[] args) throws InterruptedException {
        VolatileDemo volatileDemo = new VolatileDemo();
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                volatileDemo.writer();
            }
        });

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                volatileDemo.reader();
            }
        });

        a.start();
        b.start();
    }

這個時候的輸出結果爲:1

而如果flag變量沒有用volatile修飾,在step 2,線程A的本地內存裏面的變量就不會立即更新到主內存,那隨後線程B也同樣不會去主內存拿最新的值,仍然使用線程B本地內存緩存的變量的值a = 0,flag = false。

禁止重排序

爲優化程序性能,對原有的指令執行順序進行優化重新排序。重排序可能發生在多個階段,比如編譯重排序、CPU重排序等。

在JSR-133之前的舊的Java內存模型中,是允許volatile變量與普通變量重排序的。那上面的案例中,可能就會被重排序成下列時序來執行:

  • 線程A寫volatile變量,step 2,設置flag爲true;
  • 線程B讀同一個volatile,step 3,讀取到flag爲true;
  • 線程B讀普通變量,step 4,讀取到 a = 0;
  • 線程A修改普通變量,step 1,設置 a = 1;

可見,如果volatile變量與普通變量發生了重排序,雖然volatile變量能保證內存可見性,也可能導致普通變量讀取錯誤。

所以在舊的內存模型中,volatile的寫-讀就不能與鎖的釋放-獲取具有相同的內存語義了。爲了提供一種比鎖更輕量級的線程間的通信機制,JSR-133專家組決定增強volatile的內存語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排序。

volatile的非原子性
  • volatile雖然增加了實例變量在多個線程之間的可見性,但是卻不具備通同步性,也就是不具備原子性。
  • 使用volatile關鍵字之後,可以強制從公共內存中讀取變量的值。
  • Volatile只能保證對單個volatile變量的讀/寫具有原子性。
volatile的用途

volatile的主要使用場合是在多個線程中可以感知到實例變量被更改了,也就是多線程讀取共享變量的時可以獲得最新的值來使用。

在保證內存可見性這一點上,volatile有着與鎖相同的內存語義,所以可以作爲一個“輕量級”的鎖來使用。但由於volatile僅僅保證對單個volatile變量的讀/寫具有原子性,而鎖可以保證整個臨界區代碼的執行具有原子性。所以在功能上,鎖比volatile更強大;在性能上,volatile更有優勢。

比如我們熟悉的單例模式,其中有一種實現方式是“雙重鎖檢查”,比如這樣的代碼:

public class Singleton {

    private static volatile Singleton instance; // 不使用volatile關鍵字

    // 雙重鎖檢驗
    public static Singleton getInstance() {
        if (instance == null) { // 第7行
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 第10行
                }
            }
        }
        return instance;
    }
}
synchronized和volatile對比
  • volatile是線程同步的輕量級的實現,所以volatile性能比synchronized要好,並且volatile只能修飾變量,而synchronized可以修飾方法以及代碼塊。
  • 多線程訪問volatile不會發生阻塞,而synchronized會發生阻塞。
  • volatile能保證數據的可見性,但不能保證原子性。而synchronized可以保證原子性,也可以間接保證可見性,因爲synchronized會將私有內存和公共內存中的數據做同步,所以更加推薦使用synchronized。
  • volatile的關注點在於解決變量在多個線程之間的可見性,而synchronized的關注點在於解決多個線程之間訪問資源的同步性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章