前面大致提到了JDK中的一些個原子類,也提到原子類是並發的基礎,更提到所謂的線程安全,其實這些類或者並發包中的這么一些類,都是為了保證系統在運行時是線程安全的,那到底怎么樣才算是線程安全呢?
Java並發與實踐一書中提出,當多個線程同時訪問一個類的時候,如果不用考慮這些線程在運行時環境下的調度和交替運行,並且不需要做額外的同步以及在調用代碼時不需要做其他的協調,這個類的運行仍然是正確的,那么這個類是線程安全的。
很顯然只有資源競爭時才會出現線程不安全,而無狀態的類將永遠是線程安全的。因此我們再做分層結果的時候,Service層可以輕松的使用單例去顯示,而展示層和數據層卻需要每個單獨的線程單獨一個對象去處理。
之前說了這么一些原子類,他們都是線程安全的類,原子操作的描述是多個線程執行同一個操作時,其中一個線程要么完全執行完成這個操作,要么根本沒有執行任何步驟。
在JDK中,JAVA語言為了維持順序內部的順序化語義,也就是為了保證程序的最終運行結果需要和在單線程嚴格意義的順序化環境下執行的結果一致,程序指令的執行順序有可能和代碼的順序不一致,這個過程就稱之為指令的重排序。指令重排序的意義在於:JVM能根據處理器的特性,充分利用多級緩存,多核等進行適當的指令重排序,使程序在保證業務運行的同時,充分利用CPU的執行特點,最大的發揮機器的性能!
我們來看一組代碼示例:
package com.yhj.concurrent; /** * @Described:Happen-Before測試 * @author YHJ create at 2013-4-13 下午05:12:36 * @ClassNmae com.yhj.concurrent.HapenBefore */ public class HappenBefore { static int x,y,m,n;//測試用的信號變量 public static void main(String[] args) throws InterruptedException { int count = 10000; for(int i=0;i<count;++i){ x=y=m=n=0; //線程一 Thread one = new Thread(){ public void run() { m=1; x=n; }; }; //線程二 Thread two = new Thread(){ public void run() { n=1; y=m; }; }; //啟動並等待線程執行結束 one.start(); two.start(); one.join(); two.join(); //輸出結果 System.out.println("index:"+i+" {x:"+x+",y:"+y+"}"); } } }
這段代碼循環1w次, 每次啟動兩個線程去修改x、y、m、n四個變量,能得到什么結果的呢?運行一下,很容易得到x=1,y=0;x=0,y=1兩種結果,事實上根據JVM規范以及CPU的特性,我們很可能還能得到x=0,y=0或者x=1,y=1的情況。當然上端代碼大家不一定能得到x=0,y=0或者x=1,y=1的結果,這是因為這段代碼太簡單了,以現在CPU 的運算速度,根本無需做線程切換就能將這些很快的執行完畢。x=1,y=1這種情況大家也許還能理解,當發生線程切換時,第一個線程第一行代碼執行完畢,再次執行第二線程的第一行代碼,就會發生x=1,y=1的結果。但x=0,y=0是否可能發生?按照現在的JVM和CPU特性,這種情況的確是存在的。由於線程的run方法里面的動作對結果是無關的,因此里面的指令可能發生指令重排序,即使是按照程序的順序方法執行,數據從線程緩沖區刷新到主存也是需要時間的(之前有提到,原理可參考http://yhjhappy234.blog.163.com/blog/static/316328322011101723933875/,實踐可通過下面方框中的測試代碼驗證)。假定是按照m=1,x=n,n=1,y=m執行的,顯然x=0是很正常的,m=1雖然在y=m之前執行,但是線程one有可能還沒來得及將m=1的數據從高速緩存(work memory)寫入主存,線程two就從主存中取m的數據,所以還有可能是0,這樣就發生了數據錯誤!尤其是在大並發和多核CPU的執行下,數據的結果就更無法確定了!
package com.yhj.jvm.memory.concurrent; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Described:並發常量測試 * @author YHJ create at 2013-04-17 下午08:54:24 * @FileNmae com.yhj.jvm.memory.concurrent.ConcurrentStaticTest.java */ public class ConcurrentStaticTest { public static int counter = 0;//volatile public final static int THRED_COUNT = 20; public static void plus() { counter++; } /** * @param args * @Author YHJ create at 2011-11-17 下午08:54:19 */ public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<THRED_COUNT;++i){ executorService.execute(new Runnable() { @Override public void run() { for(int j = 0;j<10000;++j){ plus(); } } }); } //等待所有進程結束 while(Thread.activeCount()>1){ Thread.yield(); } System.out.println(counter); } }
為了解決此類額外難題,Java存儲模型引入了happens-Before發則,確保並發情況下的數據正確性!通俗的說就是如果動作B要看到動作A的執行結果(無論A/B是否在同一個線程中),那么A/B必須滿足happens-before發則!
在說happens-before發則之前我們還得先看另外一個概念:在Java中還有一個概念叫JMMA(Java Memory Medel Action):Java模型動作。一個Action包含:編寫讀、變量寫、監視器加鎖、釋放鎖、線程啟動(start)、線程等待(join)。關於鎖我們后續會詳細介紹。
說了這么多,那究竟什么是happens-before發則呢?完整的發則如下
(1)同一個線程中的每個Action都happens-before於出現在其后的任何一個Action。
(2)對一個監視器的解鎖happens-before於每一個后續對同一個監視器的加鎖。
(3)對volatile字段的寫入操作happens-before於每一個后續的同一個字段的讀操作。
(4)Thread.start()的調用會happens-before於啟動線程里面的動作。
(5)Thread中的所有動作都happens-before於其他線程檢查到此線程結束或者Thread.join()中返回或者Thread.isAlive()==false。
(6)一個線程A調用另一個另一個線程B的interrupt()都happens-before於線程A發現B被A中斷(B拋出異常或者A檢測到B的isInterrupted()或者interrupted())。
(7)一個對象構造函數的結束happens-before與該對象的finalizer的開始
(8)如果A動作happens-before於B動作,而B動作happens-before與C動作,那么A動作happens-before於C動作。
法則中提到了一個關鍵字volatile,其實我們前面講JVM的時候也多次提到這個關鍵字,今天我們略微擴展一點,因為他對我們后續的CAS理解有很大幫助。
Volatile相當於synchronized的一個弱實現,他實現了synchronized的語義卻沒有鎖機制,它確保對volatile字段的更新以可預見的形式告知其他線程。
Java存儲模型不對對olatile指令的操作做重排序,保證volatile的變量都能按照指令的順序執行。
Volatile類型的變量不會被緩存在寄存器中(寄存器中的數據只有當前線程可以訪問),或者其他對CPU不可見的地方,每次都需要充主存中讀取對應的數據,這保證每次對變量的修改,其他線程也是可見的,而不是僅僅修改自己線程的局部變量,在happens-before發則中,對一個volatile變量進行寫操作后了,此后的任何讀操作都可見該次寫操作的結果。
Volatile關鍵字主要用於以下場景
volatile boolean condition = false; public void method() { while(!condition){ doSth(); } }
應用volatile關鍵字的三個發則
(1)寫入變量不依賴此變量的值,或者只有一個線程修改此變量
(2)變量的狀態不需要與其它變量共同參與不變約束
(3)訪問變量不需要加鎖
Happens-before和volatile是后面鎖和原子操作的基礎,那鎖操作和原子操作是怎么實現的呢?請參考后續連載的章節!