Java 線程淺析

一、什麼是線程
要理解什麼線程,我麼得先知道什麼是進程。現代操作系統在運行一個程序時,會爲其創建一個進程。例如啓動eclipse.exe其實就是啓動了win系統的一個進程。現代操作系統調度的最小單元就是線程,也叫輕量級進程,在一個進程裏面包含多個線程,這些線程都有各自的計數器、堆棧等,並且能夠共享內存變量。例如我們啓動了一個eclipse進程,我們運行在其中的程序就可以理解爲線程。
二、爲什麼要使用線程
(1)更多的處理器核心(可以運行更多的線程)。
(2)更快的響應時間(線程可以並行執行)。
(3)更好的編程模型。
三、線程的狀態
Java線程在運行的生命週期中有6中不同的狀態,在給定的一個時刻,線程只能處於其中一個狀態。如下圖所示。

狀態名稱 說明
NEW 初始狀態,線程被創建,但是還沒有調用start方法。
RUNNABLE 運行狀態,Java線程將操作系統中的就緒和運行兩種狀態統稱地稱作“運行中”
BLOCKED 阻塞狀態,表示線程阻塞於鎖
WAITING 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷)
TIME_WAITING 超時狀態,該狀態不同於WAITING,它是可以在指定時間自行返回的
TERMINATED 停止狀態,表示當前線程已經執行完畢

四、線程的調度(狀態的變化)
我們先來看一張圖:線程的調度對線程狀態的影響
Java 線程淺析
1)NEW(狀態)線程創建未啓動時的狀態。如下代碼示例:

Thread thread = new Thread(new ThreadTest());
        System.out.println(thread.getState());

輸出結果:
NEW

Process finished with exit code 0
2)NEW-RUNNABLE線程調用start方法。如下代碼示例:

Thread thread = new Thread(new ThreadTest());
        thread.start();
        System.out.println(thread.getState());

輸出結果:
RUNNABLE
線程調用yield()方法,yield方法的作用就是讓出CPU,當前線程從運行中變爲可運行狀態(READY),讓和它同級或者更高級別的線程運行,但是不能保證運行的線程立馬變成可運行狀態(不確定的)。看如下代碼示例:
代碼設置了線程的優先級,但是測試了幾次的測試結果都不相同。

**
 * 測試yield方法
 */
public class ThreadYieldTest {

    public static void main (String[] args) {
        Thread threadone = new Thread(new ThreadTestOne(),"ThreadTestOne");
        Thread threadtwo = new Thread(new ThreadTestTwo(),"ThreadTestTwo");
        threadone.setPriority(Thread.MIN_PRIORITY);
        threadtwo.setPriority(Thread.MAX_PRIORITY);
        threadone.start();
        threadtwo.start();
    }

    static class ThreadTestOne implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("ThreadTestOne----MIN_PRIORITY-----"+i);
                Thread.yield();
            }
        }
    }

    static class ThreadTestTwo implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("ThreadTestTwo-----MAX_PRIORITY----"+i);
                Thread.yield();
            }
        }
    }
}

結果一:
ThreadTestOne----MIN_PRIORITY-----0
ThreadTestTwo-----MAX_PRIORITY----0
ThreadTestOne----MIN_PRIORITY-----1
ThreadTestTwo-----MAX_PRIORITY----1
結果二:
ThreadTestTwo-----MAX_PRIORITY----0
ThreadTestOne----MIN_PRIORITY-----0
ThreadTestTwo-----MAX_PRIORITY----1
ThreadTestOne----MIN_PRIORITY-----1
ThreadTestTwo-----MAX_PRIORITY----2
ThreadTestTwo-----MAX_PRIORITY----3

3)RUNNABLE-WAITING(調用wait、join等方法)
(1)、wait方法可以讓一個線程的狀態變爲WAITING或者TIME_WAITING,wait方法的作用是讓當前線程進入等待隊列,讓出CPU的執行權,線程的狀態變化爲等待狀態,執行wait方法的前提就是獲取到對象鎖,因爲執行線程需要知道進入誰的等待隊列,之後才能被誰喚醒。看如下代碼示例:(jps 看java的進程id,jstack 看java線程的信息)

