Java併發編程之線程調度

個人博客請訪問 http://www.x0100.top             

1. 優先級

每個線程執行時都有一個優先級的屬性,優先級高的線程可以獲得較多的執行機會,而優先級低的線程則獲得較少的執行機會。

操作系統採用時分的形式調度運行的線程,操作系統會分出一個個時間片,線程會分配到若干時間片,當線程的時間片用完了就會發生線程調度,並等待着下次分配。線程分配到的時間片多少也就決定了線程使用處理器資源的多少,而線程優先級就是決定線程需要多或者少分配一些處理器資源的線程屬性。

Thread 類通過一個整型成員變量 priority 來控制優先級,優先級的範圍從 1 ~ 10,默認優先級是 5。

舉例:如下代碼,一般情況下,高級線程更先執行完畢。

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        new MyThread("高級", 10).start();
        new MyThread("低級", 1).start();
    }
}

class MyThread extends Thread {
    public MyThread(String name, int pro) {
        super(name);// 設置線程的名稱
        setPriority(pro);// 設置線程的優先級
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(this.getName() + "線程第" + i + "次執行!");
        }
    }
}

雖然 Java 提供了 10 個優先級別,但這些優先級別需要操作系統的支持,所以需要注意:

  1. 操作系統的優先級可能不能很好的和 Java 的 10 個優先級別對應,所以最好使用 MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三個靜態常量來設定優先級,以保證程序更好的可移植性。

  2. 線程優先級不能作爲程序正確性的依賴,因爲操作系統可以完全不用理會 Java 線程對於優先級的設定。

2. Deamon 守護線程

守護線程是什麼?

Daemon 線程是一種支持型線程,在後臺守護一些系統服務,比如 JVM 的垃圾回收、內存管理等線程都是守護線程。

與之對應的就是用戶線程,用戶線程就是系統的工作線程,它會完成整個系統的業務操作。

用戶線程結束後就意味着整個系統的任務全部結束了,因此係統就沒有對象需要守護的了,守護線程自然而然就會退出。所以當一個 Java 應用只有守護線程的時候,虛擬機就會自然退出

舉例:

public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyCommon();
        Thread t2 = new Thread(new MyDaemon());
        t2.setDaemon(true); // 設置爲守護線程

        t2.start();
        t1.start();
    }
}

class MyCommon extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("線程1第" + i + "次執行!");
            try {
                Thread.sleep(7);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyDaemon implements Runnable {
    public void run() {
        for (long i = 0; i < 9999999L; i++) {
            System.out.println("後臺線程第" + i + "次執行!");
            try {
                Thread.sleep(7);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行結果:

後臺線程第0次執行!
線程1第0次執行!
線程1第1次執行!
後臺線程第1次執行!
後臺線程第2次執行!
線程1第2次執行!
線程1第3次執行!
後臺線程第3次執行!
線程1第4次執行!
後臺線程第4次執行!
後臺線程第5次執行!
後臺線程第6次執行!
後臺線程第7次執行!

通過結果可以看到,用戶線程 MyCommon 執行完畢之後,程序中只有守護線程 MyDaemon,虛擬機退出,守護線程 MyDaemon 也就結束了。

如何設置守護線程?

Thread 類 boolean 類型的 daemon 屬性標誌守護線程,通過 setDaemon(boolean on)方法設置守護線程。

  • 調用 setDaemon(boolean on)設置守護線程要在線程啓動前,否則會拋出異常。

  • 守護線程在退出的時候並不會執行 finnaly 塊中的代碼,所以將釋放資源等操作不要放在 finnaly 塊中執行,這種操作是不安全的。

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
        thread.setDaemon(true);
        thread.start();
        System.out.println("主線程執行完畢");
    }

    static class DaemonRunner implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("DaemonThread finally run.");
            }
        }
    }
}

運行 Daemon 程序,只會輸出"主線程執行完畢"。

main 線程在啓動了線程 DaemonRunner 之後隨着 main 方法執行完畢而終止,而此時 Java 虛擬機中已經沒有非 Daemon 線程,虛擬機需要退出。JDaemon 線程 DaemonRunner 立即終止,DaemonRunner 中的 finally 塊並沒有執行

3. 中斷

中斷代表線程狀態,每個線程都關聯了一箇中斷狀態,用 boolean 值表示,初始值爲 false。中斷一個線程,其實就是設置了這個線程的中斷狀態 boolean 值爲 true。

注意區分字面意思,中斷只是一個狀態,處於中斷狀態的線程不一定要停止運行。

Thread 類線程中斷的方法:

// 設置一個線程的中斷狀態爲true
public void interrupt() {}

// 檢測線程中斷狀態,處於中斷狀態返回true
public boolean isInterrupted() {}

// 靜態方法,檢測調用這個方法的線程是否已經中斷,處於中斷狀態返回true
// 注意:這個方法返回中斷狀態的同時,會將此線程的中斷狀態重置爲false
public static boolean interrupted() {}

自動感知中斷

以下方法會自動感知中斷:

Object 類的 wait()、wait(long)、wait(long, int)
Thread 類的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)

當一個線程處於 sleep、wait、join 這三種狀態之一時,如果此時線程中斷狀態爲 true,那麼就會拋出一個 InterruptedException 的異常,並將中斷狀態重新設置爲 false。

舉例:利用中斷結束線程

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.start();
        Thread.sleep(3000);
        thread.interrupt();
    }
}

