Java - 線程入門

線程是程序中的執行線程,Java 虛擬機允許應用程序併發地運行多個執行線程。

每個線程都有一個優先級,高優先級線程的執行優先於低優先級線程。每個線程都可以或不可以標記爲一個守護程序。當某個線程中運行的代碼創建一個新 Thread 對象時,該新線程的初始優先級被設定爲創建線程的優先級,並且當且僅當創建線程是守護線程時,新線程纔是守護程序。

創建新執行線程有兩種方法:

  • 一種方法是將類聲明爲 Thread 的子類,該子類應重寫 Thread 類的 run() 方法,接下來可以分配並啓動該子類的實例。
  • 另一種方法是聲明實現 Runnable 接口的類,該類實現 run() 方法,然後可以分配該類的實例,在創建 Thread 時作爲一個參數來傳遞並啓動。

當 Java 虛擬機啓動時,通常都會有單個非守護線程(它通常會調用某個指定類的 main() 方法)。Java 虛擬機會繼續執行線程,直到下列任一情況出現時爲止:

  • 調用了 Runtime 類的 exit() 方法,並且安全管理器允許退出操作發生。
  • 非守護線程的所有線程都已停止運行,無論是通過從對 run() 方法的調用中返回,還是通過拋出一個傳播到 run() 方法之外的異常。

一、創建線程類

1 創建Thread類的子類

Java使用 java.lang.Thread 類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。

Java中通過繼承Thread類來創建並啓動多線程的步驟如下:

  1. 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體中設置線程需要完成的任務,因此把run()方法稱爲線程執行體。
  2. 創建Thread子類的實例,即創建了線程對象
  3. 調用線程對象的start()方法來啓動該線程。

start()方法使得該線程開始執行,JVM調用該線程的run()方法,其執行的結果是兩個線程併發地運行;當前線程(調用 start() 方法的那個線程)和另一個線程(創建的新線程,會執行其 run() 方法)。多次啓動一個線程是非法的,特別是當線程已經結束執行後,不能再重新啓動。 主線程是指執行主方法(main()方法)的那個線程,注意父線程不一定是主線程。

注:Java程序屬於搶佔式調度,哪個線程的優先級高哪個線程就優先執行,同一個優先級的線程則隨機選擇一個執行。

src/main/java/top/onefine/demo/thread/MyThread.java:

package top.onefine.demo.thread;

public class MyThread extends Thread {

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

src/main/java/top/onefine/demo/thread/Demo01Thread.java

package top.onefine.demo.thread;

public class Demo01Thread {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();  // mt對象
        myThread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main: " + i);
        }
    }
}

執行結果(某一種):

main: 0
main: 1
run: 0
run: 1
run: 2
run: 3
run: 4
main: 2
main: 3
run: 5
main: 4
main: 5
main: 6
main: 7
main: 8
main: 9
run: 6
run: 7
run: 8
run: 9

多線程內存圖解

在這裏插入圖片描述

程序啓動運行main時候,java虛擬機啓動一個進程,主線程main在main()調用時候被創建。隨着調用mt的對象的start方法,另外一個新的線程也啓動了,這樣,整個應用就在多線程下運行。

多線程執行時,在棧內存中每一個執行線程都有一片自己所屬的棧內存空間,進行方法的壓棧和彈棧。
在這裏插入圖片描述
在這裏插入圖片描述

當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那麼進程就結束了。

Thread類

至此已經可以完成最基本的線程開啓,在完成操作過程中用到了 java.lang.Thread 類,此類的API中中定義了有關線程的一些方法,具體如下:
構造方法

  • public Thread() :分配一個新的線程對象。
  • public Thread(String name) :分配一個指定名字的新的線程對象。
  • public Thread(Runnable target) :分配一個帶有指定目標新的線程對象。
  • public Thread(Runnable target, String name) :分配一個帶有指定目標新的線程對象並指定名字。

常用方法

  • public String getName():獲取當前線程名稱。可以先獲取到當前正在執行的線程,再通過此方法獲取當前線程名稱。
  • public void setName(String name):改變線程名稱,使之與參數name相同。
  • public void start():導致此線程開始執行; Java虛擬機調用此線程的run方法。
  • public void run():此線程要執行的任務在此處定義代碼。
  • public static void sleep(long millis):使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
  • public static Thread currentThread():返回對當前正在執行的線程對象的引用。
栗子1:獲取線程名稱

src/main/java/top/onefine/demo/thread/MyThread.java

package top.onefine.demo.thread;

public class MyThread extends Thread {

    @Override
    public void run() {
        String name = super.getName();// 直接獲取線程的名稱
        System.out.println("線程名稱:" + name);
    }
}

src/main/java/top/onefine/demo/thread/Demo01Thread.java

package top.onefine.demo.thread;

public class Demo01Thread {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();

