5、Java 線程類常用方法

概述

線程在運行過程中可以通過調用方法來修改狀態和屬性。本篇我們主要介紹 Java 線程常用的方法


Java 線程常用方法

我打算從以下三個模塊出發,依次介紹常用的線程方法:

  1. 實例方法
  2. 靜態方法
  3. Object 繼承方法

1、實例方法

Java 線程類常用的實例方法有以下這些:

  • start()
  • run()
  • interrupt()
  • isInterrupted()
  • isAlive()
  • join()
  • isDaemon()
  • getState()

1-1、start()

public synchronized void start()

通過調用該方法,線程對象被啓動,jvm 虛擬機將調用該對象的 run() 方法。其中調用後,我們會得到兩個同時運行的線程,一個是調用 start() 方法的線程,另一就是我們新啓動的線程。

該方法最終會調用 start0() 方法,start0() 是一個 native 方法。

start() 方法被 synchronized 修飾,也就是說同時只能有一個線程執行當前對象的 start() 方法。通過該關鍵字保證:一個線程只能啓動一次,即使該線程已經執行完成。當我們再次調 start() 方法時,會拋出以下異常:

java.lang.IllegalThreadStateException

1-2、run()

public void run()

該方法是線程的執行體,也就是線程運行的內容。通過繼承 Thread 類實現的線程需要重寫該方法,通過 Runnable 接口實現的線程,會直接調用參數對象的 run()方法。

start() 方法啓動線程後,執行的內容就是該 run() 方法。


1-3、interrupt()

public void interrupt()

調用該方法,嘗試 停止某個線程。一般在實際應用中有以下兩種情況:

  1. 停止阻塞的線程,會拋出異常
  2. 停止正在運行的線程,一般被調線程無響應

首先我們驗證拋出異常能否中斷線程,具體代碼如下:

