線程sleep,wait,notify,join,yield方法解析

線程的五種狀態(tài)

線程從創(chuàng)建到銷毀一般分為五種狀態(tài),如下圖:

image

1) 新建

當(dāng)用new關(guān)鍵字創(chuàng)建一個線程時,就是新建狀態(tài)。

2) 就緒

調(diào)用了 start 方法之后,線程就進入了就緒階段。此時,線程不會立即執(zhí)行run方法,需要等待獲取CPU資源。

3) 運行

當(dāng)線程獲得CPU時間片后,就會進入運行狀態(tài),開始執(zhí)行run方法。

4) 阻塞

當(dāng)遇到以下幾種情況,線程會從運行狀態(tài)進入到阻塞狀態(tài)。

  • 調(diào)用sleep方法,使線程睡眠。
  • 調(diào)用wait方法,使線程進入等待。
  • 當(dāng)線程去獲取同步鎖的時候,鎖正在被其他線程持有。
  • 調(diào)用阻塞式IO方法時會導(dǎo)致線程阻塞。
  • 調(diào)用suspend方法,掛起線程,也會造成阻塞。

需要注意的是,阻塞狀態(tài)只能進入就緒狀態(tài),不能直接進入運行狀態(tài)。因為,從就緒狀態(tài)到運行狀態(tài)的切換是不受線程自己控制的,而是由線程調(diào)度器所決定。只有當(dāng)線程獲得了CPU時間片之后,才會進入運行狀態(tài)。

5) 死亡

當(dāng)run方法正常執(zhí)行結(jié)束時,或者由于某種原因拋出異常都會使線程進入死亡狀態(tài)。另外,直接調(diào)用stop方法也會停止線程。但是,此方法已經(jīng)被棄用,不推薦使用。

線程常用方法

1)sleep

當(dāng)調(diào)用 Thread.sleep(long millis) 睡眠方法時,就會使當(dāng)前線程進入阻塞狀態(tài)。millis參數(shù)指定了線程睡眠的時間,單位是毫秒。 當(dāng)時間結(jié)束之后,線程會重新進入就緒狀態(tài)。

注意,如果當(dāng)前線程獲得了一把同步鎖,則 sleep方法阻塞期間,是不會釋放鎖的。

2) wait、notify和notifyAll

首先,它們都是Object類中的方法。需要配合 Synchronized關(guān)鍵字來使用。

調(diào)用線程的wait方法會使當(dāng)前線程等待,直到其它線程調(diào)用此對象的notify/notifyAll方法。 如果,當(dāng)前對象鎖有N個線程在等待,則notify方法會隨機喚醒其中一個線程,而notifyAll會喚醒對象鎖中所有的線程。需要注意,喚醒時,不會立馬釋放鎖,只有當(dāng)前線程執(zhí)行完之后,才會把鎖釋放。

另外,wait方法和sleep方法不同之處,在于sleep方法不會釋放鎖,而wait方法會釋放鎖。wait、notify的使用如下:

public class WaitTest {
    private static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        ListAdd listAdd = new ListAdd();

        Thread t1 = new Thread(() -> {
            synchronized (obj){
                try {
                    for (int i = 0; i < 10; i++) {
                        listAdd.add();
                        System.out.println("當(dāng)前線程:"+Thread.currentThread().getName()+"添加了一個元素");
                        Thread.sleep(300);
                        if(listAdd.getSize() == 5){
                            System.out.println("發(fā)出通知");
                            obj.notify();
                        }
                    }
                } catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (obj){
                try {
                    if(listAdd.getSize() != 5){
                        obj.wait();
                    }
                } catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("線程:"+Thread.currentThread().getName()+"被通知.");
            }

        });

        t2.start();
        Thread.sleep(1000);
        t1.start();
    }
}

class ListAdd {
    private static volatile List<String> list = new ArrayList<String>();

    public void add() {
        list.add("abc");
    }

    public int getSize() {
        return list.size();
    }
}

以上,就是創(chuàng)建一個t2線程,判斷l(xiāng)ist長度是否為5,不是的話,就一直阻塞。然后,另外一個t1線程不停的向list中添加元素,當(dāng)元素長度為5的時候,就去喚醒阻塞中的t2線程。