        Thread currentThread = Thread.currentThread();
        System.out.println("主線程名稱:" + currentThread.getName());  // Thread.currentThread().getName();
        new MyThread().start();
        new MyThread().start();
    }
}

執行結果(不唯一):

線程名稱:Thread-0
主線程名稱:main
線程名稱:Thread-2
線程名稱:Thread-1
栗子2:設置線程名稱

src/main/java/top/onefine/demo/thread/MyThread.java:

package top.onefine.demo.thread;

public class MyThread extends Thread {

    public MyThread() {
        super();
    }

    // 創建一個帶參數的構造方法,參數傳遞線程的名稱;調用父類的帶參構造方法,
    // 把線程名稱傳遞給父類,讓父類(Thread)給子線程起一個名字
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println("線程名稱:" + Thread.currentThread().getName());
    }
}

src/main/java/top/onefine/demo/thread/Demo01Thread.java:

package top.onefine.demo.thread;

public class Demo01Thread {

    public static void main(String[] args) {
//        MyThread myThread = new MyThread();
//        myThread.setName("Thread-one");
        MyThread myThread = new MyThread("Thread-fine");
        myThread.start();

        System.out.println("主線程名稱:" + Thread.currentThread().getName());
        new MyThread().start();
        new MyThread().start();
    }
}
栗子3:sleep()使用

src/main/java/top/onefine/demo/thread/MyThread.java:

package top.onefine.demo.thread;

public class MyThread extends Thread {

    @Override
    public void run() {
        // 模擬秒錶
        for (int i = 0; i < 60; i++) {
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i + 1);
        }
    }
}

src/main/java/top/onefine/demo/thread/Demo01Thread.java:

package top.onefine.demo.thread;

public class Demo01Thread {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

2 實現Runnable接口方式(重點)

實現 java.lang.Runnable接口也是非常常見的一種,只需要重寫run()方法即可。Runnable 接口應該由那些打算通過某一線程執行其實例的類來實現。

步驟如下:

  1. 定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
  2. 創建Runnable實現類的實例,並以此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正的線程對象。
  3. 調用線程對象的start()方法來啓動線程。

src/main/java/top/onefine/demo/thread/MyThread.java:

package top.onefine.demo.thread;

public class MyThread implements Runnable {

    // 實現Runnable接口的run()方法,設置線程任務
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

src/main/java/top/onefine/demo/thread/Demo02Runnable.java:

package top.onefine.demo.thread;

public class Demo02Runnable {

    public static void main(String[] args) {
        // 1. 創建Runnable接口的實現類對象
        MyThread run = new MyThread();
        // 2. 創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象
        Thread myThread = new Thread(run);
        // 3. 調用Thread類中的start方法,開啓新的線程執行run方法
        myThread.start();
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

實現Runnable接口創建多線程程序的好處:
1.避免了單繼承的侷限性

  • Java中一個類只能繼承一個類,類繼承了Thread類就不能繼承其他的類
  • 實現了Runnable接口,還可以繼承其他的類,實現其他的接口

2.增強了程序的擴展性,降低了程序的耦合性(解耦)

  • 實現Runnable接口的方式,把設置線程任務和開啓新線程進行了分離(解耦)
  • 實現類中,重寫了run方法:用來設置線程任務
  • 創建Thread類對象,調用start方法:用來開啓新線程

通過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個執行目標。所有的多線程代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類

在啓動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用Thread對象的start()方法來運行多線程代碼。

實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。

Runnable對象僅僅作爲Thread對象的target,Runnable實現類裏包含的run()方法僅作爲線程執行體。
而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。

3. Thread和Runnable的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。

實現Runnable接口比繼承Thread類所具有的優勢總結:

  1. 適合多個相同的程序代碼的線程去共享同一個資源。
  2. 可以避免java中的單繼承的侷限性。
  3. 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
  4. 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。

擴充:在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM其實在就是在操作系統中啓動了一個進程。

4. 匿名內部類方式實現線程的創建

src/main/java/top/onefine/demo/thread/Demo03InnerClassThread.java:

package top.onefine.demo.thread;

public class Demo03InnerClassThread {

    public static void main(String[] args) {
        // Thread
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        }.start();

        // Runnable
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        }).start();
    }
}

執行結果(不唯一):

Thread-1: 0
Thread-0: 0
Thread-0: 1
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-1: 4
Thread-1: 5
Thread-0: 2
Thread-0: 3
Thread-1: 6
Thread-1: 7
Thread-0: 4
Thread-0: 5
Thread-0: 6
Thread-0: 7
Thread-1: 8
Thread-0: 8
Thread-1: 9
Thread-1: 10
Thread-0: 9
Thread-1: 11
Thread-1: 12
Thread-1: 13
Thread-1: 14
Thread-1: 15
Thread-1: 16
Thread-1: 17
Thread-1: 18
Thread-1: 19
Thread-0: 10
Thread-0: 11
Thread-0: 12
Thread-0: 13
Thread-0: 14
Thread-0: 15
Thread-0: 16
Thread-0: 17
Thread-0: 18
Thread-0: 19

二、線程安全

1. 線程安全

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

src/main/java/top/onefine/demo/thread/RunnableImpl.java:

package top.onefine.demo.thread;

/**
 * 實現賣票案例
 */
public class RunnableImpl implements Runnable {
    // 定義一個多線程共享的票源
    private int ticket = 100;