private class Worker implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            System.out.println(threadName + "正在運行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
@Test
public void test() throws InterruptedException {
    Thread t = new Thread(new Worker());
    t.start();
    t.interrupt();
    t.join();
}

執行結果

Thread-0正在運行
java.lang.InterruptedException: sleep interrupted
Thread-0正在運行
Thread-0正在運行
...

從結果來看,拋出異常後,線程並沒有停止,而是繼續執行。也就是說:interrupt() 方法並不能停止阻塞線程。並且在測試過程中,我發現了一個奇怪的現象:線程阻塞時,如果調用 interrupt() 方法,線程拋出異常後會直接執行,從而跳過阻塞過程。具體我們看代碼:

private class Worker implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            System.out.println(threadName + "正在運行,當前時間:" + System.currentTimeMillis());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
@Test
public void test() throws InterruptedException {
    Thread t = new Thread(new Worker());
    t.start();
    while (true) {
        Thread.sleep(1000);
        t.interrupt();
    }
}

執行結果

Thread-0正在運行,當前時間:1591276900354
java.lang.InterruptedException: sleep interrupted
Thread-0正在運行,當前時間:1591276901353
java.lang.InterruptedException: sleep interrupted
Thread-0正在運行,當前時間:1591276902353
...

在上述代碼中,Worker 線程每次循環睡眠 10秒,主線程每秒執行一次 interrupt() 方法。從運行結果來看,每秒都會打印信息。也就是說 interrupt() 方法會使被調線程跳出阻塞狀態,直接向下執行(鎖情況除外,線程沒有獲取到鎖資源時,永遠無法向下執行)。

爲什麼 interrupt() 方法會跳出阻塞呢?其實是因爲每個線程都有一個 中斷狀態標識,調用 sleep() 方法會將該標識置位 true,表示線程處於阻塞狀態。調用 interrupt() 方法後,該標識被重置爲 false,而sleep() 方法中有循環判斷該標識。當校驗到該標識爲 false 後拋出異常,直接向下執行。

既然 interrupt() 無法停止阻塞的線程,那我們看看它能否停止正在運行的線程。具體我們看代碼:

@Test
public void test() throws InterruptedException {
    Thread t = new Thread() {
        @Override
        public void run() {
            while (true) {
                System.out.println("我還沒停止");
            }
        }
    };
    t.start();
    t.interrupt();
    t.join();
}

執行結果

我還沒停止
我還沒停止
我還沒停止
...

通過運行結果我們可以看出,該線程永遠無法停止。其實原因是這樣的:在JAVA語言中,無論阻塞線程還是運行線程,是否停止只能由自己來決定,interrupt() 只能修改線程的中斷標識,提示它應該停止了

這種場景就類似:有個人在跑步,我過去告訴他:“你該停下了”。具體停不停由他自己決定。

然而線程本身是不帶有思想的,因此我們可以通過編碼,讓線程定時檢查自己是否停止了。檢查線程是否停止的方法是 isInterrupted()。具體代碼如下:

@Test
public void test() throws InterruptedException {
    Thread t = new Thread() {
        @Override
        public void run() {
            while (true) {
                if (this.isInterrupted()) {
                    System.out.println("我該停止了");
                    return;
                }
                System.out.println("我還沒停止");
            }
        }
    };
    t.start();
    Thread.sleep(1);
    t.interrupt();
    t.join();
}

執行結果

我還沒停止
我還沒停止
...
我該停止了

從運行結果可以看出,最終該線程調用 isInterrupted() 方法判斷已停止,通過 return 結束。


1-4、isInterrupted()

public boolean isInterrupted();

通過該方法判斷線程是否停止,需要注意的一點是:它只根據中斷標識判斷,具體有沒有停止並不一定,舉個簡單的例子:

public class IsInterruptedTest {
    class Worker implements Runnable {
        @Override
        public void run() {
            while (true) {
                System.out.println("我沒停止");
            }
        }
    }
    @Test
    public void test() throws InterruptedException {
        Thread t = new Thread(new Worker());
        t.start();
        t.interrupt();
        System.out.println(t.isInterrupted());
    }
}

執行結果

true
我沒停止
我沒停止
...

從上面的結果我們可以看出,即使線程已經知道自己處於停止狀態,也不會主動停止,必須通過 return 或執行完線程體代碼來結束

實際應用中,在線程體中通過該方法判斷來決定是否停止線程,也就是上面提到的自己中斷自己,一般寫法如下:

@Override
public void run() {
    while (true) {
        System.out.println("我沒停止");
        if(Thread.currentThread().isInterrupted()){
        	System.out.println("我該停止了");
        	return;
        }
    }
}

最後需要注意的一點是:isInterrupted() 方法不會清除中斷標識,也就是說如果一個線程的狀態沒有發生變化,連續調用該方法獲取的結果永遠是一致的,關於這點我們在靜態方法 interrupted() 模塊中詳細說明。


1-5、isAlive()

public final native boolean isAlive();

通過該方法判斷一個線程是否處於活躍狀態,如果活躍返回 true,否則返回false。

什麼樣的線程處於活躍狀態呢?關於這點我是這樣理解的:啓動並且沒有停止的線程都屬於活躍狀態。

也就是說 sleep() 睡眠,搶鎖失敗阻塞的線程,都屬於活躍狀態。下面我們通過簡單Demo驗證:

public class IsAliveTest {

    class Worker implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "嘗試搶鎖");
            synchronized (Worker.class) {
                System.out.println(threadName + "獲取到鎖,開始sleep");
                try {
                    Thread.sleep(10000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread t1 = new Thread(new Worker());
        Thread t2 = new Thread(new Worker());
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println(t1.getName() + "的狀態:" + t1.isAlive());
        System.out.println(t2.getName() + "的狀態:" + t2.isAlive());
    }

}

執行結果

Thread-0嘗試搶鎖
Thread-0獲取到鎖,開始sleep
Thread-1嘗試搶鎖
Thread-0的狀態:true
Thread-1的狀態:true

在上述代碼中,線程0獲取到鎖後執行sleep()方法阻塞。線程1搶鎖失敗阻塞。從結果來看兩個線程都屬於活躍狀態。通俗點來說就是:如果一個線程還能執行(未啓動線程除外),那麼它就是活躍的


1-6、join()

public final void join() throws InterruptedException
public final synchronized void join(long millis)

通過該方法,讓當前線程阻塞,直到被調用線程執行完畢或等待足夠時間後才向下執行。因爲存在阻塞的原因,該方法也會拋出 InterruptedException 異常。

上述方法1會調用方法2,默認參數爲0。當參數爲0時表示,直到被調用線程執行完畢後才向下執行。參數不爲0時表示,最多等待XXX毫秒,等待足夠的時間也會繼續向下執行。

舉個簡答的例子:A和B去郊遊,出發的時候B發現自己忘記帶錢包。假設A和B關係特別好,此時A對B說,你先回去取吧,我會一直等你回來。此時就對應 join() 方法參數爲0的情況,即一直等待你執行完畢。假設A和B關係一般,那麼A對B說,你先回去取吧,我最多等你10分鐘。此時就對應 join() 方法帶參數且不爲0的場景,等待足夠的時間後繼續向下執行。 下面我們具體看代碼:

public class JoinTest {

    private class Worker implements Runnable {
        @Override
        public void run() {
            System.out.println("我執行了,接下來我要休息10S。當前時間:" + System.currentTimeMillis());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我執行完了");
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread workerThread = new Thread(new Worker());
        System.out.println("啓動worker線程時間:" + System.currentTimeMillis());
        workerThread.start();
        workerThread.join();
        System.out.println("執行完worker線程時間:" + System.currentTimeMillis());
    }

}

執行結果

啓動worker線程時間:1591326308027
我執行了,接下來我要休息10S。當前時間:1591326308027
我執行完了
執行完worker線程時間:1591326318027

從結果來看,本地線程調用 worker 對象的 join() 方法後阻塞,直到 worker() 線程執行完畢才向下執行。

既然出現阻塞等待,就可能產生死鎖。即 A 等待 B,B 等待 C,C 等待 A。當出現上述情況時,所有線程都無法向下執行,產生死鎖。下面覺個最簡單的例子:我等待我自己

public void run() {
    Thread t = Thread.currentThread();
    try {
        t.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("我執行完了");
}

執行結果:無

上述代碼中,線程必須阻塞並等待自己執行完,然而阻塞永遠無法執行,因此產生死鎖。

join() 方法源碼中通過調用 wait() 方法實現,在下面的 wait() 方法中我們重點討論。


1-7、isDaemon()

public final boolean isDaemon()

Java 語言中有兩種類型的線程:普通線程和守護線程。通過該方法判斷一個線程是否守護線程

關於守護線程我是這樣理解的:守護線程就是爲其他普通線程提供服務的線程。只要還存在普通線程,所有守護線程都必須工作,只有當所有普通線程執行完畢後,JVM和守護線程才一起停止,因爲此時已經沒有需要服務的線程了。垃圾回收器 就是最典型的守護線程。

在本模塊的測試案例開始之前,我首先強調非常重要的一點:isDaemon() 只能通過 main() 方法測試,不能通過 JUnit 方法測試

因爲 JUnit 不支持多線程,不會等待其它線程執行完畢。JUnit 線程執行完畢後,所有線程自然也會停止,我們無法判斷其它線程的停止原因。其它模塊可以測試是因爲我們通過 join() 或 CountDownLatch 保證主線程不結束,但是在這裏控制 JUnit 線程不結束的話,就無法滿足所有普通線程都停止的條件,更加無法驗證。

下面我們看具體代碼:

public class IsDaemonTest {

    private class Worker implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        IsDaemonTest isDaemonTest = new IsDaemonTest();
        Worker worker = isDaemonTest.new Worker();
        Thread t = new Thread(worker);
        t.start();
    }
}

執行結果

0
1
2
3
...

main() 方法本身也是一個普通線程。當 mian() 方法對應的線程執行完畢後,新創建的 worker 線程還在執行,終端每秒都會打印出最新 i 變量的值。下面我們嘗試將 worker 線程修改爲守護線程。需要注意的一點是,設置守護線程必須在線程啓動前操作,正在運行的線程狀態無法改變。修改後的新代碼爲:

public static void main(String[] args) {
    IsDaemonTest isDaemonTest = new IsDaemonTest();
    Worker worker = isDaemonTest.new Worker();
    Thread t = new Thread(worker);
    t.setDaemon(true);
    t.start();
}

執行結果

0
Process finished with exit code 0
或
Process finished with exit code 0

通過結果我們可以發現,當所有普通線程執行完畢後,守護線程也會隨着自動停止。


1-8、getState()

public State getState()

通過該方法可以獲取 Java 線程的狀態。其中 State 是一個枚舉類。下面我們依次介紹每種狀態:

  • NEW:剛剛創建,還沒有啓動的線程。即 new 出來的 Thread 對象。

  • RUNNABLE:正在運行的線程,即使CPU沒有調度,也視爲 RUNNABLE 狀態。

  • BLOCKED:線程競爭鎖失敗,阻塞等待鎖時的狀態

  • WAITING:沒有最大等待時長的阻塞狀態,等待其他線程喚醒。調用無參的 join() 、wait() 都會進入該狀態

  • TIMED_WAITING:有最大等待時長的阻塞狀態,如調用 thread.sleep(1000)、thread.join(1000)

  • TERMINATED:已經死亡(執行完畢)的線程

關於線程狀態之間轉移關係,後面我們通過其他博客專門整理。


2、靜態方法

Java 線程類常用的靜態方法有以下這些:

  • currentThread()
  • yield()
  • sleep()
  • interrupted()

2-1、currentThread()

public static native Thread currentThread();

該方法是 native 方法。通過它可以獲取當前執行該段代碼的線程。一般在代碼中這樣使用:

Thread t = Thread.currentThread();

該方法一般在實現 Runnable 接口類的 run() 方法中使用。因爲實現 Runnable 接口的類本身不是線程類,線程體/run()方法 中無法直接獲取當前線程,因此可以通過上述靜態方法獲取線程。而繼承 Thread 的類本身就是線程類,可以在方法體中直接通過 this 關鍵字獲取當前線程對象。


2-2、yield()

public static native void yield();

該方法是 native() 方法。通過調度該方法向調度程序發送信號,表示自己願意放棄當前對處理器資源的使用。當然處理器也可以忽略該方法繼續執行。下面我們通過一個簡單的 Demo 瞭解yield()方法:

public class YieldTest{

    CountDownLatch countDownLatch = new CountDownLatch(2);

    private class WorkerA implements Runnable {
        @Override
        public void run() {
            String tName = Thread.currentThread().getName();
            for (int i = 1; i < 11; i++) {
                System.out.println(tName + ":執行第" + i + "次循環");
                if (i == 2) {
                    Thread.yield();
                }
            }
            countDownLatch.countDown();
        }
    }

    private class WorkerB implements Runnable {
        @Override
        public void run() {
            String tName = Thread.currentThread().getName();
            for (int i = 1; i < 11; i++) {
                System.out.println(tName + ":執行第" + i + "次循環");
            }
            countDownLatch.countDown();
        }
    }

    @Test
    public void test() {
        new Thread(new WorkerA()).start();
        new Thread(new WorkerB()).start();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

執行結果:該方法運行結果有很多種,我列出其中兩個足以說明問題即可。

Thread-0:執行第1次循環
Thread-0:執行第2次循環
Thread-1:執行第1次循環
Thread-1:執行第2次循環
...

Thread-0:執行第1次循環
Thread-0:執行第2次循環
Thread-0:執行第3次循環
Thread-0:執行第4次循環
...

在上述代碼中,我們創建兩個線程分別打印循環信息。線程A在第二輪循環時執行 yield() 方法,放棄CPU資源。此時需要注意的一點是:放棄之後,不代表線程A不競爭,而是說重新開始競爭。也就是說,線程A也有可能重新獲得CPU資源。關於這點從運行結果我們也可以看出。

舉個例子:張三贏得冠軍之後覺得沒意思,沒有達到預期的效果,因此申請重新比賽。重新比賽意味着張三有可能再次奪冠,當然其他人也有可能奪冠,而不是說張三讓出冠軍給其他人競爭。

最後我們來談談 yield() 方法的作用:在實際應用中,假如系統現在提供兩種服務,服務A特別重要,服務B相比一般。當服務A請求較多時,我們就可以在服務B的代碼中循環執行 yield() 方法釋放CPU資源,讓比較重要的服務A先執行。但是一般情況我不建議使用,因爲它會影響效率執行相同數量的任務,增加了CPU上下文切換的時間


2-3、sleep()

public static void sleep(long millis, int nanos) throws InterruptedException

public static native void sleep(long millis) throws InterruptedException;

上述方法1最終也會調用方法2,該方法也是native()方法。方法1中的參數分別表示毫秒和納秒,也就是說方法1只是多了一個單位,讓線程多睡眠一會。

通過 sleep() 方法使當前線程放棄CPU調度,睡眠(暫時停止執行)指定的毫秒數(線程睡眠不會精確到納秒,上述方法1最終會四捨五入到毫秒)。其中sleep方法不會失去狀態監視,當休眠夠指定的時間後恢復運行狀態等待CPU調度。下面我們通過簡單 Demo 實踐 sleep() 方法:

@Test
public void test() {
    System.out.println(System.currentTimeMillis());
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(System.currentTimeMillis());
}

執行結果

1591250232213
1591250235214

我們通過 System.currentTimeMillis() 獲取系統時間,當線程在執行到 sleep() 方法後暫停,3000毫秒後繼續向下執行。

在使用sleep()方法時,我們需要手動通過 try-catch 方法捕獲 InterruptedException 異常。這是由於在 native 源碼中,sleep() 方法中有判斷線程狀態標識,如果線程狀態標識顯示已中斷:就拋出異常。下面我們通過簡單 Demo 測試該場景:

@Test
    @Test
    public void test2() {
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("我被終止啦");
                }
            }
        };
        t.start();
        t.interrupt();
    }

執行結果

java.lang.InterruptedException: sleep interrupted
我被終止啦

上述案例中,在線程休眠期間我們調用它的 interrupt() 方法中斷線程,系統拋出異常。這裏需要注意的一點是,finally 代碼塊中代碼仍然會被執行,在線程池源碼我們經常會看到這種操作。

最後,我們來聊一下該方法重要特性:sleep() 方法不會放棄鎖資源。 也就是說如果一個線程佔有鎖資源,在該線程調用 sleep() 方法阻塞後,它不會放棄鎖資源,直到執行完併發代碼塊。下面我來看一個簡單示例:

private class Worker implements Runnable {
    @Override
    public void run() {
        synchronized (Worker.class) {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "獲取到鎖啦,獲取到鎖時間:" + System.currentTimeMillis());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadName + "睡眠結束");
        }
    }
}