class MyThread extends Thread {
    int i = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("中斷異常被捕獲了");
                return;
            }
            i++;
        }
    }
}

執行結果:

0
1
2
中斷異常被捕獲了

MyThread 線程一直循環打印數字,3s 之後主線程將 MyThread 線程中斷,MyThread 線程處於 sleep 狀態會自動感應中斷,拋出 InterruptedException 異常,線程結束執行。

4. join

當一個線程必須等待另一個線程執行時,就用到 join。

Thread 類中的三個 join 方法:

// 當前線程加入該線程後面,等待該線程終止。
void join()

// 當前線程等待該線程終止的時間最長爲 millis 毫秒。如果在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度
void join(long millis)

// 等待該線程終止的時間最長爲 millis 毫秒 + nanos 納秒。如果在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度
void join(long millis,int nanos)

使用舉例:將主線程加入到子線程後面,不過如果子線程在 1 毫秒時間內沒執行完,則主線程便不再等待它執行完,進入就緒狀態,等待 cpu 調度。

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        t.join(1);// 將主線程加入到子線程後面,不過如果子線程在1毫秒時間內沒執行完,則主線程便不再等待它執行完,進入就緒狀態,等待cpu調度
        for (int i = 0; i < 30; i++) {
            System.out.println("main線程第" + i + "次執行!");
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("MyThread線程第" + i + "次執行!");
        }
    }
}

join 實現:三個 join 方法都調用同一個 join(long millis)方法,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) {
        // 只要子線程MyThread isAlve,主線程就一直掛起
        while (isAlive()) {
            wait(0);
        }
    } else {
        // 1.delay時間>0,主線程wait delay時間
        // 2.主線程自動喚醒之後,再次檢查如果子線程MyThread isAlive且delay時間還沒到就就繼續將主線程wait
        // 3.循環1 2 ,直到子線程MyThread執行完或者主線程wait時間超過millis
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

5. sleep

sleep 方法是 Thread 的靜態方法,sleep 讓線程進入到阻塞狀態,交出 CPU,讓 CPU 去執行其他的任務。

sleep 方法不會釋放鎖。

6. yield

yield 方法是 Thread 的靜態方法,yield 方法讓當前正在執行的線程進入到就緒狀態,讓出 CPU 資源給其他的線程。

注意:

yield 方法只是讓當前線程暫停一下,重新進入就緒線程池中,讓系統的線程調度器重新調度器重新調度一次,完全可能出現這樣的情況:當某個線程調用 yield()方法之後,線程調度器又將其調度出來重新進入到運行狀態執行。

7. wait & notify/notifyAll

先來複習一下 synchronized 監視器鎖 monitor 的實現原理。

Monitor 中有幾個關鍵屬性:

_owner:指向持有ObjectMonitor對象的線程
_WaitSet:存放處於wait狀態的線程隊列
_EntryList:存放處於等待鎖block狀態的線程隊列
_recursions:鎖的重入次數
_count:用來記錄該線程獲取鎖的次數
  • 線程 T 中鎖對象調用 wait():_owner 置爲 null,計數器_count 減 1,_WaitSet 中加入 T 等待被喚醒。

  • 鎖對象調用 notify():從_存放處於 wait 狀態的線程隊列 _WaitSet 中隨意選擇一個線程 T,將線程 T 從_WaitSet 中移到_EntryList 中重新去競爭鎖。

Monitor

同步隊列(鎖池/_EntryList):由於線程沒有競爭到鎖,只能等待鎖釋放之後再去競爭,此時線程就處於該對象的同步隊列(鎖池)中,線程狀態爲 BLOCKED。

等待隊列(等待池/_WaitSet):線程調用了 wait 方法後被掛起,等待 notify 喚醒或者掛起時間到自動喚醒,此時線程就處於該對象的等待隊列(等待池)中,線程狀態爲 WAITING 或者 TIMED_WAITING。

wait 方法:釋放持有的對象鎖,線程狀態由 RUNNING 變爲 WAITING,並將當前線程放置到對象的等待隊列;

notify 方法:在目標對象的等待集合中隨意選擇一個線程 T,將線程 T 從等待隊列移到同步隊列重新競爭鎖,線程狀態由 WAITING 變爲 BLOCKED。

  • 當等待集合中存在多個線程時,並沒有機制保證哪個線程會被選擇到。

  • 調用 notify 的線程釋放鎖,線程 T 競爭鎖,如果競爭到鎖,線程 T 從之前 wait 的點開始繼續執行。

notifyAll 方法:notifyAll 方法與 notify 方法的運行機制是一樣的,只是將等待隊列中所有的線程全部移到同步隊列。

  • wait & notify/notifyAll 這三個都是 Object 類的方法。

  • 使用 wait,notify 和 notifyAll 前提是先獲得對象的鎖。

總結

通過設置線程優先級屬性可以改變線程被 CPU 調度的機會,需要注意線程優先級不能作爲程序正確性的依賴。

Daemon 線程是一種支持型線程,在後臺守護一些系統服務,當只有守護線程的時候,程序就會自然退出。

線程中斷代表線程狀態,每個線程都關聯了一個用 boolean 值表示中斷狀態。當一個線程處於 sleep、wait、join 這三種狀態之一時,線程中斷會拋出一個 InterruptedException 的異常。

線程調度還有 Thread 類的 join、sleep、yield 方法,Object 的 wait、notify/notifyAll 方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章