    // 設置線程任務:賣票
    @Override
    public void run() {
        // 讓賣票操作重複執行
        while (true) {
            // 先判斷票是否存在
            if (ticket > 0) {
                // 讓程序睡眠,大概率暴露出安全問題
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
                this.ticket--;
            } else break;
        }

    }
}

src/main/java/top/onefine/demo/thread/Demo04ThreadSafe.java:

package top.onefine.demo.thread;

/**
 * 模擬賣票案例
 */
public class Demo04ThreadSafe {

    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();

        Thread thread_1 = new Thread(runnable);
        Thread thread_2 = new Thread(runnable);
        Thread thread_3 = new Thread(runnable);

        thread_1.start();
        thread_2.start();
        thread_3.start();
    }
}

執行結果(不唯一):

Thread-1正在賣第100張票
Thread-0正在賣第100張票
Thread-2正在賣第100張票
Thread-2正在賣第97張票
Thread-1正在賣第97張票
Thread-0正在賣第97張票
Thread-2正在賣第94張票
Thread-0正在賣第94張票
Thread-1正在賣第94張票
Thread-0正在賣第91張票
Thread-2正在賣第91張票
Thread-1正在賣第91張票
Thread-0正在賣第88張票
Thread-2正在賣第88張票
Thread-1正在賣第88張票
Thread-2正在賣第85張票
Thread-1正在賣第85張票
Thread-0正在賣第85張票
Thread-1正在賣第82張票
Thread-0正在賣第82張票
Thread-2正在賣第82張票
Thread-2正在賣第79張票
Thread-0正在賣第79張票
Thread-1正在賣第79張票
Thread-1正在賣第76張票
Thread-0正在賣第76張票
Thread-2正在賣第76張票
Thread-2正在賣第73張票
Thread-1正在賣第73張票
Thread-0正在賣第73張票
Thread-1正在賣第70張票
Thread-0正在賣第70張票
Thread-2正在賣第70張票
Thread-0正在賣第67張票
Thread-1正在賣第67張票
Thread-2正在賣第67張票
Thread-2正在賣第64張票
Thread-1正在賣第64張票
Thread-0正在賣第64張票
Thread-1正在賣第61張票
Thread-2正在賣第61張票
Thread-0正在賣第61張票
Thread-2正在賣第58張票
Thread-1正在賣第58張票
Thread-0正在賣第58張票
Thread-0正在賣第55張票
Thread-2正在賣第55張票
Thread-1正在賣第55張票
Thread-0正在賣第52張票
Thread-2正在賣第52張票
Thread-1正在賣第52張票
Thread-2正在賣第49張票
Thread-1正在賣第49張票
Thread-0正在賣第49張票
Thread-2正在賣第46張票
Thread-1正在賣第46張票
Thread-0正在賣第46張票
Thread-2正在賣第43張票
Thread-0正在賣第42張票
Thread-1正在賣第42張票
Thread-1正在賣第40張票
Thread-2正在賣第40張票
Thread-0正在賣第40張票
Thread-0正在賣第37張票
Thread-1正在賣第36張票
Thread-2正在賣第37張票
Thread-1正在賣第34張票
Thread-0正在賣第34張票
Thread-2正在賣第32張票
Thread-1正在賣第31張票
Thread-0正在賣第31張票
Thread-2正在賣第29張票
Thread-0正在賣第28張票
Thread-1正在賣第28張票
Thread-2正在賣第26張票
Thread-0正在賣第25張票
Thread-1正在賣第25張票
Thread-2正在賣第23張票
Thread-0正在賣第22張票
Thread-1正在賣第22張票
Thread-2正在賣第20張票
Thread-0正在賣第19張票
Thread-1正在賣第19張票
Thread-2正在賣第17張票
Thread-1正在賣第16張票
Thread-0正在賣第16張票
Thread-2正在賣第14張票
Thread-1正在賣第13張票
Thread-0正在賣第13張票
Thread-2正在賣第11張票
Thread-0正在賣第10張票
Thread-1正在賣第10張票
Thread-2正在賣第8張票
Thread-1正在賣第7張票
Thread-0正在賣第7張票
Thread-2正在賣第5張票
Thread-1正在賣第4張票
Thread-0正在賣第4張票
Thread-2正在賣第2張票
Thread-0正在賣第1張票
Thread-1正在賣第1張票
Thread-2正在賣第-1張票