@Test
public void test3() {
    new Thread(new Worker()).start();
    new Thread(new Worker()).start();
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

執行結果

Thread-0獲取到鎖啦,獲取到鎖時間:1591252519454
Thread-0睡眠結束
Thread-1獲取到鎖啦,獲取到鎖時間:1591252521455
Thread-1睡眠結束

通過結果我可以看出,只有在 Thread-0 線程睡眠結束後,Thread-1 才能獲取到鎖,也就是說 sleep() 方法不會釋放鎖資源。


2-4、interrupted()

public static boolean interrupted()

該方法和 isInterrupted() 方法功能相同,都是判斷線程是否中斷。主要區別有以下幾點:

  • isInterrupted() 是實例方法,判斷調用方法的對象線程是否停止
  • interrupted() 是靜態方法,判斷當前線程是否停止
  • isInterrupted() 不會處理線程中斷標識,interrupted()方法會清理線程狀態標識

關於區別一和區別二,根據方法修飾我們就能看出。我們主要驗證方案三,具體我們看代碼:

    @Test
public void test() {
    Thread thread1 = new Thread() {
        @Override
        public void run() {
            while (true) {
            }
        }
    };
    thread1.start();
    thread1.interrupt();
    System.out.println("第一次調用isInterrupted()方法,判斷線程狀態:" + thread1.isInterrupted());
    System.out.println("第二次調用isInterrupted()方法,判斷線程狀態:" + thread1.isInterrupted());
    Thread.currentThread().interrupt();
    System.out.println("第一次調用interrupted()方法,判斷線程狀態:" + Thread.interrupted());
    System.out.println("第二次調用interrupted()方法,判斷線程狀態:" + Thread.interrupted());
}

執行結果

第一次調用isInterrupted()方法,判斷線程狀態:true
第二次調用isInterrupted()方法,判斷線程狀態:true
第一次調用interrupted()方法,判斷線程狀態:true
第二次調用interrupted()方法,判斷線程狀態:false

通過執行結果我們可以看出,isInterrupted() 方法前後線程的狀態沒有變化。interrupted() 方法前後坐席的狀態由 true 變爲 false。

關於該方法的作用我是這樣理解的:通過該方法判斷線程標識爲停止時,具體是否停止還是由代碼邏輯決定。如果此時需要停止線程,就在判斷爲 true 時通過 return 或其他方法停止線程。如果線程還有用,不能停止,就不做任何處理,線程停止標識重置爲 false,防止後續調用阻塞方法時拋出異常。


3、Object 繼承方法

線程常用 Object 方法有以下幾種:

  • wait()
  • notify()
  • notifyAll()

3-1、wait()

public final void wait() throws InterruptedException

wait() 方法和 sleep() 方法都會讓當前線程阻塞。主要區別有以下幾點:

  • wait() 方法是 Object 方法,sleep() 方法是 Thread 方法。

  • wait() 方法必須放棄鎖資源,也就是說調用 wait() 方法必須先獲取鎖纔行,否則拋出異常。sleep()方法不會釋放鎖資源,也不強制要求必須含有鎖資源。

  • wait() 方法無參時表示無限期等待,等待其他線程調用 notify() 方法,sleep() 必須有參數。

  • wait() 方法有參時,除了等待具體的時長外,其他線程調用 notify() 方法也可以喚醒,而 sleep()只能等待參數時長。

  • wait() 線程喚醒後進入鎖池,爭奪鎖資源。sleep() 線程喚醒後只要CPU調度就可以運行。

wait() 會拋出 InterruptedException 異常,也就是調用 wait() 方法阻塞時調用 interrupt() 方法也會跳過阻塞過程,不過它不能直接執行,因爲還要獲取鎖資源。

在示例開始之前我先簡單的介紹兩個概念:鎖池等待池

  • 鎖池:所有競爭某個 synchronized 鎖的線程都會進入這個對象的鎖池。也就是說鎖池中的線程都是阻塞競爭鎖的線程

  • 等待池:所有競爭某個 synchronized 鎖但 暫不搶佔鎖 的線程都會進入這個對象的等待池。等待池中的線程必須等待足夠的時候或者被 notify() 喚醒後纔會進入鎖池,開始競爭鎖。

在線程中調用某個對象的 wait() 方法會讓當前線程放棄鎖,進入該對象的鎖池。下面我們看具體案例:

public class WaitTest {

    Object lock = new Object();

    private class WorkerA implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                synchronized (lock) {
                    System.out.println(threadName + "獲取到鎖的時間:" + System.currentTimeMillis());
                    lock.wait();
                }
                System.out.println(threadName + "執行完的時間" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private class WorkerB implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                Thread.sleep(1000);
                synchronized (lock) {
                    System.out.println(threadName + "獲取到鎖的時間:" + System.currentTimeMillis());
                    lock.notify();
                }
                System.out.println(threadName + "執行完的時間" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    public void test() throws InterruptedException {
        new Thread(new WorkerA()).start();
        new Thread(new WorkerB()).start();
        Thread.sleep(3000);
    }
}

執行結果

Thread-0獲取到鎖的時間:1591413700745
Thread-1獲取到鎖的時間:1591413701745
Thread-1執行完的時間1591413701745
Thread-0執行完的時間1591413701745

上述案例中,WorkerB 線程首先睡眠一段時間,保證 WorkerA 線程搶佔到鎖。WorkerA 線程搶佔到鎖後調用 wait() 方法阻塞,放棄鎖資源。此時 WorkerB 線程取到鎖資源,執行 notify() 方法喚醒 WorkerA 線程,兩個線程最終都執行完畢。

這裏需要注意的一點是:wait() 方法會拋棄所有鎖資源,而不僅僅是被調用的對象。具體我們看代碼:

synchronized (lock)
修改爲-synchronized (WaitTest.class)

執行結果

Thread-0獲取到鎖的時間:1591414650563
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
Thread-1獲取到鎖的時間:1591414651564
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException

從結果我們可以看出:wait() 方法調用後,WorkerA 線程拋出異常,並釋放鎖資源。WorkerB 線程獲取鎖資源,調用 notify() 方法後拋出異常結束。

IllegalMonitorStateException無論 wait()、notify() 還是 notifyAll() 方法都需要獲取被調用對象的鎖資源,如果沒有獲取到,就會拋出該異常,調用 wait() 方法的線程不會進入等待池,調用 notify() 方法的線程也不會喚醒對象等待池中的線程。

  • 前文我們提到 join() 方法的原理是調用 wait() 方法,但是在使用 join() 方法時我們不需要加鎖,這是爲什麼呢?

    其實是因爲 join() 方法本身就是 synchronized 修飾的,也就是說:我們調用 threadA.join() 方法後,會進入 threadA 對象的等待池。而 threadA對象本身也是線程,它執行完畢後會喚醒等待池中的線程,當前線程就可以繼續向下執行。這也就是join() 方法的原理。

最後我再來談談我對 wait() 方法的理解(可能有誤):調用 wait() 方法阻塞的線程被喚醒後,如果搶佔到鎖資源。它會根據程序計數器的指示執行對應行的代碼,也就是說 synchronized() 方法不是在方法體的第一步獲取鎖的,而是每一步執行時都需要判斷鎖資源


3-2、notify()

public final native void notify();

notify() 也是 native 方法。關於 notify() 方法的作用已經在 wait() 方法模塊介紹。這裏需要補充的一點是:notify() 會喚醒等待池中隨機一個線程。下面我們具體看案例:

public class NotifyTest {

    Object lock = new Object();

    private class Worker implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                synchronized (lock) {
                    System.out.println(threadName + "獲取到鎖");
                    lock.wait();
                }
                System.out.println(threadName + "被喚醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
        @Test
    public void test() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            new Thread(new Worker()).start();
        }
        Thread.sleep(2000);
        for (int i = 0; i < 5; i++) {
            synchronized (lock) {
                lock.notify();
            }
        }
        Thread.sleep(1000);
    }
}

執行結果

Thread-0獲取到鎖
Thread-2獲取到鎖
Thread-3獲取到鎖
Thread-1獲取到鎖
Thread-4獲取到鎖
Thread-0被喚醒
Thread-1被喚醒
Thread-2被喚醒
Thread-4被喚醒
Thread-3被喚醒

上述代碼中,我們通過循環依次從 lock 對象的等待池中喚醒線程。從結果可以看出,notify() 方法喚醒線程是隨機的,並且每次只喚醒一個線程。

在實際應用中,一般不使用 notify() 方法,因爲它無法保證可以喚醒我們需要的那個線程。下面我們來看 notifyAll() 方法


3-3、notifyAll()

public final native void notifyAll();

notifyAll() 也是 native 方法。顧名思義,notifyAll() 方法可以喚醒所有線程。下面我們修改 notify() 測試用例的 JUnit 方法:

@Test
public void test() throws InterruptedException {
    for (int i = 0; i < 5; i++) {
        new Thread(new Worker()).start();
    }
    Thread.sleep(2000);
    synchronized (lock) {
        lock.notifyAll();
    }
    Thread.sleep(1000);
}

執行結果

Thread-0獲取到鎖
Thread-4獲取到鎖
Thread-2獲取到鎖
Thread-3獲取到鎖
Thread-1獲取到鎖
Thread-3被喚醒
Thread-2被喚醒
Thread-1被喚醒
Thread-4被喚醒
Thread-0被喚醒

從結果來看:調用 notifyAll() 方法可以喚醒所有線程。 一般實際應用中更偏向使用 notifyAll() 方法,保證需要使用的線程被喚醒。


參考:
https://www.cnblogs.com/java-spring/p/8309931.html
https://www.cnblogs.com/hongten/p/hongten_java_sleep_wait.html
https://www.cnblogs.com/jenkov/p/juc_interrupt.html
https://www.cnblogs.com/2015110615L/p/6736323.html
https://blog.csdn.net/qq_32679835/article/details/90174955
https://blog.csdn.net/weixin_42862834/article/details/106427027
https://blog.csdn.net/djzhao/article/details/79410229
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章