Java 的一個優勢就是在語言層面支持多線程,這種支持集中在協調多線程對數據的訪問上。java
JVM 將運行時數據劃分爲幾個區域:一個或多個棧,一個堆,一個方法區。數組
在 JVM 中,每一個線程擁有一個棧,其餘線程沒法訪問,裏面的數據包括:局部變量,函數參數,線程調用的方法的返回值。棧裏面的數據只包含原生數據類型和對象引用。在 JVM 中,不可能將實際對象的拷貝放入棧。全部對象都在堆裏面。多線程
JVM 只有一個堆,全部線程都共享它。堆中只包含對象,把單獨的原生類型或者對象引用放入堆也是不可能的,除非它們是對象的一部分。數組也在堆中,包括原生類型的數組,由於在 Java 中,數組也是對象。函數
除了棧和堆,另外一個存放數據的區域就是方法區了,它包含程序中使用到的全部類(靜態)變量。方法區相似於棧,也只包含原生類型和對象引用,可是又跟棧不一樣,方法區中類變量是線程共享的。this
正如前面所說,JVM 中的兩個區域包含線程共享的數據,分別是:spa
若是多個線程須要同時使用同一個對象或者類變量,它們對數據的訪問必須被恰當地控制。不然,程序會產生不可預測的行爲。線程
爲了協調多個線程對共享數據的訪問,JVM 給每一個對象和類關聯了一個鎖。鎖就像是任意時間點只有一個線程可以擁有的特權。若是一個線程想要鎖住一個特定的對象或者類,它須要向 JVM 請求鎖。線程向 JVM 請求鎖以後,可能很快就拿到,或者過一會就拿到,也可能永遠拿不到。當線程不須要鎖以後,它把鎖還給 JVM。若是其餘線程須要這個鎖,JVM 會交給該線程。code
類鎖的實現其實跟對象鎖是同樣的。當 JVM 加載類文件的時候,它會建立一個對應類java.lang.Class
對象。當你鎖住一個類的時候,你其實是鎖住了這個類的Class
對象。對象
線程訪問對象實例或者類變量的時候不須要獲取鎖。可是若是一個線程獲取了一個鎖,其餘線程不能訪問被鎖住的數據,直到擁有鎖的線程釋放它。同步
JVM 使用鎖和管程協做。管程監視一段代碼,保證一個時間點內只有一個線程能執行這段代碼。
每一個管程與一個對象引用關聯。當線程到達管程監視代碼段的第一條指令時,線程必須獲取關聯對象的鎖。線程不能執行這段代碼直到它獲得了鎖。一旦它獲得了鎖,線程能夠進入被保護的代碼段。
當線程離開被保護的代碼塊,無論是如何離開的,它都會釋放關聯對象的鎖。
一個線程被容許鎖定一個對象屢次。對於每一個對象,JVM 維護了一個鎖的計數器。沒有被鎖的對象計數爲 0。當一個線程第一次獲取鎖,計數器自增變爲 1。每次這個線程(已經獲得鎖的線程)請求同一個對象的鎖,計數器都會自增。每次線程釋放鎖,計數器都會自減。當計數器變爲 0 時,鎖才被釋放,能夠給別的線程使用。
在 Java 語言的術語中,協調多個線程訪問共享數據被稱爲同步(synchronization)。Java 提供了兩種內建的方式來同步對數據的訪問:
爲了建立同步語句,你須要使用synchronized
關鍵字,括號裏面是同步的對象引用,以下所示:
class KitchenSync { private int[] intArray = new int[10]; void reverseOrder() { synchronized (this) { int halfWay = intArray.length / 2; for (int i = 0; i < halfWay; ++i) { int upperIndex = intArray.length - 1 - i; int save = intArray[upperIndex]; intArray[upperIndex] = intArray[i]; intArray[i] = save; } } } }
在上面的例子中,被同步塊包含的語句不會被執行,直到線程獲得this
引用的對象鎖。若是不是鎖住this
引用,而是鎖住其餘對象,在線程執行同步塊語句以前,它須要得到該對象的鎖。
有兩個字節碼monitorenter
和monitorexit
,被用來同步方法中的同步塊。
字節碼 | 操做數 | 描述 |
---|---|---|
monitorenter | 無 | 取出對象引用,請求與對象引用關聯的鎖 |
monitorexit | 無 | 取出對象引用,釋放與對象引用關聯的鎖 |
當monitorenter
被 JVM 執行時,它請求棧頂對象引用關聯的鎖。若是該線程已經擁有該對象的鎖,計數器自增。每次monitorexit
被執行,計數器自減。當計數器變爲 0 時,該鎖被釋放。
注意:當同步塊中拋出異常時,catch
語句保證對象鎖被釋放。無論同步塊是如何退出的,JVM 保證線程會釋放鎖。
爲了同步整個方法,你只須要在方法聲明前面加上synchronized
關鍵字。
class HeatSync { private int[] intArray = new int[10]; synchronized void reverseOrder() { int halfWay = intArray.length / 2; for (int i = 0; i < halfWay; ++i) { int upperIndex = intArray.length - 1 - i; int save = intArray[upperIndex]; intArray[upperIndex] = intArray[i]; intArray[i] = save; } } }
JVM 不會使用特殊的字節碼來調用同步方法。當 JVM 解析方法的符號引用時,它會判斷方法是否是同步的。若是是,JVM 要求線程在調用以前請求鎖。對於實例方法,JVM 要求獲得該實例對象的鎖。對於類方法,JVM 要求獲得類鎖。在同步方法完成以後,無論它是正常返回仍是拋出異常,鎖都會被釋放。