發現程序出現了兩個問題:

  1. 相同的票數,比如1這張票被賣了兩回。
  2. 不存在的票,比如-1票,是不存在的。

這種問題,幾個窗口(線程)票數不同步了,這種問題稱爲線程不安全。

線程安全問題是不能產生的,可以讓一個線程在訪問共享數據的時候,無論是否失去了cpu的執行權(控制權),讓其他的線程只能等待,等待當前程序賣完票,其他線程再進行賣票。保證只有一個線程在賣票。

線程安全問題都是由**全局變量(如類中定義int i = 9)及靜態變量(如static int j = 8;)**引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

2. 線程同步

當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。

要解決上述多線程併發訪問一個資源的安全性問題,也就是解決重複票與不存在票問題,就要保證每個線程都能正常執行原子操作,Java中提供了同步機制(synchronized)來解決。

有三種方式完成同步操作:

  1. 同步代碼塊

  2. 同步方法

  3. 鎖機制

2.1 同步代碼塊

同步代碼塊: synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。

同步鎖(對象鎖或對象監視器):對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖。鎖的作用是把同步代碼塊鎖住,只讓一個線程在同步代碼塊中執行。

  1. 鎖對象 可以是任意類型。
  2. 多個線程對象 要使用同一把鎖。

src/main/java/top/onefine/demo/thread/RunnableImpl.java:

package top.onefine.demo.thread;

/**
 * 使用同步代碼塊解決線程安全問題
 */
public class RunnableImpl implements Runnable {
    // 定義一個多線程共享的票源
    private int ticket = 100;

    // 創建一個鎖對象
    Object obj = new Object();

    // 設置線程任務:賣票
    @Override
    public void run() {
        // 讓賣票操作重複執行
        while (true) {
            // 同步代碼塊
            synchronized (obj) {
                // 先判斷票是否存在
                if (ticket > 0) {
                    // 讓程序睡眠,大概率暴露出安全問題
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
                    this.ticket--;
                } else break;
            }
        }
    }
}

線程遇到synchronized代碼塊時會檢查synchronized代碼塊是否有鎖對象,若有就會獲取到鎖對象,進入到同步中執行。執行完同步代碼塊中的代碼,線程就會把鎖對象歸還。若沒有就會進入到阻塞狀態,一直等待其他線程歸還鎖對象。即同步中的線程沒有執行完不會釋放鎖,同步外的線程沒有鎖進不去同步。

同步保證了只能有一個線程在同步中執行共享數據,保證了安全,但程序頻繁的判斷鎖,獲取鎖,釋放鎖,會造成程序的效率低下。

注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着(BLOCKED)。

2.2 同步方法

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着。同步方法把方法內部的代碼鎖住。

同步方法的鎖對象——同步鎖是誰?
對於非static方法,同步鎖就是this。
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)。

package top.onefine.demo.thread;

/**
 * 使用同步代碼方法解決線程安全問題
 */
public class RunnableImpl implements Runnable {
    // 定義一個多線程共享的票源
    private int ticket = 100;

    // 創建一個鎖對象
    Object obj = new Object();

    // 把訪問了共享數據的代碼抽取出來,放到一個方法中
    // 設置線程任務:賣票
    @Override
    public void run() {
    	// 可以優化,比如先判斷是否大於0
        // 讓賣票操作重複執行
        do {
            payTicket();
        } while (ticket > 0);
    }

    // 定義一個同步方法
    public synchronized void payTicket() {
        // 先判斷票是否存在
        if (ticket > 0) {
            // 讓程序睡眠,大概率暴露出安全問題
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
            this.ticket--;
        }
    }

    /* 
    public void payTicket() {
        // 先判斷票是否存在
        synchronized (this) {
            if (ticket > 0) {
                // 讓程序睡眠,大概率暴露出安全問題
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
                this.ticket--;
            }
        }
    }
    */
}

靜態方法:

package top.onefine.demo.thread;

/**
 * 使用同步代碼方法解決線程安全問題
 */
public class RunnableImpl implements Runnable {
    // 定義一個多線程共享的票源
    private static int ticket = 100;

    // 把訪問了共享數據的代碼抽取出來,放到一個方法中
    // 設置線程任務:賣票
    @Override
    public void run() {
        // 可以優化,比如先判斷是否大於0
        if (!(ticket > 0))
            return;
        // 讓賣票操作重複執行
        do {
            payTicketStatic();
        } while (ticket > 0);
    }

    // 定義一個靜態同步方法
    public static synchronized void payTicketStatic() {
        // 先判斷票是否存在
        if (ticket > 0) {
            // 讓程序睡眠,大概率暴露出安全問題
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
            ticket--;
        }
    }
}

this是創建對象之後產生的,靜態方法優先於對象,所以靜態方法的鎖對象是本類的class屬性,即class文件對象(反射):

