在使用多線程的時候,經常需要多個線程進行協作來完成一件事情。在前面兩章分析了Java多線程的基本使用以及利用synchronized
來實現多個線程同步調用方法或者執行代碼塊。但上面兩章的內容涉及到的例子以及使用的多線程代碼都是獨自運行,兩個程序除了競爭同一個對象以外,沒有任何聯系。
這次內容將講解當多個線程需要協作來完成一件事情的時候,如何去等待其他線程執行,又如何當線程執行完去通知其他線程結束等待。
本次主要介紹如下內容:
- 等待/通知機制
- join方法的使用
所有的代碼均在char03線程間通信
等待/通知機制
Java中對多線程類提供了兩個方法來完成等待/通知機制,等待的方法是-wait()
,通知的方法是notify()
。先說一下什么是等待/通知機制,所謂等待/通知機制,就是線程A在執行的時候,需要一個其他線程來提供的結果,但是其他線程還沒有告訴他這個結果是什么,於是線程A開始等待,當其他線程計算出結果之后就將結果通知給線程A,A線程喚醒,繼續執行。這個過程就是等待/通知機制。
等待/通知機制實際上多個線程之間的一種互動,而為了保證這個互動僅限於期望的那些線程,因此需要多個線程擁有一個統一的對象監視器,也就是都要在synchronized(x)
同步代碼塊中執行x.wait
以及x.notify
方法。
如果細心觀察,會發現wait方法和notify方法是Object類自帶的方法。這個原因是因為任何一個對象都能成為監視器,而wait和notify只有對同一個監視器才能起到預期的作用。也就是說任何一個監視器都能用wait以及notify方法,任何對象都有的方法,自然就需要放到Object中
wait方法與notify方法的講解
wait方法會使執行該wait方法的線程停止,直到等到了notify的通知。細說一下,執行了wait方法的那個線程會因為wait方法而進入等待狀態,該線程也會進入阻塞隊列中。而執行了notify那個線程在執行完同步代碼之后會通知在阻塞隊列中的線程,使其進入就緒狀態。被重新喚醒的線程會試圖重新獲得臨界區的控制權,也就是對象鎖,然后繼續執行臨界區也就是同步語句塊中wait之后的代碼。
上面這個描述,可以看出一些細節。
- wait方法進入了阻塞隊列,而上文講過執行notify操作的線程與執行wait的線程是擁有同一個對象監視器,也就說wait方法執行之后,立刻釋放掉鎖,這樣,另一個線程才能執行同步代碼塊,才能執行notify。
- notify線程會在執行完同步代碼之后通知在阻塞隊列中的線程,也就是說notify的那個線程並不是立即釋放鎖,而是在同步方法執行完,釋放鎖以后,wait方法的那個線程才會繼續執行。
- 被重新喚醒的線程會試圖重新獲得鎖,也就說,在notify方法的線程釋放掉鎖以后,其通知的線程是不確定的,看具體是哪一個阻塞隊列中的線程獲取到對象鎖。
下面看一個例子:
public class Service {
public void testMethod(Object lock){
try{
synchronized (lock){
System.out.println("begin wait()");
lock.wait();
System.out.println(" end wait()");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void synNotifyMethod(Object lock){
try{
synchronized (lock){
System.out.println("begin notify() ThreadName=" + Thread.currentThread().getName() +
" time=" +System.currentTimeMillis());
lock.notify();
Thread.sleep(1000 * 1);
System.out.println("end notify() ThreadName=" + Thread.currentThread().getName() +
" time=" + System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
該Service中有兩個方法,一個是testMethod方法,包含了wait方法,另一個是synNotifyMethod方法了notify方法,我們首先看一下,wait方法會釋放鎖的測試。
public class ServiceThread extends Thread{
private Object lock;
public ServiceThread(Object lock){
this.lock = lock;
}
@Override
public void run() {
super.run();
Service service = new Service();
service.testMethod(lock);
}
}
測試方法如下:
public void testRun() throws Exception {
Object lock = new Object();
new ServiceThread(lock).start();
new ServiceThread(lock).start();
Thread.sleep(1000 * 4);
}
結果如下:
begin wait()
begin wait()
很明顯結果是執行了2次同步代碼塊,其執行的原因,就是因為第一個wait之后,釋放掉了對象鎖,所以第二個線程才會執行同步代碼塊。
還是利用上面的代碼,現在我們看一下,notify方法通知等待的線程, 但是不會立即釋放鎖的例子。
public class NotifyServiceThread extends Thread{
private Object lock;
public NotifyServiceThread(Object lock){
this.lock = lock;
}
@Override
public void run() {
super.run();
Service service = new Service();
service.synNotifyMethod(lock);
}
}
測試的例子如下:
public class NotifyServiceThreadTest extends TestCase {
public void testRun() throws Exception {
Object lock = new Object();
ServiceThread a = new ServiceThread(lock);
a.start();
Thread.sleep(1000);
new NotifyServiceThread(lock).start();
new NotifyServiceThread(lock).start();
Thread.sleep(1000 * 10);
}
}
其結果如下:
begin wait()
begin notify() ThreadName=Thread-1 time=1484302436105
end notify() ThreadName=Thread-1 time=1484302437108
end wait()
begin notify() ThreadName=Thread-2 time=1484302437108
end notify() ThreadName=Thread-2 time=1484302438110
測試方法,首先調用上wait的例子,讓ServiceThread線程進入等待狀態,然后執行2個含有notify操作的線程,可以看出,第一個notify執行完,wait線程並沒有立即開始運行,而是Thread-1繼續執行后續的notify方法,直到同步語句塊結束,然后wait線程立即得到鎖,並繼續運行。之后Thread-2開始運行,直到結束,因為已經沒有等待的線程,所以不會有后續的等待的線程運行。
這里,可以看出一個細節,競爭鎖的線程有3個,一個包含wait線程,兩個包含notify線程。第一個notify執行結束,獲得鎖一定是阻塞的線程,而不是另一個notify的線程。
上面的程序展現了等待/通知機制是如何通過wait和notify實現。在這里,我們可以看出wait方法使線程進入等待,和Thread.sleep
是很相似的。但是兩者卻截然不同,區別如下:
- wait使線程進入等待,是可以被通知喚醒的,但是sleep只能自己到時間喚醒。
- wait方法是對象鎖調用的成員方法,而sleep卻是Thread類的靜態方法
- wait方法出現在同步方法或者同步代碼塊中,但是sleep方法可以出現在非同步代碼中。
wait和notify還提供了幾個其他API,如wait(long timeout)
該方法可以提供一個喚醒的時間,如果在時間內,沒有其他線程喚醒該等待線程,則到設定的時間,會自動結束等待。
因為notify僅僅能喚醒一個線程,所以Java提供了一個notifyAll()
的方法來喚醒所有的線程,讓所有的線程來競爭。我們看一下只喚醒一個線程和喚醒所有線程的不同。
public class CommonWait {
private Object object;
public CommonWait(Object object){
this.object = object;
}
public void doSomething() throws Exception{
synchronized (object){
System.out.println("begin wait " + Thread.currentThread().getName());
object.wait();
System.out.println("end wait " + Thread.currentThread().getName());
}
}
}
public class CommonNotify {
private Object object;
public CommonNotify(Object object){
this.object = object;
}
public void doNotify(){
synchronized (object){
System.out.println("准備通知");
object.notify();
System.out.println("通知結束");
}
}
}
測試通知一個等待線程
public void testRun() throws Exception{
Object lock = new Object();
new Thread(()->{
try {
new CommonWait(lock).doSomething();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
new CommonWait(lock).doSomething();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
Thread.sleep(1000);
new Thread(()->{
new CommonNotify(lock).doNotify();
}).start();
Thread.sleep(1000 * 3);
}
結果如下:
begin wait Thread-0
begin wait Thread-1
准備通知
通知結束
end wait Thread-0
結果看來,只有一個線程結束了等待,繼續往下面執行。另一個線程直到結束也沒有執行。
現在看一下notifyAll的效果,把CommonNotify
這個類中的object.notify();
改成object.notifyAll()
其他的不變,看看結果:
begin wait Thread-0
begin wait Thread-1
准備通知
通知結束
end wait Thread-1
end wait Thread-0
很明顯,兩個等待線程都執行了,而且這次Thread-1的線程先執行,可見通知喚醒是隨機的。
這里詳細說一下,這個結果。wait使線程進入了阻塞狀態,阻塞狀態可以細分為3種:
- 等待阻塞:運行的線程執行wait方法,JVM會把該線程放入等待隊列中。
- 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池當中。
- 其他阻塞: 運行的線程執行了
Thread.sleep
或者join
方法,或者發出I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()
狀態超時、join()等待線程終止,或者超時、或者I/O處理完畢時,線程重新轉入可運行狀態。
可運行狀態就是線程執行start
時,就是可運行狀態,一旦CPU切換到這個線程就開始執行里面的run方法就進入了運行狀態。
上面會出現這個結果,就是因為notify僅僅讓一個線程進入了可運行狀態,而另一個線程則還在阻塞中。而notifyAll
則使所有的線程都從等待隊列中出來,而因為同步代碼的關系,獲得鎖的線程進入可運行態,沒有得到鎖的則進入鎖池,也是阻塞狀態,但是會因為鎖的釋放而重新進入可運行態。所以notifyAll會讓所有wait的線程都會繼續執行。
join方法的使用
wait方法使線程進入阻塞,並且因為通知而喚醒執行,sleep方法同樣使線程進入阻塞,並且因此超時而結束阻塞。以上兩者都是因為特定的條件而結束阻塞,現在主線程需要知道子線程的結果再繼續執行,這個時候要怎么做,用通知/等待不是很容易實現這個操作,sleep則完全不知道要等待的時間。因此Java提供了一個join()
方法,join()
方法是Thread對象的方法,他的功能是使所屬的線程對象x正常執行run方法的內容,而使當前線程z進行無限期的阻塞,等待線程x銷毀后在繼續執行線程z后面的代碼。這說起來有點繞口,其實看例子就很簡單。
public class JoinThread extends Thread{
@Override
public void run() {
super.run();
try{
int secondValue = (int)(Math.random() * 10000);
System.out.println(secondValue);
Thread.sleep(secondValue);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
其測試的方法如下:
public void testRun() throws Exception {
JoinThread joinThread = new JoinThread();
joinThread.start();
joinThread.join();
System.out.println("我想當Join對象執行完畢后我再執行,我做到了");
}
結果如下:
3519
我想當Join對象執行完畢后我再執行,我做到了
看上去join方法很神奇,可以實現線程在執行上面的次序。但是實際上join方法內部是通過wait實現的。
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
這個join的原理很簡單,前面那些if條件不管,主要看while循環里面的,while循環就是不斷去判斷this.isAlive
的結果,用上面的例子,這個this就是joinThread
。然后關鍵的代碼就是wait(delay);
一個定時的wait。這個wait的對象也是this,就是joinThread
。上面我們已經講了wait一定要在同步方法或者同步代碼塊中,源碼中join方法的修飾符就是一個synchronized
,表明這是一個同步的方法。
不要看調用wait是joinThread
,是一個線程。但是真正因為wait進入阻塞狀態的,是持有對象監視器的線程,這里的對象監視器是joinThread
,持有他的是main線程,因為在main線程中執行了join這個同步方法。
所以main線程不斷的wait,直到調用join方法那個線程對象銷毀,才繼續向下執行。
但是源碼中只有wait的方法,沒有notify的方法。因為notify這個操作是JVM通過檢測線程對象銷毀而調用的native方法,是C++實現的,在源碼中是找不到對應這個wait方法而存在的notify方法的。
總結
這里介紹了線程間通信的一種常見的方式——等待/通知機制。此外,還介紹了一種指定線程執行順序的方法——join方法,並且講解了其內部的實現。
全部的代碼都在char03線程間通信