Java多線程之記憶體可見度

來源:互聯網
上載者:User

標籤:memory

1、什麼是JAVA 記憶體模型

Java Memory Model (JAVA 記憶體模型)描述線程之間如何通過記憶體(memory)來進行互動。 具體說來, JVM中存在一個主存區(Main Memory或Java Heap Memory),對於所有線程進行共用,而每個線程又有自己的工作記憶體(Working Memory),工作記憶體中儲存的是主存中某些變數的拷貝,線程對所有變數的操作並非發生在主存區,而是發生在工作記憶體中,而線程之間是不能直接相互訪問,變數在程式中的傳遞,是依賴主存來完成的。


Java記憶體模型的抽象如下:

650) this.width=650;" src="http://cdn3.infoqstatic.com/statics_s1_20151020-0055-2/resource/articles/java-memory-model-1/zh/resources/11.png" style="border:0px;margin:0px 10px 10px 0px;padding:0px;" />

從來看,線程A與線程B之間如要通訊的話,必須要經曆下面2個步驟:

1、線程A把本地記憶體A中更新過的共用變數重新整理到主記憶體中去。
2、線程B到主記憶體中去讀取線程A之前已更新過的共用變數。
下面通過來說明這兩個步驟:

650) this.width=650;" src="http://cdn3.infoqstatic.com/statics_s1_20151020-0055-2/resource/articles/java-memory-model-1/zh/resources/22.png" style="border:0px;margin:0px 10px 10px 0px;padding:0px;" />

如所示,本地記憶體A和B有主記憶體中共用變數x的副本。假設初始時,這三個記憶體中的x值都為0。線程A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體A中。當線程A和線程B需要通訊時,線程A首先會把自己本地記憶體中修改後的x值重新整理到主記憶體中,此時主記憶體中的x值變為了1。隨後,線程B到主記憶體中去讀取線程A更新後的x值,此時線程B的本地記憶體的x值也變為了1。

從整體來看,這兩個步驟實質上是線程A在向線程B發送訊息,而且這個通訊過程必須要經過主記憶體。JMM通過控制主記憶體與每個線程的本地記憶體之間的互動,來為Java程式員提供記憶體可見度保證。

2、什麼是記憶體可見度?

從可知,如果線程A對共用變數X進行了修改,但是線程A沒有及時把更新後的值刷入到主記憶體中,而此時線程B從主記憶體讀取共用變數X的值,所以X的值是原始值,那麼我們就說對於線程B來講,共用變數X的更改對線程B是不可見的。如果共用的更新不可見,會導致什麼問題呢? 請看下面的例子:

[java] view plain copy

  1. class MyThread implements Runnable {  

  2.     int num = 1000000;  

  3.     public void run() {  

  4.         if (Thread.currentThread().getName().equals("t1")) {  

  5.             increment();  

  6.         } else {  

  7.             decrement();  

  8.         }  

  9.     }  

  10.   

  11.     public void increment() {  

  12.         for (int i = 0; i < 10000; i++) {  

  13.             num++;  

  14.         }  

  15.     }  

  16.   

  17.     public void decrement() {  

  18.         for (int i = 0; i < 10000; i++) {  

  19.             num--;  

  20.         }  

  21.     }  

  22. }  

  23.   

  24. public class Test {  

  25.   

  26.     public static void main(String[] args) {  

  27.         MyThread thread = new MyThread();  

  28.         Thread a = new Thread(thread, "t1");  

  29.         Thread b = new Thread(thread, "t2");  

  30.   

  31.         a.start();  

  32.         b.start();  

  33.   

  34.         try {  

  35.             a.join();  

  36.             b.join();  

  37.         } catch (Exception e) {  

  38.             e.printStackTrace();  

  39.         }  

  40.   

  41.         System.out.println(thread.num);  

  42.     }  

  43. }  