package top.onefine.demo.thread;

/**
 * 使用同步代碼方法解決線程安全問題
 */
public class RunnableImpl implements Runnable {
    // 定義一個多線程共享的票源
    private static int ticket = 100;

    // 把訪問了共享數據的代碼抽取出來,放到一個方法中
    // 設置線程任務:賣票
    @Override
    public void run() {
        // 可以優化,比如先判斷是否大於0
        if (!(ticket > 0))
            return;
        // 讓賣票操作重複執行
        do {
            payTicketStatic();
        } while (ticket > 0);
    }

    // 定義一個同步方法
    public static void payTicketStatic() {
        // 先判斷票是否存在
        synchronized (RunnableImpl.class) {
            if (ticket > 0) {
                // 讓程序睡眠,大概率暴露出安全問題
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
                ticket--;
            }
        }
    }
}

2.3 鎖機制-Lock鎖

java.util.concurrent.locks.Lock機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:

  • public void lock() :加同步鎖。
  • public void unlock() :釋放同步鎖。

java.util.concurrent.locks.ReentrantLock類實現了Lock接口

package top.onefine.demo.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 使用Lock鎖解決線程安全問題
 */
public class RunnableImpl implements Runnable {
    // 定義一個多線程共享的票源
    private int ticket = 100;

    Lock lock = new ReentrantLock();

    // 把訪問了共享數據的代碼抽取出來,放到一個方法中
    // 設置線程任務:賣票
    @Override
    public void run() {
        // 可以優化,比如先判斷是否大於0
        if (!(ticket > 0))
            return;
        // 讓賣票操作重複執行
        do {
            // 先判斷票是否存在
            lock.lock();
            if (ticket > 0) {
                // 讓程序睡眠,大概率暴露出安全問題
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();  // 無論程序是否異常,都釋放鎖
                }
            }

        } while (ticket > 0);
    }
}

三、線程狀態

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中,有幾種狀態呢?在API中 java.lang.Thread.State 這個枚舉中給出了六種線程狀態:

線程狀態 導致狀態發生條件
NEW
(新建)
線程剛被創建,但是並未啓動。還沒調用start方法。
Runnable
(可運行)
線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操作系統處理器。
Blocked
(鎖阻塞)
當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。
Waiting
(無限等待)
一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。
TimedWaiting
(計時等待)
同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait。
Teminated
(被終止)
因爲run方法正常退出而死亡,或者因爲沒有捕獲的異常終止了run方法而死亡。

在這裏插入圖片描述

1. Timed Waiting(計時等待)

Timed Waiting在API中的描述爲:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處於這一狀態。單獨的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀態了,在哪裏呢?

在我們寫賣票的案例中,爲了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。

其實當我們調用了sleep方法之後,當前執行的線程就進入到“休眠狀態”,其實就是所謂的Timed Waiting(計時等待),那麼我們通過一個案例加深對該狀態的一個理解。

sleep方法的使用還是很簡單的。我們需要記住下面幾點:

  1. 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協作關係。
  2. 爲了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程中會睡眠
  3. sleep與鎖無關,線程睡眠到期自動甦醒,並返回到Runnable(可運行)狀態。

小提示:sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期後就開始立刻執行。

Timed Waiting 線程狀態圖:
在這裏插入圖片描述

2. BLOCKED(鎖阻塞)

Blocked狀態在API中的介紹爲:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處於這一狀態。

如同步機制中,線程A與線程B代碼中使用同一鎖,如果線程A獲取到鎖,線程A進入到Runnable狀態,那麼線程B就進入到Blocked鎖阻塞狀態。

這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種情況下進入阻塞狀態(瞭解)。

Blocked 線程狀態圖:

在這裏插入圖片描述

3. Waiting(無限等待)

Wating狀態在API中介紹爲:一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處於這一狀態。

package top.onefine.demo.thread;

/**
 *    等待喚醒案例:線程之間的通信
 *         創建一個顧客線程(消費者):告知老闆要的包子的種類和數量,調用wait方法,放棄cpu的執行,進入到WAITING狀態(無限等待)
 *         創建一個老闆線程(生產者):花了5秒做包子,做好包子之後,調用notify方法,喚醒顧客喫包子
 *
 *     注意:
 *         顧客和老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒只能有一個在執行
 *         同步使用的鎖對象必須保證唯一
 *         只有鎖對象才能調用wait和notify方法
 *
 *     Obejct類中的方法
 *     void wait()
 *           在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
 *     void notify()
 *           喚醒在此對象監視器上等待的單個線程。
 *           會繼續執行wait方法之後的代碼
 *
 */
public class Demo01WaitAndNotify {

