volatile 關鍵字

1 volatile 的作用

Volatile 是Java中的一個關鍵字,僅用來修飾屬性,不能修飾類和方法。它提供了一種弱的同步機制,用於多線程間的變量同步。它保證在某個線程中對變量的修改,可以立即被其他持有這個變量的線程看到。從內存模型的角度來說,就是工作內存中的改變立即刷新到主內存中,並強制其他工作內存進行刷新。

它可以保證代碼的可見性、有序性,但不能保證原子性

2 volatile適用場景

目前比較常見的volatile的應用場景是使用volatile來修飾一個信號量控制線程的中斷。目前JDK提供的線程中斷方法stop()已經不建議被使用了,我們通常通過定義一個boolean類型的信號量控制線程的中斷。

class MyThread implements Runnable
{
	private volatile boolean stopMark=false;
	@Override
	public void run()
	{
		System.out.println("thread start");
		while(!stopMark)
		{
			try
			{
				Thread.sleep(500);//模擬一個耗時的方法
			}
			catch (InterruptedException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("thread stop");
		
	}
	public void stop()
	{
		this.stopMark=true;
	}
}

有人會說,如果不使用volatile修飾的話,這個功能不能實現嗎?的確,大多數情況下是可以實現的,而且筆者在學習volatile關鍵字之前也一直都是這樣做的。但是極少數的情況下會出現問題,比如線程1調用線程2的stop的方法之後轉入其他工作狀態,改變的stopMark信號量沒能及時同步回主內存,這樣就可能導致線程2陷入死循環。爲了避免這種情況發生,不僅要使用volatile關鍵字修飾信號量,同時還應該使用反饋機制,例如在while循環之後使用另外一個信號量標識線程已經退出等等

3 volatile能保證線程安全嗎?

從前面的描述來看,似乎volatile可以保證操作的原子性來達到線程安全的目的。那麼來看看下面這個例子:

public class Volatile
{
	public  volatile int a=0;
	
	public   void increase()
	{
		a++;
	}
	public static void main(String[] args)
	{
		final Volatile voilatile=new Volatile();
		for(int i=0;i<10;i++)
		{
			Thread thread=new Thread(new Runnable()
			{
				
				@Override
				public void run()
				{
					
					for(int j=0;j<1000;j++)
					{
						voilatile.increase();
					}
					
				}
			});
			thread.start();
		}
		while(Thread.activeCount()>1)
			Thread.yield();
		System.out.println(voilatile.a);
	}
}

從代碼的角度來說加之對volatile的“線程安全”的理解,這段代碼總能輸出10000(1000*10)這個結果。但事實上,只有在極少的情況下能輸出10000;對於大多數情況,這個輸出都要比10000要小。這是什麼原因呢?

從反彙編的結果可以看到,一個a++的操作,實際上對應的多條指令。在真正的add操作指令前後分別有get..和put..指令。當一個線程執行add指令的時候,其他多個線程也可能正在執行add指令,這樣當發生put寫入動作的時候,當前線程可能把一個較小的值寫入覆蓋掉其他多個線程更改的結果,這就寫入了髒數據。例如當個get結果爲100,add之後的結果爲101,而其他線程也在add,這時主內存中的這個值可能已經是105,當把101同步回主內存的時候,這個值就變成了101,而並非我們期望的106…

由此看來volatile並不能保證操作的原子性。它只能保證操作的可見性。對於上面的代碼可以通過同步塊、加鎖或者使用原子類的形式實現。對於volatile的適用場景,它必須滿足以下條件:
  •  運算結果不依賴與當前值,或者能保證只有一個線程可以修改這個值;
  • 變量不需要與其他的狀態變量共同參與變量約束

4 volatile保證代碼有序性

在程序執行時,爲了提高性能,編譯器和處理器常常會對指令重排序。重排序分三種類型

  • 編譯器優化的重排序。編譯器在不改變單線程語義的前提下,可以重新安排語句的執行順序
  • 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行。
  • 內存系統的重排序。由於處理器使用緩存和讀/寫緩衝區,這使得加載和儲存操作看上去可能是在亂序執行。

從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序


上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序可能會導致多線程程序出現內存可見性問題。對於編輯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定的內存屏蔽來禁止特定類型的處理器重排序。而volatile恰好有這個功能。

由於JVM會對代碼進行重排序優化,普通的變量僅僅會保證在該方法的執行過程中所有依賴關係賦值結果的地方都能夠獲得正確的結果,而不能保證執行的順序與代碼的順序是一致的。下面來看一個例子,指令1把a+10,指令2把a擴大兩倍,指令3把b擴大十倍。

a=a+10;//指令1
a=a*2; //指令2
b=b*10;//指令3

這裏顯然指令1、2存在依賴關係,(a+10)*2和a*2+10顯然是不一樣的,但是如果在指令1,2之間插入指令3,顯然不會對結果構成影響。這就是指令重排序。而如果此時b被volatile關鍵字修飾,那麼指令1,2永遠在3之前執行,如果3後面存在其他指令,後面的指令也永遠在3後執行。

這個到底有什麼意義呢?試想這樣一個場景,需要讀取一段配置文件,設置一個標識符標識是否讀取完成,例如下面僞代碼:

volatile boolean isRead=false; //1
readContext();//2
isRead=true;//3

如果isRead變量不被volatile修飾的話,那麼很可能3號代碼提前到2號之前執行,這樣就違反了我們的意圖。

5 volatile的實現原理

這裏主要講volatile如何實現可見性和有序性的

5.1 可見性的實現

Java代碼:

instance = new Singleton();//instance是volatile變量

彙編代碼:

0x01a3de1d: movb $0x0,0x1104800(%esi);

0x01a3de24: lock addl $0x0,(%esp);

通過對帶有volatile關鍵字修飾的java代碼進行反編譯,可以看到有一個lock指令。這個lock指令的有以下作用:

  • 將當前處理器緩存行的數據會寫回到系統內存,這個操作相當於發生了內存模型裏的store和write操作。
  • 這個寫回內存的操作會引起在其他CPU裏緩存了該內存地址的數據無效。

從這兩句描述基本可以看出它是如何實現可見性。當對volatile關鍵字修飾的變量操作時,它會將這個操作結果立即同步會主內存,而其他線程的CPU緩存裏(可以理解爲工作內存)的數據被強制過期。而當CPU使用緩存數據的時候,首先要檢驗數據是否過期,如果過期就會從內存中重新讀取。這樣就實現了操作結果的可見性。

5.2 一致性的實現

這個實現也是利用前面的lock操作。Lock操作相當於在指令前增加了一個內存屏蔽,而指令重排序是無法跨越內存屏蔽的。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章