從上面代碼可以看出,這裡有兩個線程,其中一個對num執行1000次加1操作,另一個線程執行1000次減1操作,按理說最後num的值是不變的,但是當你運行後,發現num的值可能並不是初始值。那麼為什麼會有這種問題呢?這是記憶體不可見引起的。

650) this.width=650;" src="http://img.blog.csdn.net/20150814213739108" alt="這裡寫圖片描述" />

從中我們可以看到,當線程1對num值加一以後,還未把最新值寫入主記憶體,CPU就停止了線程1的執行,並且執行線程2,線程2首先從主記憶體中擷取num的值,然後減一,最後把值更新到主記憶體中,這個時候,CPU終止了線程2的執行,轉而繼續執行線程1, 這個時候線程1把最新值刷入主記憶體,所以主記憶體結果變為了1000001.

通過上面的分析,我們得知:記憶體不可見是由於共用變數的值沒有及時在主記憶體中更新,為什麼沒有及時更新呢?是因為加一(或者減一)的操作不具備原子性(例子中最後一步被打斷)。那麼如何保證操作具有原子性呢?這裡我們引入synchronized關鍵字。

3、Synchronized關鍵字

synchronized用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多隻有一個線程執行這段代碼。當兩個並發線程訪問同一個對象object中的這個加鎖同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。然而,當一個線程訪問object的一個加鎖代碼塊時,另一個線程仍然可以訪問該object中的非加鎖代碼塊。

JMM關於synchronized的兩條規定:

1. 線程解鎖前,必須把共用變數的最新值重新整理到主記憶體中
2. 線程加鎖時,講清空工作記憶體中共用變數的值,從而使用共用變數時需要從主記憶體中重新讀取最新的值。

這樣,線程解鎖前對共用變數的修改在下次加鎖時對其他線程可見。

所以,為了保證num在某個時刻的修改具有原子性,我們可以在下面兩個方法前加synchronized. 如果你zhi‘dui


[java] view plain copy

  1. public synchronized void increment() {  

  2.     for (int i = 0; i < 10000; i++) {  

  3.         num++;  

  4.     }  

  5. }  

  6.   

  7. public synchronized void decrement() {  

  8.     for (int i = 0; i < 10000; i++) {  

  9.         num--;  

  10.     }  

  11. }  

因為synchronized的本質是一把鎖,所以我們還可以通過真正意義上的加鎖和開鎖來實現記憶體可見度。代碼如下:


[java] view plain copy

  1. class MyThread implements Runnable {  

  2.   

  3.     int num = 1000000;  

  4.     Lock lock = new ReentrantLock();  

  5.   

  6.     public void run() {  

  7.         if (Thread.currentThread().getName().equals("t1")) {  

  8.             increment();  

  9.         } else {  

  10.             decrement();  

  11.         }  

  12.     }  

  13.   

  14.     public void increment() {  

  15.         for (int i = 0; i < 10000; i++) {  

  16.             lock.lock();  

  17.             num++;  

  18.             lock.unlock();  

  19.         }  

  20.     }  

  21.   

  22.     public void decrement() {  

  23.         for (int i = 0; i < 10000; i++) {  

  24.             lock.lock();  

  25.             num--;  

  26.             lock.unlock();  

  27.         }  

  28.     }  

  29. }  

你可能會問:我們可否在num前面加volatile 達到記憶體可見度呢? 答案是否定的,volatile實現共用變數記憶體可見度有一個條件,就是對共用變數的操作必須具有原子性。比如 num = 10; 這個操作具有原子性,但是 num++ 或者num--由3步組成,並不具有原子性,所以是不行的。

參考:

http://www.infoq.com/cn/articles/java-memory-model-1  (本文第一部分主要來自這篇文章)

http://blog.csdn.net/xingjiarong/article/details/47603813 (本文第二部分主要來自這篇文章,代碼做了修改)
http://baike.baidu.com/item/synchronized

http://www.imooc.com/learn/352


Java多線程之記憶體可見度

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.