    public static void main(String[] args) {
        // 創建鎖對象,保證鎖對象唯一
        Object obj = new Object();
        // 創建一個顧客線程(消費者)
        new Thread() {
            @Override
            public void run() {
                // 一直等着買包子
                while (true) {
                    // 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                    synchronized (obj) {
                        System.out.println("告知老闆要的包子的種類和數量");
                        // 調用wait方法,自動放棄cpu的執行,進入到waiting狀態(無限等待)
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 喚醒之後執行的代碼
                        System.out.println("開喫做好的包子!");
                    }
                }
            }
        }.start();

        // 創建老闆線程(生產者)
        new Thread() {
            @Override
            public void run() {
                // 一直做包子
                while (true) {
                    // 老闆花5s做包子
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                    synchronized (obj) {
                        System.out.println("老闆5s之後做好包子,告知顧客可以喫包子了");
                        // 做好包子後調用notify方法喚醒顧客喫包子
                        obj.notify();  // 注意,與wait()方法使用的鎖是同一個
                    }
                }
            }
        }.start();
    }
}
package top.onefine.demo.thread;

/**
 *
 *    進入到TimeWaiting(計時等待)有兩種方式
 *     1.使用sleep(long m)方法,在毫秒值結束之後,線程睡醒進入到Runnable/Blocked狀態
 *     2.使用wait(long m)方法,wait方法如果在毫秒值結束之後,還沒有被notify喚醒,就會自動醒來,線程睡醒進入到Runnable/Blocked狀態
 *
 *     喚醒的方法:
 *          void notify() 喚醒在此對象監視器上等待的單個線程。
 *          void notifyAll() 喚醒在此對象監視器上等待的所有線程。
 */
public class Demo01WaitAndNotify {

    public static void main(String[] args) {
        // 創建鎖對象,保證鎖對象唯一
        Object obj = new Object();
        // 創建一個顧客線程(消費者)
        new Thread() {
            @Override
            public void run() {
                // 一直等着買包子
                while (true) {
                    // 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                    synchronized (obj) {
                        System.out.println("顧客1告知老闆要的包子的種類和數量");
                        // 調用wait方法,自動放棄cpu的執行,進入到waiting狀態(無限等待)
                        try {
//                            obj.wait(5000);  // 自己等5s,時間到立刻醒來,無論老闆做沒做好包子
                            obj.wait();  // 只有等老闆喚醒
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 喚醒之後執行的代碼
                        System.out.println("顧客1開喫做好的包子!");
                    }
                }
            }
        }.start();

        // 創建一個顧客線程(消費者)
        new Thread() {
            @Override
            public void run() {
                // 一直等着買包子
                while (true) {
                    // 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                    synchronized (obj) {
                        System.out.println("顧客2告知老闆要的包子的種類和數量");
                        // 調用wait方法,自動放棄cpu的執行,進入到waiting狀態(無限等待)
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 喚醒之後執行的代碼
                        System.out.println("顧客2開喫做好的包子!");
                    }
                }
            }
        }.start();

        // 創建老闆線程(生產者)
        new Thread() {
            @Override
            public void run() {
                // 一直做包子
                while (true) {
                    // 老闆花5s做包子
                    try {
                        Thread.sleep(5000);  // 睡眠5s
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                    synchronized (obj) {
                        System.out.println("老闆5s之後做好包子,告知顧客可以喫包子了");
                        // 做好包子後調用notify方法喚醒顧客喫包子
                        // 若有多個等待線程,則隨機喚醒一個等待的進程
//                        obj.notify();  // 注意,與wait()方法使用的鎖是同一個
                        // 喚醒所有等待的線程
                        obj.notifyAll();
                    }
                }
            }
        }.start();
    }
}

通過上述案例我們會發現,一個調用了某個對象的Object.wait()方法的線程會等待另一個線程調用此對象的Object.notify()方法或Object.notifyAll()方法。

其實waiting狀態並不是一個線程的操作,它體現的是多個線程間的通信,可以理解爲多個線程之間的協作關係,多個線程會爭取鎖,同時相互之間又存在協作關係。就好比在公司裏你和你的同事們,你們可能存在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那麼A線程就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了notify()方法,那麼就會將無限等待的A線程喚醒。注意是喚醒,如果獲取到鎖對象,那麼A線程喚醒後就進入Runnable(可運行)狀態;如果沒有獲取鎖對象,那麼就進入到Blocked(鎖阻塞狀態)。

Waiting 線程狀態圖:
在這裏插入圖片描述

4. 總結

在這裏插入圖片描述

在翻閱API的時候會發現Timed Waiting(計時等待) 與 Waiting(無限等待) 狀態聯繫還是很緊密的,比如Waiting(無限等待) 狀態中wait方法是空參的,而timed waiting(計時等待) 中wait方法是帶參的。這種帶參的方法,其實是一種倒計時操作,相當於我們生活中的小鬧鐘,我們設定好時間,到時通知,可是如果提前得到(喚醒)通知,那麼設定好時間在通知也就顯得多此一舉了,那麼這種設計方案其實是一舉兩得。如果沒有得到(喚醒)通知,那麼線程就處於Timed Waiting狀態,直到倒計時完畢自動醒來;如果在倒計時期間得到(喚醒)通知,那麼線程從Timed Waiting狀態立刻喚醒。