static class Parent implements Runnable {

        @Override
        public void run() {
            synchronized (lock){
                System.out.println("執行lock.wait");
                try {
                    lock.wait();
                    // 不會執行
                    System.out.println(Thread.currentThread().getName()+"----------"+Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

通過命令看下線程信息如下圖所示:可以看到Thread.State:WAITING
Java 線程淺析

(2)、join方法也可以使線程以讓一個線程的狀態變爲WAITING或者TIME_WAITING,join的方法的作用,字面意思加入、參加。我們可以這麼理解join方法,一個線程加入一個正在運行的主線程中,並且使得正在運行的主線程等待加入的線程執行完畢才能繼續執行。看如下代碼示例:
1、我們在main方法啓動兩個線程,分別調用jion方法和不調用,看下執行結果一(不調用join方法):
Parent-----------------
Child-----------
結果順序不確定
Child-----------
Parent-----------------

public static void main (String[] args) {
        Thread thread = new Thread(new Parent());
        thread.setName("Parent");

        Thread threadChild = new Thread(new Child());
        threadChild.setName("Child");

        thread.start();
        threadChild.start();
    }

    /**
     * 父線程
     */
    static class Parent implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"-----------------");
        }
    }

    /**
     * 子線程
     */
    static class Child  implements Runnable {

        @Override
        public void run() {
                System.out.println(Thread.currentThread().getName()+"-----------");
        }
    }

結果二(其中一個調用join方法)
(多次運行結果順序不變)
Parent-----------------
Child-----------

public static void main (String[] args) {
        Thread thread = new Thread(new Parent());
        thread.setName("Parent");

        Thread threadChild = new Thread(new Child());
        threadChild.setName("Child");

        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadChild.start();
    }

2、join方法源碼
可以看出join方法是調用的wait方法,所以線程的狀態會變成WAITING或者TIME_WAITING。
我們來分析下是哪個線程加入等待隊列,可以看出join方法是個synchronized方法,也就是說鎖對象就是調用者,然後擁有鎖對象的線程調用wait方法進入等待隊列。我們通過上面的main方法來分析到底是誰進入等待隊列,主角有main線程、Parent線程、Child線程,我們在main線程裏面new了Parent線程和Child線程,然後在Child線程啓動前面調用了Parent線程的join方法,也就是說是Parent線程調用了join方法,所以鎖對象就是Parent線程實例,我們再來分析是那個線程擁有這個鎖對象,答案是main線程,所以main線程調用wait方法進入等待隊列Parent線程執行完畢;join方法結束時會喚醒主線程。

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) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

4)RUNNABLE-TIME_WAITING(調用sleep、wait、join等方法)
sleep(long)方法就是讓線程睡眠一定的時間在執行,不過這個是有時間限制的,到了時間就會又變成RUNNABLE狀態。如下代碼示例:

static class Parent implements Runnable {

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println("-----------------------");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

結果
Java 線程淺析
join(long)和wait(long)方法和WAITING狀態的方式類似,只是加入時間後線程可以自動喚醒,自動從等待隊列加入同步隊列,獲取到鎖變成RUNNABLE狀態。
sleep和wait的區別:
相同點:sleep和wait都可以使線程等待特定的時間;都可以使用interrupt()後調用阻塞方法中斷線程;
不同點:sleep是Thread方法,wait是Object的方法;slepp到了時間自動喚醒,而wait沒有規定時間時需要手動喚醒;在synchronized關鍵字修飾的方法或者塊中,sleep不會釋放鎖,wait會釋放鎖。
4)TIME_WAITING or WAITING-RUNNABLE(調用notify()、notifyAll(),sleep時間到了)
sleep時間到了線程就會進入RUNNABLE狀態(但是可能是RUNNING or READY狀態)。
notify()是喚醒一個線程,進入同步隊列,沒有獲取到鎖就是BLOCKED狀態,獲取到鎖就是RUNNABLE狀態。
notifyAll()是喚醒等待隊列的所有線程進入同步隊列。
5)RUNNABLE-BLOCKED(線程獲取鎖失敗,進入同步隊列)
如下代碼示例:

static class Parent implements Runnable {

        @Override
        public void run() {
            synchronized (lock) {
                    System.out.println(Thread.currentThread().getName()+"----------------------------------");
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 子線程
     */
    static class Child  implements Runnable {

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName()+"----------------------------------");
            }
        }
    }

結果Java 線程淺析
6)RUNNABLE-TERMINATED(線程執行完畢)
五、如何優雅終止線程(手動)
我們知道線程提供了interrupt()、stop()方法;中斷線程的三種方式;
1)stop方法,停止一個線程,現在已經是一個過期方法(不推薦使用)。
代碼示例:

public static void main (String[] args) throws InterruptedException {
        Thread thread = new Thread(new Parent(),"Parent");
        thread.start();
        Thread.sleep(200);
        thread.stop();
    }

    /**
     * 父線程
     */
    static class Parent implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println("-----------------------");
            }
        }
    }

2) 使用interrupt()方法,只是給線程設置了一箇中斷標記,並不會中斷線程,配合阻塞方法才能實現線程的中斷。
如下代碼示例:
中斷了
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.ge.thread.method.ThreadStopTest$Parent.run(ThreadStopTest.java:30)
at java.lang.Thread.run(Thread.java:745)

public static void main (String[] args)  {
        Thread thread = new Thread(new Parent(),"Parent");
        thread.start();
        thread.interrupt();
    }

    /**
     * 父線程
     */
    static class Parent implements Runnable {

        @Override
        public void run() {
            try {
                while (true) {
                    /*System.out.println("------------");
                    // 測試 isInterrupted方法
                    System.out.println("0isInterrupted------"+Thread.currentThread().isInterrupted());
                    // 測試interrupted方法
                    System.out.println("1interrupted------"+  Thread.interrupted());
                    System.out.println("2interrupted------"+  Thread.interrupted());*/
                    Thread.sleep(500);

                }
            }catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("中斷了");
            }
        }
    }

interrupt、interrupted和isInterrupted方法的區別,大家可以運行上面註釋代碼看運行結果。
interrupt:給線程一箇中斷標記,配合阻塞方法中斷線程,拋出InterruptedException異常,並清除標記。
interrupted:Thread的靜態方法,返回線程的中斷標記狀態,並且清理,所以第一次返回true或者false,第二次一定是false。
isInterrupted:返回線程的中斷標記狀態。
3)給線程的執行設置一個標記,滿足就執行,不滿足就結束線程。
如下代碼示例:

 private static volatile boolean flag = true;

    public static void main (String[] args) {
        Thread thread = new Thread(new Parent());
        thread.setName("Parent");
        thread.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 中斷線程標記
        flag = false;
    }

    /**
     * 父線程
     */
    static class Parent implements Runnable {

        @Override
        public void run() {
            while (flag && !Thread.currentThread().isInterrupted()) {
                System.out.println("--------------");
            }
        }
    }

4)總結,stop方法就是強行結束線程,不推薦可能造成業務數據未知的錯誤,因爲線程運行在那個過程時未知的;interrupt通過標記和阻塞方法一起中斷線程,會拋出異常,但是要對異常做處理,例如處理線程爲執行完成的任務或者回滾保證業務正確;推薦標記法,因爲線程會走一個完整的過程,不會出現業務方面的未知錯誤,線程要麼執行,要麼不執行,不會執行一半就退出,所以就不會出現未知錯誤。
六、總結
本文針對線程的生命週期,線程的每個狀態進行解釋,以及線程執行過程的狀態變化;線程調度對線程狀態的影響,以及一些線程的基本方法;最後介紹了停止線程的三種方式;通過學習這些有助於我們理解線程的基礎,爲學習多線程打下基礎,只有學習好了單線程,才能更好的學習多線程;希望與諸君共勉。

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