volatile synschonized的區別

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++;   
            }//加入Java開發交流君樣:756584822一起吹水聊天
        }).start();
    }      
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(count);     
}

這段代碼執行的結果有時候會小於10000,原因就是count++不是原子操作,雖然用volatile修飾,但是也不起作用。
//加入Java開發交流君樣:756584822一起吹水聊天
  那麼上面這段代碼如何正確運行呢,答案很顯然,把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的使用場景有所限制。因爲它無法保證多線程下的原子性。

image

最新2020整理收集的一些高頻面試題(都整理成文檔),有很多幹貨,包含mysql,netty,spring,線程,spring cloud、jvm、源碼、算法等詳細講解,也有詳細的學習規劃圖,面試題整理等,需要獲取這些內容的朋友請加Q君樣:756584822

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