Java多線程之內存可見性

1、什麼是JAVA 內存模型

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


Java內存模型的抽象示意圖如下:

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

1、線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2、線程B到主內存中去讀取線程A之前已更新過的共享變量。
下面通過示意圖來說明這兩個步驟:

如上圖所示,本地內存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的值可能並不是初始值。那麼爲什麼會有這種問題呢?這是內存不可見引起的。

這裏寫圖片描述

從上圖中我們可以看到,當線程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


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