能夠在語言級支持多線程是Java語言的一大優點,這種支持主要集中在同步上,或調節多個線程間的活動和共享數據。Java所使用的同步是監視器
。java
Monitor
Java中的監視器支持兩種線程:互斥和協做bash
Object
類的wait
方法和notify
方法來實現,容許多個線程爲了同一個目標而共同工做咱們能夠把監視器
比做一個建築,它有一個很特別的房間,房間裏有一些數據,並且在同一時間只能被一個線程佔據。一個線程從進入這個房間到它離開以前,它能夠獨佔地訪問房間中的所有數據。多線程
咱們用一些術語來定義這一系列動做:ide
進入監視器
得到監視器
持有監視器
釋放監視器
退出監視器
除了與一些數據關聯外,監視器仍是關聯到一些或更多的代碼,這樣的代碼稱做監視區域
,對於一個監視器來講,監視區域
是最小的、不可分割的代碼塊。而監視器
會保證在監視區域
上同一時間只會執行一個線程。一個線程想要進入監視器
的惟一途徑就是到達該監視器
所關聯的一個監視區域
的開始處,而線程想要繼續執行監視區域
的惟一途徑就是得到該監視器
。ui
當一個線程到達了一個監視區域的開始處,它就會被放置到該監視器的入口區。若是沒有其餘線程在入口區等待,也沒有線程持有該監視器,則這個線程就能夠得到監視器,並繼續執行監視區域中的代碼。當這個線程執行完監視區域後,它就會退出(並釋放)該監視器。this
若是一個線程到達了一個一個監視區域的開始處,犯這個監視區域已經有線程持有該監視器了,則這個剛剛到達的線程必須在入口區等待。當監視器的持有者退出監視器後,新到達的線程必須與其它已經在入口區等待的線程進行一次比賽,最終只會有一個線程得到監視器。spa
當一個線程須要一些特別狀態的數據,而由另外一個線程負責改變這些數據的狀態時,同步就顯得特別重要。.net
舉例:一個讀線程
會從緩衝區
讀取數據,而另外一個寫線程
會向緩衝區
填充數據。讀線程
須要緩衝區
處於一個非空的狀態,這樣才能夠從中讀取數據,若是讀線程
發現緩衝區
是空的,它就必須等待。寫線程
負責向緩衝區
寫數據,只有寫線程
寫入完成,讀線程
才能相應的讀取。線程
Java虛擬機使用的這種監視器被稱做等待-喚醒
監視器。在這種監視器中,在一個線程(方便區分,叫線程A
)持有監視器的狀況下,能夠經過執行一個等待命令
,暫停自身的執行。
當線程A
執行了等待命令
後,它就會釋放監視器,並進入一個等待區,這個線程A
會一直持續暫停狀態,直到一段時間後,這個監視器中的其餘線程執行了喚醒命令
。
當一個線程(線程B
)執行了喚醒命令
後,它會繼續持有監視器,直到他主動釋放監視器(執行完監視區域或執行一個等待命令)。當執行喚醒的線程(線程B
)釋放了監視器後,等待線程(線程A
)會甦醒,並從新得到監視器。翻譯
等待-喚醒
監視器有時也被稱做發信號並繼續
(這個翻譯沒誰了。。。。)監視器,究其緣由,就是在一個線程執行喚醒操做後,它還會繼續持有監視器並繼續執行監視區域,過了段時間以後,喚醒線程釋放監視器,等待線程纔會甦醒。
因此一次喚醒每每會被等待線程
看做是一次提醒,告訴它「數據已是你想要的狀態了」。當等待線程
甦醒後,它須要再次檢查狀態,以確認是否能夠繼續完成工做,若是數據不是它所須要的狀態,等待線程
可能會再次執行等待命令或者放棄等待退出監視器
。
仍是上面的例子:一個讀線程
、一個緩衝區
、一個寫線程
。假定緩衝區
是由某個監視器
所保護的,當讀線程
進入這個監視器
時,它會檢查緩衝區
是否爲空:
等待區
。這樣讀線程釋放了監視器,讓其餘線程有機會能夠進入。稍後,寫線程進入了監視器,向緩衝區寫入了一些數據,而後執行喚醒,並退出監視器。當寫線程執行了喚醒指令後,讀線程被標誌爲可能甦醒,當寫線程退出監視器後,讀線程被喚醒併成爲監視器的持有者。
Java虛擬機中的監視器模型分紅了三個區域。以下圖:
虛擬機將監視器分爲三個區域:監視區域
只容許一個單獨的線程,是監視器的持有者;入口區
等待區
等待線程和活動線程使用紅色和藍色區分。
模型中也規定了線程和監視器交互所必須經過的幾道門:
1號箭頭
進入入口區
,當進入入口區
後
2號箭頭
,並持有監視器。做爲監視器的持有者,它能夠繼續執行監視區域
中的代碼。等待區
中,這些線程會一直在那裏,直到監視區域
中的活動線程
釋放監視器活動線程
會經過兩條途徑釋放監視器:
活動線程
執行完了監視區域
的代碼,它會從5號箭頭
退出監視器。活動線程
執行了等待命令,它會經過3號箭頭
進入等待區,並釋放監視器活動線程
在釋放監視器前沒有執行喚醒命令(同時在此以前沒有任何等待區的線程被喚醒並等待甦醒),那麼位於入口區的線程們將會競爭得到監視器。活動線程
在釋放監視器前執行了喚醒命令,入口區
的線程就不得不和等待區
的線程一塊兒來競爭:
入口區
的線程獲勝,它就會經過2號箭頭
進入監視區域
,並得到監視器
等待區
的線程獲勝,它會經過4號箭頭
退出等待區並從新得到監視器。請注意,==一個線程只有經過3號箭頭
和4號箭頭
才能進入或退出等待區。而且一個線程只有在它持有監視器的時候才能執行等待命令,並且它只能經過再次成爲監視器的持有者才能離開等待區。==
在Java虛擬機中,線程在執行等待命令時能夠隨意指定一個暫停之間。在暫停時間到了以後,即便沒有來自其餘線程的明確的喚醒命令,它也會自動甦醒。看下面這段代碼:
public class MonitorTest {
public static void main(String[] args) {
byte[] buffer = new byte[4];
MonitorObj monitorObj = new MonitorObj();
Thread read00 = new Thread() {
@Override
public void run() {
System.out.println("read00 準備獲取鎖");
synchronized (monitorObj) {
System.out.println("read00 = " + buffer[3]);
try {
Thread.sleep(1000);
monitorObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("read00 = " + buffer[3]);
}
System.out.println("read00 釋放鎖");
}
};
Thread read01 = new Thread() {
@Override
public void run() {
System.out.println("read01 準備獲取鎖");
synchronized (monitorObj) {
System.out.println("read01 = " + buffer[3]);
try {
Thread.sleep(1000);
monitorObj.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("read01 = " + buffer[3]);
}
System.out.println("read01 釋放鎖");
}
};
Thread write = new Thread() {
@Override
public void run() {
System.out.println("write 準備獲取鎖");
synchronized (monitorObj) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer[3] = 99;
//monitorObj.notifyAll();
try {
Thread.sleep(3000);
System.out.println("write thread finish");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
read00.start();
read01.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
write.start();
}
}
class MonitorObj {
}
複製代碼
請注意,read01
線程使用的是wait(2000)
方法;read00
線程使用的是wait()
方法。
而後,咱們把write
線程的monitorObj.notifyAll()
喚醒方法註釋掉,輸出以下:
read00 準備獲取鎖
read01 準備獲取鎖
read00 = 0
read01 = 0
write 準備獲取鎖
write thread finish
read01 = 99
read01 釋放鎖
複製代碼
由於wait(2000)
加了暫停時間的緣由,read01
仍是自動喚醒了。而對於read00
仍然而且會一直處於等待,除非調用喚醒指令notify()
或notifyAll()
。
而對於notify()
和notifyAll()
的使用,請注意==只有當絕對確認只會有一個線程在等待區中掛起的時候,纔可使用notify
(notifyAll
也能夠);只要同時存在有多個線程在等待區中被掛起,就應該使用notifyAll
==
前面講過,Java虛擬機的一些運行時數據區會被全部線程共享,像方法區
和堆
。因此,Java程序須要爲這兩種多線程下的數據訪問進行協調:
程序不須要考慮Java棧中的局部變量,由於是線程私有的。
在Java虛擬機中,每一個對象和類在邏輯上都有一個監控器與之相關聯的。
若是一個對象沒有實例變量,或者一個類沒有類變量,相關聯的監視器就什麼都不監視。
爲了實現監視器的排他性監視能力,Java虛擬機爲每個對象和類都關聯了一個鎖(有時候被稱爲互斥體mutex
)。一個鎖就像就像一種任什麼時候候只容許一個線程擁有的特權。
鎖住一個對象,其實就是獲取對象相關聯的監視器。而類鎖實際上也是用對象鎖來實現的。咱們前面說過,當虛擬機裝載一個class文件時,它會建立一個java.lang.Class
類的實例。當鎖住一個類時,實際上鎖住的的就是那個類的Class對象。
一個線程能夠容許屢次對同一個對象上鎖(可重入)。對於每個對象來講,Java虛擬機維護了一個計數器,記錄對象被加了多少次鎖:
==監視器可以實現攔截線程,保證監視區域只有一個線程在工做。靠的就是對象鎖==
在Java虛擬機中,每個監視區域
都和一個對象引用相關聯。因此整個流程差很少是這樣子的:
入口區
監視區域
的對象引用,找到對應的數據
監視區域
沒有活動線程
,能夠(多個線程的話須要競爭)加鎖並經過2號箭頭
進入監視區域,執行後續代碼。監視區域
正在被佔用,線程就要在入口區
等待,等待鎖的數值變爲0,和其餘線程(若是有的話)競爭進入監視區域
後,不論是如何離開的,它都會釋放相關對象上的鎖。監視區域
的處理==怎麼定義上面提到的監視區域
呢?==
Java中的關鍵字synchronized
就是用來定義監視區域
的關鍵。synchronized
能夠用來定義同步語句
和同步方法
被synchronized
包裹起來的代碼塊就是同步語句
,像下面這樣:
public class SynchronizeTest {
private int[] array = new int[]{1, 2, 3, 4};
public void expandArray() {
synchronized (this) {
for (int i = 0; i < array.length; i++) {
array[i] = array[i] * 10;
}
System.out.println(Arrays.toString(array));
}
}
}
複製代碼
對於上面的同步代碼塊來講,虛擬機要保證無論線程以什麼樣的形式退出,必需要及時釋放鎖。
==怎麼保證呢?==
假如上面的代碼array[i] = array[i] * 10;
不當心寫成了array[i] = array[i]/0;
,當執行到這一步的時候就要報java.lang.ArithmeticException
異常了。
對於可能拋出的異常來講,咱們會使用try-catch
進行捕獲,編譯器的作法也是同樣的。
咱們看下javap -p SynchronizeTest.class
後expandArray()
部分輸出:
public void expandArray();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=4, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: iconst_0
5: istore_2
6: iload_2
7: aload_0
8: getfield #2 // Field array:[I
11: arraylength
12: if_icmpge 35
15: aload_0
16: getfield #2 // Field array:[I
19: iload_2
20: aload_0
21: getfield #2 // Field array:[I
24: iload_2
25: iaload
26: iconst_0
27: idiv
28: iastore
29: iinc 2, 1
32: goto 6
35: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
38: aload_0
39: getfield #2 // Field array:[I
42: invokestatic #4 // Method java/util/Arrays.toString:([I)Ljava/lang/String;
45: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
48: aload_1
49: monitorexit
50: goto 58
53: astore_3
54: aload_1
55: monitorexit
56: aload_3
57: athrow
58: return
Exception table:
from to target type
4 50 53 any
53 56 53 any
複製代碼
請注意Exception table
這個異常表,這個就是編譯器細心爲咱們加上的。它會監遵從方法的第4條指令
到第50條指令
執行過程當中的any
異常,出現異常就跳到第53條指令
。
咱們能夠看到53
日後還有一個monitorexit
在等待執行(這個any
說明啥異常也阻止不了釋放鎖的決心啊)。
==是否是感受編譯器真滴很貼心哇,贊!==
若是以爲不真實的話咱們把synchronized
代碼塊去掉,再編譯一次看下字節碼信息,你會發現Exception table
也被清除了。
仍是上面的類SynchronizeTest
,此次咱們把方法改爲這樣:
public synchronized void expandArray() {
for (int i = 0; i < array.length; i++) {
array[i] = array[i] / 0;
}
System.out.println(Arrays.toString(array));
}
複製代碼
咱們再來看下相關的字節碼:
public synchronized void expandArray();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=4, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: aload_0
4: getfield #2 // Field array:[I
7: arraylength
8: if_icmpge 31
11: aload_0
12: getfield #2 // Field array:[I
15: iload_1
16: aload_0
17: getfield #2 // Field array:[I
20: iload_1
21: iaload
22: iconst_0
23: idiv
24: iastore
25: iinc 1, 1
28: goto 2
31: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
34: aload_0
35: getfield #2 // Field array:[I
38: invokestatic #4 // Method java/util/Arrays.toString:([I)Ljava/lang/String;
41: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: return
複製代碼
咱們看下區別(本人就找到了3條):
monitorenter
和monitorexit
都不見了。ACC_SYNCHRONIZED
的flag
因爲編譯器在同步語句
裏表現那麼好,我來揣測一下它這麼作的理由哈?
ACC_SYNCHRONIZED
的標記符號引用
時,它會判斷這個方法是不是同步的(根據ACC_SYNCHRONIZED
標記)類Class實例
相關聯的鎖從字節碼指令上來看:
monitorenter
和motorexit
等指令同步方法字節碼更簡潔,看上去更高效一些。
==但真的是這樣嗎?==
Amdahl 定律瞭解一下
speed=\frac{1}{F+\frac{1-F}{N}}
複製代碼
N 表示處理器,F 表示必須串行的部分
當N趨近於無窮大時,
speed = \frac{1}{F}
複製代碼
你懂得。。
Object
的協調支持Object
一些方法咱們前面已經用過了,統一整理一下。下次讓你介紹Object
中定義的方法就能夠把下面這幾個說一下了:
方法 | 描述 |
---|---|
void wait() | 進入監視器的等待區,直到被其餘線程喚醒 |
void wait(long timeout) | 進入監視器的等待區,直到被其餘線程喚醒。或者通過timeout指定的毫秒後,自動甦醒 |
void wait(long timeout,int nanos) | 進入監視器的等待區,直到被其餘線程喚醒。或者通過timeout指定的毫秒加上nanos指定的納秒後,自動甦醒 |
void notify() | 喚醒監視器的等待區中的一個等待線程(若是等待區中沒有線程,那就什麼也不敢) |
void notifyAll() | 喚醒監視器的等待區中的全部等待線程(若是等待區中沒有線程,那就什麼也不敢) |
==上面的5個方法,請在同步語句或同步方法中使用==,否則會報錯喲!
就是這種java.lang.IllegalMonitorStateException
附上Object
類的代碼:
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
}
複製代碼
很簡潔有沒有。。。。。。
==有個疑問哈,這麼多native方法,咋沒看到在哪裏加載的lib呢?==
真滴是個疑問,權當挖個坑,他日必來回復。。。。。
好滴,到這裏虛擬機的線程同步就結束啦,深刻Java虛擬機
也到了尾聲,收穫不少。下一篇好好總結一下吧。