Java多線程-線程生命週期(一)

如果要問我Java當中最難的部分是什麼?最有意思的部分是什麼?最多人討論的部分是什麼?那我會毫不猶豫地說:多線程。

Java多線程說它難,也不難,就是有點繞;說它簡單,也不簡單,需要理解的概念很多,尤其是很多底層知識,如數據結構、操作系統的部分。

Java多線程掌握得好,不僅僅只是對Java,對任何其他具有併發特性的編程語言,甚至是操作系統,都能有更全面和準確的認識。

Java多線程最大的特點,而且也是唯一確定的一件事,那就是:在多線程環境下,程序的運行結果是無法預料的,但這也正是它最有趣的地方。

在瞭解多線程之前,最好先知道什麼是併發,什麼是並行。不然很容易迷糊。

總的來說,就是這樣:

並行:同一時刻可以同時發生/執行多個任務。

併發:同一時刻只能發生/執行一個任務。


學習多線程最好從如下六個方面循序漸進(純粹個人經驗和建議,可無視):

1、線程生命週期:NEW、RUNNABLE(READY、RUNNING)、BLOCKED、WAITING、TIMED_WAITING、TERMINATED狀態

2、關鍵字:synchronized和volatile

3、線程池:ThreadPoolExecutor

4、鎖(AQS):悲觀鎖/樂觀鎖、輕量級鎖/重量級鎖、自旋鎖/可重入鎖等各種鎖

5、CAS:各種原子類

6、併發工具類:ArrayBlockingQueue、CountDownLatch、CyclicBarrier、Semaphore等


Java多線程用一句話總結就是「6類5法」。

所謂「6類」,就是多狀態的狀態分爲這6類:

1、新建(NEW):新創建了一個線程,但還沒調用start方法

2、運行(RUNNABLE)

2.1、就緒(ready):運行start方法後,線程位於可運行線程池中,等待被調度

2.2、運行中(RUNNING):就緒的線程獲得CPU的時間片就變爲運行中

3、阻塞(BLOCKED):線程等待獲取鎖

4、等待(WAITING):接收事件通知後或系統中斷後進入等待

5、超時(TIMED_WAITING):等待指定時間後會自行返回

6、終止(TERMINATED):線程已執行完畢

這是線程生命週期的狀態變化圖:

 

 

 簡單來說,就是這樣:

 

 

 

而所謂「5法」就是線程的核心方法是這麼5個:

1、wait:當前線程調用鎖對象的wait方法,當前線程釋放鎖,進入等待狀態,由其他線程接着執行

2、notify/notifyAll:喚醒任意一個或全部等待的線程後接着執行,但並不釋放鎖

3、join:當前線程調用其他線程的join方法,調用後當前線程進入等待狀態

4、yield:當前線程調用,調用後暫停執行(可能無效),變爲就緒態

5、sleep:當前線程調用,調用後進入TIME_WAITING狀態


 

用代碼來解釋一下會更直觀一些。

 

第一種wait/notify的情況:

public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized ("鎖") {
                System.out.println("t1 start");
                try {
                    // t1釋放鎖
                    "鎖".wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1 end");
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized ("鎖") {
                System.out.println("t2 start");
                try {
                    // 通知t1進入等待隊列
                    "鎖".notify();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("t2 end");
            }
        }
    });

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

此時代碼執行流程(兩種可能):

1、T1先執行

  1.1、T1啓動,wait讓出鎖,讓出CPU,T2獲得CPU,T2啓動,notify了通過object鎖等待的線程

  1.2、T1被喚醒後等待啓動,T2繼續執行,T2執行完,T1獲得CPU後繼續執行

2、T2先執行

  2.1、T2執行完,T1啓動,讓出CPU,由於沒有線程再來執行notify,程序無限期等待

這裏要強調的重點是:

1、wait會讓出CPU而notify不會

2、wait重點在通知其它同用一個object的線程“我暫時不用了”,並且讓出CPU

3、notify重點在於通知使用object的對象“我用完了!”

如果說只有兩個線程的時候,還能嘗試着分析一下結果,那麼當有四個線程的時候會如何呢?看看代碼:

public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized ("鎖") {
                System.out.println("t1 start");
                try {
                    // t1釋放鎖
                    "鎖".wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1 end");
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized ("鎖") {
                try {
                    System.out.println("t2 start");
                    // 隨機通知一個等待的線程進入等待隊列
                    "鎖".notify();
                    System.out.println("t2 end");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    });

    Thread t3 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized ("鎖") {
                try {
                    System.out.println("t3 start");
                    // 隨機通知一個等待的線程進入等待隊列
                    "鎖".notify();
                    System.out.println("t3 end");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    });
    Thread t4 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized ("鎖") {
                try {
                    System.out.println("t4 start");
                    // t4釋放鎖
                    "鎖".wait();
                    System.out.println("t4 end");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    });

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

然後同時開啓這四個線程,但結果是無法預料!爲什麼?因爲只有兩種可能的流程(要麼wait先執行完,要麼notify先執行完),至於每種流程裏面怎麼執行的?不知道!不清楚!無法預料!這就是多線程讓人困惑的地方和魅力所在。

而且線程還有一個無賴的行爲就是:雖然你有優先級,但我不保證有用!

public class MyThread extends Thread {
    MyThread(String s) {
        super(s);
    }

    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            System.out.println(getName() + " : " + i);
            if (i == 5) {
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主線程啓動");
        Thread t1 = new MyThread("t1");
        Thread t2 = new MyThread("t2");
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        t2.setPriority(Thread.MAX_PRIORITY);
        t2.start();
        t1.join();
        t2.join();
        System.out.println("主線程結束");
    }
}

這裏不管怎麼設置t1或者t2的優先級,都沒有用,運行的結果每次都可能不一樣。

線程的生命週期6類5法算是比較簡單的,是基礎中的基礎。但是用好很難,關鍵在於多練多想,多多嘗試各種組合。

 

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