四、等待喚醒機制

1. 線程間通信

**概念:**多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。

比如:線程A用來生成包子的,線程B用來喫包子的,包子可以理解爲同一資源,線程A與線程B處理的動作,一個是生產,一個是消費,那麼線程A與線程B之間就存在線程通信問題。

爲什麼要處理線程間通信:
多個線程併發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,並且我們希望他們有規律的執行, 那麼多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。

如何保證線程間通信有效利用資源:
多個線程在處理同一個資源,並且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作。 就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪。也就是我們需要通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制

2. 等待喚醒機制

什麼是等待喚醒機制
這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這並不是故事的全部,線程間也會有協作機制。就好比在公司裏你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

就是在一個線程進行了規定操作後,就進入等待狀態(wait()), 等待其他線程執行完他們的指定代碼過後 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。

wait/notify 就是線程間的一種協作機制。

等待喚醒中的方法
等待喚醒機制就是用於解決線程間通信的問題的,使用到的3個方法的含義如下:

  1. wait:線程不再活動,不再參與調度,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等着別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
  2. notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置後,等候就餐最久的顧客最先入座。
  3. notifyAll:則釋放所通知對象的 wait set 上的全部線程。

注意:
哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因爲它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功後才能在當初調用 wait 方法之後的地方恢復執行。

總結如下:

  • 如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;
  • 否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態

調用wait和notify方法需要注意的細節

  1. wait方法與notify方法必須要由同一個鎖對象調用。因爲:對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法後的線程。
  2. wait方法與notify方法是屬於Object類的方法的。因爲:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
  3. wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用。因爲:必須要通過鎖對象調用這2個方法。

3. 生產者與消費者問題

等待喚醒機制其實就是經典的“生產者與消費者”的問題。

就拿生產包子消費包子來說等待喚醒機制如何有效利用資源:

包子鋪線程生產包子,喫貨線程消費包子。當包子沒有時(包子狀態爲false),喫貨線程等待,包子鋪線程生產包子(即包子狀態爲true),並通知喫貨線程(解除喫貨的等待狀態),因爲已經有包子了,那麼包子鋪線程進入等待狀態。接下來,喫貨線程能否進一步執行則取決於鎖的獲取情況。如果喫貨獲取到鎖,那麼就執行喫包子動作,包子喫完(包子狀態爲false),並通知包子鋪線程(解除包子鋪的等待狀態),喫貨線程進入等待。包子鋪線程能否進一步執行則取決於鎖的獲取情況。

包子類:

package top.onefine.demo.thread;

/**
 * 資源類:包子類
 */
public class BaoZi {
    String pi;  // 包子屬性:皮
    String xian;  // 包子屬性:餡

    Boolean flag = false;   // 包子狀態,默認沒有
}

包子鋪類:

package top.onefine.demo.thread;

/**
 *
 * 生產者(包子鋪)
 *      注意:包子鋪線程和包子線程關係爲通信關係(互斥)
 *          必須使用同步技術保證兩個線程只能有一個在執行
 *          鎖對象必須唯一,可以使用包子對象作爲鎖對象
 *          包子鋪類和喫貨類就需要把包子作爲參數傳遞進來
 */
public class BaoZiPu extends Thread {

    // 定義包子變量
    private BaoZi baoZi;

    public BaoZiPu(BaoZi baoZi) {
        this.baoZi = baoZi;
    }

    @Override
    public void run() {
        int count = 0;  // 交替生產兩種包子
        // 讓包子鋪一直生產包子
        while (true) {
            synchronized (baoZi) {
                if (baoZi.flag)  // 有包子
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                // 沒包子
                // 被喚醒之後執行,包子鋪生產包子
                if (count % 2 == 0) {
                    // 生產薄皮肉餡包子
                    baoZi.pi = "薄皮";
                    baoZi.xian = "肉餡";
                } else {
                    // 生產冰皮菜餡包子
                    baoZi.pi = "冰皮";
                    baoZi.xian = "菜餡";
                }
                count++;
                System.out.println("包子鋪正在生產:" + baoZi.pi + baoZi.xian + "的包子!");
                // 生產包子需要3s鍾
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 修改包子狀態爲有包子
                baoZi.flag = true;
                // 喚醒喫貨線程
                baoZi.notify();
                System.out.println("包子鋪已經生產好了" + baoZi.pi + baoZi.xian + "的包子!");
            }
        }
    }
}

喫貨類:

package top.onefine.demo.thread;

/**
 * 消費者(喫貨類)
 */
public class ChiHu extends Thread {