然而,我們會發(fā)現(xiàn),此時的t1線程會繼續(xù)往下執(zhí)行。直到方法執(zhí)行完畢,才會把鎖釋放。t1線程去喚醒t2的時候,只是讓t2具有參與鎖競爭的資格。只有t2真正獲得了鎖之后才會繼續(xù)往下執(zhí)行。

3) join

當(dāng)線程調(diào)用另外一個線程的join方法時,當(dāng)前線程就會進入阻塞狀態(tài)。直到另外一個線程執(zhí)行完畢,當(dāng)前線程才會由阻塞狀態(tài)轉(zhuǎn)為就緒狀態(tài)。

或許,你在面試中,會被問到,怎么才能保證t1,t2,t3線程按順序執(zhí)行呢。(因為,我們知道,正常情況下,調(diào)用start方法之后,是不能控制線程的執(zhí)行順序的)

咳咳,當(dāng)前青澀的我,面試時就被問到這個問題,是一臉懵逼。其實,是非常簡單的,用join方法就可以輕松實現(xiàn):

public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MultiT("a"));
        Thread t2 = new Thread(new MultiT("b"));
        Thread t3 = new Thread(new MultiT("c"));
        
        t1.start();
        t1.join();

        t2.start();
        t2.join();

        t3.start();
        t3.join();
    }

}

class MultiT implements Runnable{
    private String s;
    private int i;

    public MultiT(String s){
        this.s = s;
    }

    @Override
    public void run() {
        while(i<10){
            System.out.println(s+"===="+i++);
        }
    }
}

最終,我們會看到,線程會按照t1,t2,t3順序執(zhí)行。因為,主線程main總會等調(diào)用join方法的那個線程執(zhí)行完之后,才會往下執(zhí)行。

4) yield

Thread.yield 方法會使當(dāng)前線程放棄CPU時間片,把執(zhí)行機會讓給相同或更高優(yōu)先級的線程(yield英文意思就是屈服,放棄的意思嘛,可以理解為當(dāng)前線程暫時屈服于別人了)。

注意,此時當(dāng)前線程不會阻塞,只是進入了就緒狀態(tài),隨時可以再次獲得CPU時間片,從而進入運行狀態(tài)。也就是說,其實yield方法,并不能保證,其它相同或更高優(yōu)先級的線程一定會獲得執(zhí)行權(quán),也有可能,再次被當(dāng)前線程拿到執(zhí)行權(quán)。

yield方法和sleep方法一樣,也是不釋放鎖資源的。可以通過代碼來驗證這一點:

public class TestYield {
    public static void main(String[] args) {
        YieldThread yieldThread = new YieldThread();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(yieldThread);
            t.start();
        }
    }
}

class YieldThread implements Runnable {

    private int count = 0;

    @Override
    public synchronized void run() {
        for (int i = 0; i < 10; i++) {
            count ++;
            if(count == 1){
                Thread.yield();
                System.out.println("線程:"+Thread.currentThread().getName() + "讓步");
            }
            System.out.println("線程:"+Thread.currentThread().getName() + ",count:"+count);
        }
    }
}

結(jié)果:

image

會看到,線程讓步之后,并不會釋放鎖。因此,其它線程也沒機會獲得鎖,只能把當(dāng)前線程執(zhí)行完之后,才會釋放。(對于這一點,其實我是有疑問的。既然yield不釋放鎖,那為什么還要放棄執(zhí)行權(quán)呢。就算放棄了執(zhí)行權(quán),別的線程也無法獲得鎖啊。)

所以,我的理解,yield一般用于不存在鎖競爭的多線程環(huán)境中。如果當(dāng)前線程執(zhí)行的任務(wù)時間可能比較長,就可以選擇用yield方法,暫時讓出CPU執(zhí)行權(quán)。讓其它線程也有機會執(zhí)行任務(wù),而不至于讓CPU資源一直消耗在當(dāng)前線程。

5)suspend、resume

suspend 會使線程掛起,并且不會自動恢復(fù),只有調(diào)用 resume 方法才能使線程進入就緒狀態(tài)。注意,這兩個方法由于有可能導(dǎo)致死鎖,已經(jīng)被廢棄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容