本節包括wait(),notify(),notifyAll()介紹。以及爲何notify,wait等方法要定義在Object中而不是Thread中。java
在Object中,定義了wait(), notify()和notifyAll()等接口。wait()的做用是讓當前線程進入等待狀態,同時,wait()也會讓當前線程釋放它所持有的鎖。而notify()和notifyAll()的做用,則是喚醒當前對象上的等待線程;notify()是喚醒單個線程,而notifyAll()是喚醒全部的線程。多線程
Object類中關於等待/喚醒的API詳細信息以下:
notify() -- 喚醒在此對象監視器上等待的單個線程。
notifyAll() -- 喚醒在此對象監視器上等待的全部線程。
wait() -- 讓當前線程處於「等待(阻塞)狀態」,「直到其餘線程調用此對象的 notify() 方法或 notifyAll() 方法」,當前線程被喚醒(進入「就緒狀態」)。
wait(long timeout) -- 讓當前線程處於「等待(阻塞)狀態」,「直到其餘線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量」,當前線程被喚醒(進入「就緒狀態」)。
wait(long timeout, int nanos) -- 讓當前線程處於「等待(阻塞)狀態」,「直到其餘線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其餘某個線程中斷當前線程,或者已超過某個實際時間量」,當前線程被喚醒(進入「就緒狀態」)。ide
// WaitTest.java的源碼 class ThreadA extends Thread{ public ThreadA(String name) { super(name); } public void run() { synchronized (this) { System.out.println(Thread.currentThread().getName()+" call notify()"); // 喚醒當前的wait線程 notify(); } } } public class WaitTest { public static void main(String[] args) { ThreadA t1 = new ThreadA("t1"); synchronized(t1) { try { // 啓動「線程t1」 System.out.println(Thread.currentThread().getName()+" start t1"); t1.start(); // 主線程等待t1經過notify()喚醒。 System.out.println(Thread.currentThread().getName()+" wait()"); t1.wait(); System.out.println(Thread.currentThread().getName()+" continue"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
運行結果:函數
main start t1 main wait() t1 call notify() main continue
結果說明:oop
注意:t1.wait()方法時引發「當前線程」等待,直到另一個線程調用notify()或notifyAll()喚醒該線程。意思也就是t1.wait()讓主線程(當前線程)等待而不是t1等待。源碼分析
Object中的wait(), notify()等函數,和synchronized同樣,會對「對象的同步鎖」進行操做。this
wait()會使「當前線程」等待,由於線程進入等待狀態,因此線程應該釋放它鎖持有的「同步鎖」,不然其它線程獲取不到該「同步鎖」而沒法運行!線程調用wait()以後,會釋放它鎖持有的「同步鎖」;並且,根據前面的介紹,咱們知道:等待線程能夠被notify()或notifyAll()喚醒。spa
責喚醒等待線程的那個線程(咱們稱爲「喚醒線程」),它只有在獲取「該對象的同步鎖」(這裏的同步鎖必須和等待線程的同步鎖是同一個),而且調用notify()或notifyAll()方法以後,才能喚醒等待線程。雖然,等待線程被喚醒;可是,它不能馬上執行,由於喚醒線程還持有「該對象的同步鎖」。必須等到喚醒線程釋放了「對象的同步鎖」以後,等待線程才能獲取到「對象的同步鎖」進而繼續運行。線程
總之,notify(), wait()依賴於「同步鎖」,而「同步鎖」是對象鎖持有,而且每一個對象有且僅有一個!這就是爲何notify(), wait()等函數定義在Object類,而不是Thread類中的緣由。code
線程讓步:讓當前線程由「運行狀態」進入到「就緒狀態」,從而讓其餘具備相同優先級的等待線程獲取執行權。可是在當前線程調用yield()以後,其餘具備相同優先級的線程就必定能獲取執行權,也有多是當前線程獲取到了執行權從而又進入到「運行狀態」繼續執行。這與線程調度器有關。
// YieldTest.java的源碼 class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public synchronized void run(){ for(int i=0; i <10; i++){ System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i); // i整除4時,調用yield if (i%4 == 0) Thread.yield(); } } } public class YieldTest{ public static void main(String[] args){ ThreadA t1 = new ThreadA("t1"); ThreadA t2 = new ThreadA("t2"); t1.start(); t2.start(); } }
運行結果(每一次運行結果可能不一致):
t1 [5]:0 t2 [5]:0 t1 [5]:1 t1 [5]:2 t1 [5]:3 t1 [5]:4 t1 [5]:5 t1 [5]:6 t1 [5]:7 t1 [5]:8 t1 [5]:9 t2 [5]:1 t2 [5]:2 t2 [5]:3 t2 [5]:4 t2 [5]:5 t2 [5]:6 t2 [5]:7 t2 [5]:8 t2 [5]:9
結果說明:
「線程t1」在能被4整數的時候,並無切換到「線程t2」。這代表,yield()雖然可讓線程由「運行狀態」進入到「就緒狀態」;可是,它不必定會讓其它線程獲取CPU執行權(即,其它線程進入到「運行狀態」),即便這個「其它線程」與當前調用yield()的線程具備相同的優先級。
咱們知道,wait()的做用是讓當前線程由「運行狀態」進入「等待(阻塞)狀態」的同時,也會釋放同步鎖。而yield()的做用是讓步,它也會讓當前線程離開「運行狀態」。它們的區別是:
(01) wait()是讓線程由「運行狀態」進入到「等待(阻塞)狀態」,而不yield()是讓線程由「運行狀態」進入到「就緒狀態」。
(02) wait()是會線程釋放它所持有對象的同步鎖,而yield()方法不會釋放鎖。
線程休眠:當線程調用sleep()方法後,當前線程休眠,即當前線程從「運行狀態」進入到「休眠(阻塞)狀態」。sleep()會指定休眠時間,線程休眠的時間會大於/等於該休眠時間;在線程從新被喚醒時,它會從「阻塞狀態」進入「就緒狀態」,從而等待CPU的調度執行。
// SleepTest.java的源碼 class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public synchronized void run() { try { for(int i=0; i <10; i++){ System.out.printf("%s: %d\n", this.getName(), i); // i能被4整除時,休眠100毫秒 if (i%4 == 0) Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class SleepTest{ public static void main(String[] args){ ThreadA t1 = new ThreadA("t1"); t1.start(); } }
運行結果;
t1: 0 t1: 1 t1: 2 t1: 3 t1: 4 t1: 5 t1: 6 t1: 7 t1: 8 t1: 9
結果說明:
程序比較簡單,在主線程main中啓動線程t1。t1啓動以後,當t1中的計算i能被4整除時,t1會經過Thread.sleep(100)休眠100毫秒。
咱們知道,wait()的做用是讓當前線程由「運行狀態」進入「等待(阻塞)狀態」的同時,也會釋放同步鎖。而sleep()的做用是也是讓當前線程由「運行狀態」進入到「休眠(阻塞)狀態」。
可是,wait()會釋放對象的同步鎖,而sleep()則不會釋放鎖。
join() 定義在Thread.java中。
join() 的做用:讓「主線程」等待「子線程」結束以後才能繼續運行。
public final void join() throws InterruptedException { join(0); } 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; } } }
說明:
從代碼中,咱們能夠發現。當millis==0時,會進入while(isAlive())循環;即只要子線程是活的,主線程就不停的等待。
wait方法的做用是讓「當前線程」等待,而這裏的「當前線程」是指當前在CPU上運行的線程。因此,雖然是調用子線程的wait()方法,可是它是經過「主線程」去調用的;因此,休眠的是主線程,而不是「子線程」!而join方法時讓主線程等待,不是CPU上執行的線程。
// JoinTest.java的源碼 public class JoinTest{ public static void main(String[] args){ try { ThreadA t1 = new ThreadA("t1"); // 新建「線程t1」 t1.start(); // 啓動「線程t1」 t1.join(); // 將「線程t1」加入到「主線程main」中,而且「主線程main()會等待它的完成」 System.out.printf("%s finish\n", Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } static class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public void run(){ System.out.printf("%s start\n", this.getName()); // 延時操做 for(int i=0; i <1000000; i++) ; System.out.printf("%s finish\n", this.getName()); } } }
運行結果:
t1 start t1 finish main finish
結果說明:
interrupt()的做用是中斷本線程。
本線程中斷本身是被容許的;其它線程調用本線程的interrupt()方法時,會經過checkAccess()檢查權限。這有可能拋出SecurityException異常。
若是本線程是處於阻塞狀態:調用線程的wait(), wait(long)或wait(long, int)會讓它進入等待(阻塞)狀態,或者調用線程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也會讓它進入阻塞狀態。
若線程在阻塞狀態時,調用了它的interrupt()方法,那麼它的「中斷狀態」會被清除而且會收到一個InterruptedException異常。例如,線程經過wait()進入阻塞狀態,此時經過interrupt()中斷該線程;調用interrupt()會當即將線程的中斷標記設爲「true」,可是因爲線程處於阻塞狀態,因此該「中斷標記」會當即被清除爲「false」,同時,會產生一個InterruptedException的異常。
若是線程被阻塞在一個Selector選擇器中,那麼經過interrupt()中斷它時;線程的中斷標記會被設置爲true,而且它會當即從選擇操做中返回。
若是不屬於前面所說的狀況,那麼經過interrupt()中斷線程時,它的中斷標記會被設置爲「true」。
中斷一個「已終止的線程」不會產生任何操做。
一般,咱們經過「中斷」方式終止處於「阻塞狀態」的線程。
當線程因爲被調用了sleep(), wait(), join()等方法而進入阻塞狀態;若此時調用線程的interrupt()將線程的中斷標記設爲true。因爲處於阻塞狀態,中斷標記會被清除,同時產生一個InterruptedException異常。將InterruptedException放在適當的爲止就能終止線程,形式以下:
@Override public void run() { try { while (true) { // 執行任務... } } catch (InterruptedException ie) { // 因爲產生InterruptedException異常,退出while(true)循環,線程終止! } }
說明:在while(true)中不斷的執行任務,當線程處於阻塞狀態時,調用線程的interrupt()產生InterruptedException中斷。中斷的捕獲在while(true)以外,這樣就退出了while(true)循環!
注意:對InterruptedException的捕獲務通常放在while(true)循環體的外面,這樣,在產生異常時就退出了while(true)循環。不然,InterruptedException在while(true)循環體以內,就須要額外的添加退出處理。形式以下:
@Override public void run() { while (true) { try { // 執行任務... } catch (InterruptedException ie) { // InterruptedException在while(true)循環體內。 // 當線程產生了InterruptedException異常時,while(true)仍能繼續運行!須要手動退出 break; } } }
一般,咱們經過「標記」方式終止處於「運行狀態」的線程。其中,包括「中斷標記」和「額外添加標記」。
(01) 經過「中斷標記」終止線程。
@Override public void run() { while (!isInterrupted()) { // 執行任務... } }
說明:isInterrupted()是判斷線程的中斷標記是否是爲true。當線程處於運行狀態,而且咱們須要終止它時;能夠調用線程的interrupt()方法,使用線程的中斷標記爲true,即isInterrupted()會返回true。此時,就會退出while循環。
注意:interrupt()並不會終止處於「運行狀態」的線程!它會將線程的中斷標記設爲true。
(02) 經過「額外添加標記」終止線程
private volatile boolean flag= true; protected void stopTask() { flag = false; } @Override public void run() { while (flag) { // 執行任務... } }
說明:線程中有一個flag標記,它的默認值是true;而且咱們提供stopTask()來設置flag標記。當咱們須要終止該線程時,調用該線程的stopTask()方法就可讓線程退出while循環。
注意:將flag定義爲volatile類型,是爲了保證flag的可見性。即其它線程經過stopTask()修改了flag以後,本線程能看到修改後的flag的值。
綜合線程處於「阻塞狀態」和「運行狀態」的終止方式,比較通用的終止線程的形式以下:
@Override public void run() { try { // 1. isInterrupted()保證,只要中斷標記爲true就終止線程。 while (!isInterrupted()) { // 執行任務... } } catch (InterruptedException ie) { // 2. InterruptedException異常保證,當InterruptedException異常產生時,線程被終止。 } }
interrupt()經常被用來終止「阻塞狀態」線程。參考下面示例:
// Demo1.java的源碼 class MyThread extends Thread { public MyThread(String name) { super(name); } @Override public void run() { try { int i=0; while (!isInterrupted()) { Thread.sleep(100); // 休眠100ms i++; System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException."); } } } public class Demo1 { public static void main(String[] args) { try { Thread t1 = new MyThread("t1"); // 新建「線程t1」 System.out.println(t1.getName() +" ("+t1.getState()+") is new."); t1.start(); // 啓動「線程t1」 System.out.println(t1.getName() +" ("+t1.getState()+") is started."); // 主線程休眠300ms,而後主線程給t1發「中斷」指令。 Thread.sleep(300); t1.interrupt(); System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted."); // 主線程休眠300ms,而後查看t1的狀態。 Thread.sleep(300); System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now."); } catch (InterruptedException e) { e.printStackTrace(); } } }
運行結果:
t1 (NEW) is new. t1 (RUNNABLE) is started. t1 (RUNNABLE) loop 1 t1 (RUNNABLE) loop 2 t1 (TIMED_WAITING) is interrupted. t1 (RUNNABLE) catch InterruptedException. t1 (TERMINATED) is interrupted now.
結果說明:
(01) 主線程main中經過new MyThread("t1")建立線程t1,以後經過t1.start()啓動線程t1。
(02) t1啓動以後,會不斷的檢查它的中斷標記,若是中斷標記爲「false」;則休眠100ms。
(03) t1休眠以後,會切換到主線程main;主線程再次運行時,會執行t1.interrupt()中斷線程t1。t1收到中斷指令以後,會將t1的中斷標記設置「false」,並且會拋出InterruptedException異常。在t1的run()方法中,是在循環體while以外捕獲的異常;所以循環被終止。
6.總結
線程間同步機制以及中斷機制就先探討到這裏,下面將有一個經典的生產-消費者問題來多線程的具體應用。