    // 定義包子變量
    private BaoZi baoZi;

    public ChiHu(BaoZi baoZi) {
        this.baoZi = baoZi;
    }

    @Override
    public void run() {
        // 使用死循環讓喫貨一直喫包子
        while (true) {
            synchronized (baoZi) {
                if (!baoZi.flag)  // 沒有包子
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                // 被喚醒後喫貨喫包子
                System.out.println("喫貨正在喫:" + baoZi.pi + baoZi.xian + "的包子!");
                // 喫貨喫完包子
                baoZi.flag = false;
                // 喚醒包子鋪線程生產包子
                baoZi.notify();
                System.out.println("喫貨喫完了" + baoZi.pi + baoZi.xian + "的包子,包子鋪開始快生產包子!");
            }
        }
    }
}

場景類:

package top.onefine.demo.thread;

/**
 * 場景類
 */
public class Client {
    public static void main(String[] args) {
        // 創建包子對象
        BaoZi baoZi = new BaoZi();
        // 創建包子鋪線程生產包子
        new BaoZiPu(baoZi).start();
        // 創建喫貨線程喫包子
        new ChiHu(baoZi).start();
    }
}

五、線程池

在使用線程的時候就要去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬而是可以繼續執行其他的任務?

在Java中可以通過線程池來達到這樣的效果,在JDK1.5之後,JDK內置了線程池,可以直接使用!

1. 線程池概念

**線程池:**其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。

在這裏插入圖片描述

合理利用線程池能夠帶來三個好處:

  1. 降低資源消耗。減少了創建和銷燬線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因爲消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。

2. 線程池的使用

Java裏面線程池的頂級接口是 java.util.concurrent.Executor ,但是嚴格意義上講 Executor 並不是一個線程池,而只是一個執行線程的工具(線程池的工廠類),真正的線程池接口是 java.util.concurrent.ExecutorService

要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在 java.util.concurrent.Executors 線程工廠類裏面提供了一些靜態工廠,生成一些常用的線程池。

官方建議使用Executors工廠類來創建線程池對象。Executors類中有個創建線程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads) :返回線程池對象——ExecutorService接口的實現類對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)

獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法如下:

  • public Future<?> submit(Runnable task) :獲取線程池中的某一個線程對象並執行。Future接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用。

使用線程池中線程對象的步驟:

  1. 創建線程池對象。
  2. 創建Runnable接口子類對象。(task)
  3. 提交Runnable接口子類對象。(take task)
  4. 關閉線程池(一般不做)。
package top.onefine.demo.threadPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 *
 *  線程池:JDK1.5之後提供的
 *     java.util.concurrent.Executors:線程池的工廠類,用來生成線程池
 *     Executors類中的靜態方法:
 *         static ExecutorService newFixedThreadPool(int nThreads) 創建一個可重用固定線程數的線程池
 *         參數:
 *             int nThreads:創建線程池中包含的線程數量
 *         返回值:
 *             ExecutorService接口,返回的是ExecutorService接口的實現類對象,我們可以使用ExecutorService接口接收(面向接口編程)
 *     java.util.concurrent.ExecutorService:線程池接口
 *         用來從線程池中獲取線程,調用start方法,執行線程任務
 *             submit(Runnable task) 提交一個 Runnable 任務用於執行
 *         關閉/銷燬線程池的方法
 *             void shutdown()
 *     線程池的使用步驟:
 *         1.使用線程池的工廠類Executors裏邊提供的靜態方法newFixedThreadPool生產一個指定線程數量的線程池
 *         2.創建一個類,實現Runnable接口,重寫run方法,設置線程任務
 *         3.調用ExecutorService中的方法submit,傳遞線程任務(實現類),開啓線程,執行run方法
 *         4.調用ExecutorService中的方法shutdown銷燬線程池(不建議執行)
 */
public class DemoThreadPool {
    public static void main(String[] args) {
        //1.使用線程池的工廠類Executors裏邊提供的靜態方法newFixedThreadPool生產一個指定線程數量的線程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.調用ExecutorService中的方法submit,傳遞線程任務(實現類),開啓線程,執行run方法
        es.submit(new RunnableImpl());
        //線程池會一直開啓,使用完了線程,會 自動 把線程歸還給線程池,線程可以繼續使用
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());

        //4.調用ExecutorService中的方法shutdown銷燬線程池(不建議執行)
        es.shutdown();

        //拋異常,線程池都沒有了,就不能獲取線程了
//        es.submit(new RunnableImpl());
    }
}

/**
 * 2.創建一個類,實現Runnable接口,重寫run方法,設置線程任務
 */
class RunnableImpl implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "創建了一個新的線程執行");
    }
}

執行結果(不唯一):

pool-1-thread-2創建了一個新的線程執行
pool-1-thread-2創建了一個新的線程執行
pool-1-thread-1創建了一個新的線程執行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章