Java多線程基礎篇(04)-線程同步機制和線程間通訊

1.線程的等待與喚醒

本節包括wait(),notify(),notifyAll()介紹。以及爲何notify,wait等方法要定義在Object中而不是Thread中。java

1.1 wait,notify,notifyAll方法介紹

在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

1.2 wait和notify示例

// 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

  • (01) 注意,圖中"主線程" 表明「主線程main」。"線程t1" 表明WaitTest中啓動的「線程t1」。 而「鎖」 表明「t1這個對象的同步鎖」。
  • (02) 「主線程」經過 new ThreadA("t1") 新建「線程t1」。隨後經過synchronized(t1)獲取「t1對象的同步鎖」。而後調用t1.start()啓動「線程t1」。
  • (03) 「主線程」執行t1.wait() 釋放「t1對象的鎖」而且進入「等待(阻塞)狀態」。等待t1對象上的線程經過notify() 或 notifyAll()將其喚醒。
  • (04) 「線程t1」運行以後,經過synchronized(this)獲取「當前對象的鎖」;接着調用notify()喚醒「當前對象上的等待線程」,也就是喚醒「主線程」。
  • (05) 「線程t1」運行完畢以後,釋放「當前對象的鎖」。緊接着,「主線程」獲取「t1對象的鎖」,而後接着運行。

注意:t1.wait()方法時引發「當前線程」等待,直到另一個線程調用notify()或notifyAll()喚醒該線程。意思也就是t1.wait()讓主線程(當前線程)等待而不是t1等待。源碼分析

1.3 爲何wait()函數定義在Obejct中

Object中的wait(), notify()等函數,和synchronized同樣,會對「對象的同步鎖」進行操做。this

wait()會使「當前線程」等待,由於線程進入等待狀態,因此線程應該釋放它鎖持有的「同步鎖」,不然其它線程獲取不到該「同步鎖」而沒法運行!線程調用wait()以後,會釋放它鎖持有的「同步鎖」;並且,根據前面的介紹,咱們知道:等待線程能夠被notify()或notifyAll()喚醒。spa

責喚醒等待線程的那個線程(咱們稱爲「喚醒線程」),它只有在獲取「該對象的同步鎖」(這裏的同步鎖必須和等待線程的同步鎖是同一個),而且調用notify()或notifyAll()方法以後,才能喚醒等待線程。雖然,等待線程被喚醒;可是,它不能馬上執行,由於喚醒線程還持有「該對象的同步鎖」。必須等到喚醒線程釋放了「對象的同步鎖」以後,等待線程才能獲取到「對象的同步鎖」進而繼續運行。線程

總之,notify(), wait()依賴於「同步鎖」,而「同步鎖」是對象鎖持有,而且每一個對象有且僅有一個!這就是爲何notify(), wait()等函數定義在Object類,而不是Thread類中的緣由。code

2.線程讓步方法yield

2.1 線程讓步簡介

線程讓步:讓當前線程由「運行狀態」進入到「就緒狀態」,從而讓其餘具備相同優先級的等待線程獲取執行權。可是在當前線程調用yield()以後,其餘具備相同優先級的線程就必定能獲取執行權,也有多是當前線程獲取到了執行權從而又進入到「運行狀態」繼續執行。這與線程調度器有關。

2.2 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()的線程具備相同的優先級。

2.3 線程讓步yield和線程等待wait的比較

咱們知道,wait()的做用是讓當前線程由「運行狀態」進入「等待(阻塞)狀態」的同時,也會釋放同步鎖。而yield()的做用是讓步,它也會讓當前線程離開「運行狀態」。它們的區別是:
(01) wait()是讓線程由「運行狀態」進入到「等待(阻塞)狀態」,而不yield()是讓線程由「運行狀態」進入到「就緒狀態」。
(02) wait()是會線程釋放它所持有對象的同步鎖,而yield()方法不會釋放鎖。

3.線程休眠sleep

3.1 線程休眠簡介

    線程休眠:當線程調用sleep()方法後,當前線程休眠,即當前線程從「運行狀態」進入到「休眠(阻塞)狀態」。sleep()會指定休眠時間,線程休眠的時間會大於/等於該休眠時間;在線程從新被喚醒時,它會從「阻塞狀態」進入「就緒狀態」,從而等待CPU的調度執行。

3.2 sleep方法示例

// 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毫秒。

3.3 sleep()與wait()的比較

咱們知道,wait()的做用是讓當前線程由「運行狀態」進入「等待(阻塞)狀態」的同時,也會釋放同步鎖。而sleep()的做用是也是讓當前線程由「運行狀態」進入到「休眠(阻塞)狀態」。
可是,wait()會釋放對象的同步鎖,而sleep()則不會釋放鎖。

4. Thead.join方法

4.1 join()方法介紹

join() 定義在Thread.java中。
join() 的做用:讓「主線程」等待「子線程」結束以後才能繼續運行。

4.2 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())循環;即只要子線程是活的,主線程就不停的等待。

4.3 join()方法與wait()方法比較

    wait方法的做用是讓「當前線程」等待,而這裏的「當前線程」是指當前在CPU上運行的線程。因此,雖然是調用子線程的wait()方法,可是它是經過「主線程」去調用的;因此,休眠的是主線程,而不是「子線程」!而join方法時讓主線程等待,不是CPU上執行的線程。

4.4 join()方法示例

// 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

結果說明:

  • (01) 在「主線程main」中經過 new ThreadA("t1") 新建「線程t1」。 接着,經過 t1.start() 啓動「線程t1」,並執行t1.join()。
  • (02) 執行t1.join()以後,「主線程main」會進入「阻塞狀態」等待t1運行結束。「子線程t1」結束以後,會喚醒「主線程main」,「主線程」從新獲取cpu執行權,繼續運行。

5 線程中斷和終止方式

5.1 中斷interrupt()說明

    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」。
    中斷一個「已終止的線程」不會產生任何操做。

5.2 終止線程

    5.2.1 終止處於「阻塞狀態」的線程

    一般,咱們經過「中斷」方式終止處於「阻塞狀態」的線程。
當線程因爲被調用了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;
        }
    }
}

    5.2.2 終止處於「運行狀態」的線程

    一般,咱們經過「標記」方式終止處於「運行狀態」的線程。其中,包括「中斷標記」和「額外添加標記」。
    (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異常產生時,線程被終止。
    }
}

5.3 終止線程示例

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.總結

線程間同步機制以及中斷機制就先探討到這裏,下面將有一個經典的生產-消費者問題來多線程的具體應用。

相關文章
相關標籤/搜索