Java 基礎 —— 多線程(讀書筆記)「一」

多線程對于 Android 開發者來說是基礎。而且這類知識在計算機里也是很重要的一環,所以很有必要整理一番。

目錄

多線程的實現

來上代碼:

// 最常見的兩種方法啟動新的線程
public static void startThread() {
    // 覆蓋 run 方法
    new Thread() {
        @Override
        public void run() {
            // 耗時操作
        }
    }.start();

    // 傳入 Runnable 對象
    new Thread(new Runnable() {
        public void run() {
            // 耗時操作
        }
    }).start();
}

其實第一個就是在 Thread 里覆寫了 run() 函數,第二個是給 Thread 傳了一個 Runnable 對象,在 Runnable 對象 run() 方法里進行耗時操作。
以前沒有怎么考慮過他們兩者的關系,今天我們來具體看看到底是什么鬼?

Thread 源碼

進入 Thread 源碼我們看看:

public class Thread implements Runnable {
    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;


    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
    }
}

源碼很長,我進行了一點分割。一點一點的來解析看看。
我們首先知道 Thread 也是一個 Runnable ,它實現了 Runnable 接口,并且在 Thread 類中有一個 Runnable 類型的 target 對象。

構造方法里我們都會調用 init() 方法,接下來看看在該方法里做了如何的初始化配置。

    private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {

    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    // group 參數如果為 null ,則獲得當前線程的 group(線程組)
    if (g == null) {
            g = parent.getThreadGroup();
    }
    // 代碼省略

    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    // 設置 target( Runnable 類型 )
    this.target = target;

    }


    public synchronized void start() {

    // 將當前線程加入線程組
    group.add(this);

    boolean started = false;
    try {
        // 啟動 native 方法啟動新的線程
        start0();
        started = true;
    } finally {
        // 代碼省略
    }

   private native void start0();



   @Override
   public void run() {
       if (target != null) {
        target.run();
       }
    }

從上我們可以明白,最終被線程執行的任務是 Runnable ,Thread 只是對 Runnable 的一個包裝,并且通過一些狀態對 Thread 進行管理和調度。
當啟動一個線程時,如果 Thread 的 target 不為空,則會在子線程中執行這個 target 的 run() 函數,否則虛擬機就會執行該線程自身的 run() 函數。

線程的幾個重要的函數
  • wait()
    當一個線程執行到 wait() 方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖,使得其他線程可以訪問。用戶可以使用 notify 、notifyAll 或者指定睡眠時間來喚醒當前等待池中的線程。
    注意:wait() notify() notifyAll() 必須放在 synchronized block 中,否則會拋出異常。
  • sleep()
    該函數是 Thread 的靜態函數,作用是使調用線程進入睡眠狀態。因為 sleep() 是 Thread 類的靜態方法,因此他不能改變對象的機鎖。所以,當在一個 synchronized 塊中調用 sleep() 方法時,線程雖然休眠了,但是對象的機鎖并沒有被釋放,其他線程無法訪問這個對象。
  • join()
    等待目標線程執行完成之后繼續執行。
  • yield()
    線程禮讓。目前線程由運行狀態轉換為就緒狀態,也就是讓出執行權限,讓其他線程得以優先執行,但其他線程能否優先執行未知。

在源碼中,查看 Thread 里的 State ,對幾種狀態解釋的很清楚。

NEW 狀態是指線程剛創建,尚未啟動

RUNNABLE 狀態是線程正在正常運行中,當然可能會有某種耗時計算 / IO 等待的操作 / CPU 時間片切換等, 這個狀態下發生的等待一般是其他系統資源, 而不是鎖, Sleep 等

BLOCKED 這個狀態下,是在多個線程有同步操作的場景, 比如正在等待另一個線程的 synchronized 塊的執行釋放,或者可重入的 synchronized 塊里別人調用 wait() 方法,也就是這時線程在等待進入臨界區

WAITING 這個狀態下是指線程擁有了某個鎖之后,調用了他的 wait 方法,等待其他線程 / 鎖擁有者調用 notify / notifyAll 一遍該線程可以繼續下一步操作,這里要區分 BLOCKED 和 WATING ,一個是在臨界點外面等待進入, 一個是在臨界點里面 wait 等待別人 notify , 線程調用了 join 方法 進入另外的線程的時候, 也會進入 WAITING 狀態,等待被他 join 的線程執行結束

TIMED_WAITING 這個狀態就是有限的 (時間限制) 的 WAITING, 一般出現在調用 wait(long), join(long) 等情況下,另外,一個線程 sleep 后, 也會進入 TIMED_WAITING 狀態

TERMINATED 這個狀態下表示 該線程的 run 方法已經執行完畢了, 基本上就等于死亡了 (當時如果線程被持久持有, 可能不會被回收)

Wait() 的實踐

我們來看一段,wait() 的用途和效果。

    static void waitAndNotifyAll() {

        System.out.println("主線程運行");

        Thread thread = new WaitThread();
        thread.start();
        long startTime = System.currentTimeMillis();
        try {
            synchronized (sLockOject) {
                System.out.println("主線程等待");
                sLockOject.wait();
            }
        } catch (Exception e) {
        }

        long timeMs = System.currentTimeMillis() - startTime;
        System.out.println("主線程繼續 —-> 等待耗時:" + timeMs + " ms");

    }

    static class WaitThread extends Thread {

        @Override
        public void run() {
            try {
                synchronized (sLockOject) {
                    System.out.println("進入子線程");
                    Thread.sleep(3000);
                    System.out.println("喚醒主線程");
                    sLockOject.notifyAll();
                }
            } catch (Exception e) {
            }
        }

    }

waitAndNotifyAll() 函數里,會啟動一個 WaitThread 線程,在該線程中將會調用 sleep 函數睡眠 3 秒。線程啟動之后在主線程調用 sLockOject 的 wait() 函數,使主線程進入等待狀態,此時將不會繼續執行。等 WaitThread 在 run() 函數沉睡了 3 秒后會調用 sLockOject 的 notifyAll() 函數,此時就會重新喚醒正在等待中的主線程,因此會繼續往下執行。

結果如下:

主線程運行
主線程等待
進入子線程
喚醒主線程
主線程繼續 —-> 等待耗時:3005 ms

wait()、notify() 機制通常用于等待機制的實現,當條件未滿足時調用 wait 進入等待狀態,一旦條件滿足,調用 notifynotifyAll 喚醒等待的線程繼續執行。

對于這里細節可能會有一些疑問。</br>

在子線程啟動的時候,run() 函數里面已經持有了該對象鎖。</br>

但是真實環境下,其實是主線程先持有對象鎖,然后調用 wait() 進入等待區并且釋放鎖等待喚醒。

這個問題涉及到 JNI 代碼,目前我只能從理論上來解釋這個問題。
我們都知道一個線程 start() 并不是馬上啟動,而是需要 CPU 分配資源的,根據目前運行來看,分配資源的時間大于 Java 虛擬機運行指令的時間,所以主線程比子線程先拿到鎖。
我們還可以知道一點,控制臺打印出的時間是 3005 ms ,在代碼里我們只等待了 3s 多出來的 5ms (這個數字會浮動)我們可以推斷是,子線程獲取 CPU 的時間加上喚醒主線程的時間。

上述只是自己的一個猜測,能力還有欠缺,準備深入學習。

不過推薦大家看看這篇文章 Synchnornized 在 JVM 下的實現 - 簡書

Join() 的實踐

join() 的注釋上面寫著:

Waits for this thread to die.

意思是,阻塞當前調用 join() 函數所在的線程,直到接收線程執行完畢之后再繼續。
我們來看看實踐代碼:

public class JoinThread {

    public static void main(String[] args) {
        joinDemo();
    }

    public static void joinDemo() {
        Worker worker1 = new Worker("work-1");
        Worker worker2 = new Worker("work-2");
        worker1.start();
        System.out.println("啟動線程 1 ");

        try {
            // 調用 worker1 的 join 函數,主線,程會阻塞直到 woker1 執行完成
            worker1.join();
            System.out.println("啟動線程 2");
            // 再啟動線程 2 ,并且調用線程 2 的 join 函數,主線程會阻塞直到 woker2 執行完成
            worker2.start();
            worker2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主線程繼續執行");
    }

    static class Worker extends Thread {
        public Worker(String name) {
            super(name);
        }

        @Override
        public void run() {

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {

                e.printStackTrace();
            }

            System.out.println("work in " + getName());

        }
    }
}

運行之后我們得到:

啟動線程 1
work in work-1
啟動線程 2
work in work-2
主線程繼續執行

joinDemo() 方法里我們創建兩個子線程,然后啟動了 work1 線程,下一步調用了 woker1 的 join() 函數。此時,主線程會進入阻塞狀態,直到 work1 執行完畢之后才開始繼續執行。因為 Worker 的 run() 方法里會休眠 2 秒,因此線程每次調用了 join() 方法實際上都會阻塞 2 秒,直到 run() 方法執行完畢再繼續。
所以,上述代碼邏輯其實就是:

啟動線程1 —-> 等待線程 1 執行完畢 —-> 啟動線程2 —-> 等待線程 2 執行完畢 —-> 繼續執行主線程代碼

Yield() 的實踐

yield() 是 Thread 的靜態方法,注釋上說:

A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

大致意思是說:當前線程讓出執行時間給其他的線程。
我們都知道,線程的執行是有時間片的,每個線程輪流占用 CPU 固定時間,執行周期到了之后讓出執行權給其他線程。
yield() 就是主動讓出執行權給其他線程。

來看看我們實踐的代碼:

public class YieldThreadTest {

    public static void main(String[] args) {
        YieldTread t1 = new YieldTread("thread-1");
        YieldTread t2 = new YieldTread("thread-2");
        t1.start();
        t2.start();
    }

    public static class YieldTread extends Thread {

        public YieldTread(String name) {
            super(name);
        }

        public synchronized void run() {
            for (int i = 0; i < 5; i++) {
                System.out.printf("%s 優先級為 [%d] -------> %d\n", this.getName(), this.getPriority(), i);
                // 當 i 為 2 時,調用當前線程的 yield 函數
                if (i == 2) {
                    Thread.yield();

                }
            }
        }

    }

}

main() 方法里創建了兩個 YieldTread 線程,控制臺輸出結果如下:

thread-1 優先級為 [5] -------> 0
thread-1 優先級為 [5] -------> 1
thread-1 優先級為 [5] -------> 2

thread-2 優先級為 [5] -------> 0
thread-2 優先級為 [5] -------> 1
thread-2 優先級為 [5] -------> 2

thread-1 優先級為 [5] -------> 3
thread-1 優先級為 [5] -------> 4
thread-2 優先級為 [5] -------> 3
thread-2 優先級為 [5] -------> 4

通常情況下 t1 首先執行,讓 t1 的 run() 函數執行到了 i 等于 2 時讓出當前線程的執行時間。所以我們看到前三行都是 t1 在執行,讓出執行時間后 t2 開始執行。后面邏輯簡單思考下就得知了,這里也不做過多詮釋。

因此,調用 yield() 就是讓出當前線程的執行權,這樣一來讓其他線程得到優先執行。

總結與參考

本章內容屬于線程的基礎,本系列會更新到線程池相關。
這章內容也及其重要,因為它是后面的基礎。
正確理解才能讓我們對各種線程問題有方向和思路。

參考讀物

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容