volatile synschonized的區別


在一次面試中,被問到volatile與synschonized的區別,概念模模糊糊,今天做一個總結,加強自己的認識。

本文參考http://www.cnblogs.com/dolphin0520/p/3920373.html,主要對自己的認識做個總結。

 

    valitile這個關鍵詞,不局限於java中,其實很多語言中都有這個關鍵詞。由於自己之前對於多線程的編程接觸比較少,而且對於java的內存模型不是很了解,所以今天做一個總結。

 

    內存模型

       現在想想大學那會學的操作系統真是太有用了,可惜當時沒有認真學,很多編程的問題,都可以歸結到操作系統,而且很多優秀的設計都是從操作系統來的。不說了,一把心酸淚。還是努力彌補吧。

       計算機的主要運算是由cpu,內存之間交互的,而他們之間的交互靠的是總線。但是由於cpu的速度遠遠大於內存的,所以在cpu旁邊往往會設計一級二級緩沖,作用就是中和cpu與內存之間的速度。

      但是讓我們想想,如果程序是單線程的,基本沒啥問題,因為數據不會存放着多個緩沖中,也就不涉及一致性的問題,但是當我們的程序里面有多線程的時候,可能不同的cpu會執行不同的線程,這樣可能不同cpu的緩沖會持有同一個變量的不同副本,這樣就有問題了。不同線程操作同一個變量,如何做到同步。

     為了解決這個問題,那篇文章里面提到有兩種方式。

  1.在訪問的時候,總線加鎖,也就是相當於人為將多線程,變為單線程,這樣不會出現數據不一致的問題,但是這樣帶來了很大問題,就是效率大打折扣,體現不出多線程的優勢。

  2.緩存一致性協議:

    這里我就引用別人的一段話:“緩存一致性協議,最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變

    量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置為無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的

    緩存行是無效的,那么它就會從內存重新讀取。”

  最后附上一張圖,來總結下上面說的。

  

 

   多線程編程的概念

     1.原子性:

        這個概念在數據庫里面也有,意思就是保證一個操作,要么完成,要不不做,而不能是做了一半。想必大家對這個最熟悉的應該就是銀行的例子了吧。轉賬的時候,從我賬戶

        扣款和給對方賬戶加款,應該是原子操作,不能說從我賬戶扣了,但是對方賬戶沒有增加。這是在多線程里面必須要避免的問題。

     2.可見性:

        這個詞的意思就是當一個線程在修改了某一個變量之后,可以馬上將改變刷新到別的線程,也就是說如果別的線程需要訪問的時候,是訪問的修改過后的值。

     3.有序性:

        程序執行的順序,不一定會按照代碼書寫的順序進行執行。而是編譯器會對代碼進行指令的優化,這樣做的目的是為了保證程序執行的效率。這樣做基本上對於單線程沒啥問

        題,編譯器保證做過優化后的代碼和沒做優化的代碼,執行的結果是一樣的。但是如果是多線程呢,這點編譯器就無法保證。

 

     綜上所述,一個多線程如果要正確的執行,就必須滿足上面三個條件。如果不滿足,則執行的結果就有可能出錯。

 

    那么java里面通過什么樣的方式來確保符合三種原則呢?

 

    java保證多線程(java內存模型)

      1.原子性:

        在java中,對基本數據類型的讀取與賦值是原子操作的,要不操作成功,要么失敗。

        那么怎么樣的操作算原子操作呢,下面舉兩個例子:

          

int x = 3;
int y = x;
y++;

                    第一條語句,是直接將值復制給x變量。第二條語句,首先讀取x的值,然后再賦值給y。第三條語句,首先讀取y的值,然后加一操作后,再賦值給y。

       所以上面三條語句只有第一條語句是原子操作。這也就解釋了下面這段代碼結果為啥不是人們的預期。

    

	public volatile static  int count = 0;
	public static void main(String[] args) {	
		for(int i=0;i<10000;i++)
		{
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					count++;	
				}
			}).start();
		}		
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(count);		
	}

  這段代碼執行的結果有時候會小於10000,原因就是count++不是原子操作,雖然用volatile修飾,但是也不起作用。

  那么上面這段代碼如何正確運行呢,答案很顯然,把count++變成原子操作即可,那么修改count的類型,如下

  public  static AtomicInteger count =new AtomicInteger(0);

  將count++變為count.getAndIncrement();

  這樣保證了原子操作。結果也就是10000了。

      2.可見性:

        對於可見性,java提供了volatile關鍵詞來修飾,上面已經用到過了。這個關鍵詞保證他修改后的值,會馬上更新到java的主存中,當其他線程再要讀取的時候,就是讀取的

        新的值,但是用這個關鍵詞的時候,也得多多注意,就跟上面說的那種情況,也是不行的。當然上面的情況可以用加鎖,或者 synchronized方式進行同步。保證結果。

      3.有序性:

        Java里面也是通過volatile來保證一定程度上的有序性。也可以通過 synchonized來保證多線程下的有序性。

        

  在《深入理解Java虛擬機》有這么一段話“

  觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”

  lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

  1)它確保指令重排序時不會把其后面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的后面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;

  2)它會強制將對緩存的修改操作立即寫入主存;

  3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。

  

  所以以后遇到問題的時候,還是得多從原理里面找答案。

 

  雖然volatile的性能比synchronized性能高,但是volatile的使用場景有所限制。因為它無法保證多線程下的原子性。

    

        

 


免責聲明!

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



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