1. 程式人生 > 實用技巧 >Java多執行緒學習筆記整理

Java多執行緒學習筆記整理

多執行緒基礎

1、建立和執行執行緒

1.1 方式一:直接使用Thread類

// 建立執行緒物件
Thread t = new Thread() {
 public void run() {
 // 要執行的任務
 }
};
// 啟動執行緒
t.start();

1.2 方式二:使用 Runnable 配合 Thread

把【執行緒】和【任務】(要執行的程式碼)分開

  • Thread代表執行緒
  • Runnable定義執行緒要執行的具體任務
Runnable runnable = new Runnable() {
 public void run(){
 // 要執行的任務
 }
};
// 建立執行緒物件
Thread t = new Thread( runnable );
// 啟動執行緒
t.start();

用Runnable定義任務的好處

  • 用 Runnable 更容易與執行緒池等高階 API 配合
  • 用 Runnable 讓任務類脫離了 Thread 繼承體系,更靈活

1.3 方法三:FutureTask 配合 Thread

FutureTask 能夠接收 Callable 型別的引數,用來處理有返回結果的情況


// 建立任務物件
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 引數1 是任務物件; 引數2 是執行緒名字,推薦
new Thread(task3, "t3").start();
// 主執行緒阻塞,同步等待 task 執行完畢的結果
Integer result = task3.get();
log.debug("結果是:{}", result)

2、檢視程序執行緒的方法

windows

  • 工作管理員可以檢視程序和執行緒數,也可以用來殺死程序
  • tasklist 檢視程序
  • taskkill 殺死程序

linux

  • ps -ef 檢視所有程序
  • ps -fT -p <PID> 檢視某個程序(PID)的所有執行緒
  • kill 殺死程序
  • top 按大寫 H 切換是否顯示執行緒
  • top -H -p <PID> 檢視某個程序(PID)的所有執行緒

java

  • jps 命令檢視所有 Java 程序
  • jstack 檢視某個 Java 程序(PID)的所有執行緒狀態
  • jconsole 來檢視某個 Java 程序中執行緒的執行情況(圖形介面)

3、Thread 常用API

方法名 static 功能 備註
start() 啟動一個新執行緒,並執行run()方法中的程式碼 start 方法只是讓執行緒進入就緒,裡面程式碼不一定立刻執行(CPU 的時間片還沒分給它)。每個執行緒物件的start方法只能呼叫一次,如果呼叫了多次會出現IllegalThreadStateException
run() 新執行緒啟動後會呼叫的方法 如果在構造 Thread 物件時傳遞了 Runnable 引數,則執行緒啟動後會呼叫 Runnable 中的 run 方法,否則預設不執行任何操作。但可以建立 Thread 的子類物件,來覆蓋預設行為
join() 等待執行緒執行結束
join(long n) 等待執行緒執行結束,最多等待n毫秒
getId() 獲取執行緒長整型id id唯一
getName() 獲取執行緒名
setName(String) 修改執行緒名
getPriority() 獲取執行緒優先順序
setPriority(int) 設定執行緒優先順序 java中規定執行緒優先順序是1~10 的整數,較大的優先順序能提高該執行緒被 CPU 排程的機率
getState() 獲取執行緒狀態 Java 中執行緒狀態是用 6 個 enum 表示,分別為:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() 判斷是否被打斷 不會清除打斷標記
isAlive() 執行緒是否存活
interrupt() 打斷執行緒 如果被打斷執行緒正在 sleep,wait,join 會導致被打斷的執行緒丟擲 InterruptedException,並清除 打斷標記 ;如果打斷的正在執行的執行緒,則會設定 打斷標記 ;park 的執行緒被打斷,也會設定 打斷標記
interrupted() static 判斷當前執行緒是否被打斷 會清除打斷標記
currentThread() static 獲取當前正在執行的執行緒
sleep(long n) static 讓當前正在執行的執行緒休息n毫秒,休眠時讓出CPU的時間片給其它執行緒
yield() static 提示執行緒排程器讓出當前執行緒對CPU的使用

3.1 sleep 與 yield

sleep

  1. 呼叫 sleep 會讓當前執行緒從 Running 進入 Timed Waiting 狀態(阻塞)
  2. 其它執行緒可以使用 interrupt 方法打斷正在睡眠的執行緒,這時 sleep 方法會丟擲InterruptedException
  3. 睡眠結束後的執行緒未必會立刻得到執行
  4. 建議用 TimeUnit 的 sleep 代替 Thread 的 sleep 來獲得更好的可讀性

yield

1.呼叫 yield 會讓當前執行緒從 Running 進入 Runnable 就緒狀態,然後排程執行其它執行緒
2. 具體的實現依賴於作業系統的任務排程器

3.2 join 方法

3.2.1 join方法的基本使用

很多情況下,主執行緒建立並啟動子執行緒,如果子執行緒中要進行大量的耗時運算,主執行緒往往將早於子執行緒結束之前結束。這時,如果主執行緒想等待子執行緒執行完成之後再結束,比如子執行緒處理一個數據,主執行緒要取得這個資料中的值,就要用到join()方法了。方法join()的作用是等待執行緒物件銷燬。

應用如下

public class TestJoin {
    static int num = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "開始進行運算...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "運算結束...");
            num = 10;
        }, "thread-1");

        t1.start();
        System.out.println(Thread.currentThread().getName() + "得到num的值: " + num);
    }
}


輸出

main得到num的值: 0
thread-1開始進行運算...
thread-1運算結束...    

這種情況我們可以通過join() 方法來解決(同步),之後還有非同步的解決方案,等到後面再說。

public class TestJoin {
    static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "開始進行運算...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "運算結束...");
            num = 10;
        }, "thread-1");
        
        t1.start();
        t1.join();
        System.out.println(Thread.currentThread().getName() + "得到num的值: " + num);
    }
}

輸出

thread-1開始進行運算...
thread-1運算結束...
main得到num的值: 10

方法join的作用是使所屬的執行緒物件x正常執行run()方法中的任務,而使當前執行緒z進行無限期的阻塞,等待執行緒x銷燬後再繼續執行執行緒z後面的程式碼。

方法join具有使執行緒排隊執行的作用,有些類似同步的執行效果。join與synchronized的區別是:join在內部使用wait()方法進行等待,而sychronized關鍵字使用的是“物件監視器”原理做為同步。

3.2.2 join() 和 sleep() 的區別

方法join(long)的功能在內部是使用wait(long)方法來實現的,所以join(long)方法具有釋放鎖的特點。

    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;
            }
        }
    }

從原始碼中可以瞭解到,當執行wait(long)方法後,當前執行緒的鎖被釋放,那麼其他執行緒就可以呼叫此執行緒中的同步方法了。

而Thread.sleep(long)方法卻不釋放鎖。

4、停止正在執行的執行緒

在Java中有以下3種方法可以終止正在執行的執行緒:

1)使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止。

2)使用stop方法強行終止執行緒,但是不推薦使用這個方法,因為stop和suspend及resume一樣,都是作廢過期的方法,使用它們可能產生不可預料的結果。

3)使用interrupt方法中斷執行緒。

4.1 通過interrupt()去停止執行緒

通過interrupt()方法來停止執行緒,但interrupt()方法的使用效果並不像for+break語句那樣,馬上就停止迴圈。呼叫interrupt()方法僅僅是在當前執行緒中打了一個停止的標記,並不是真的停止執行緒。

判斷執行緒是否是停止狀態

在Java中,Thread.java類裡提供了兩種方法。

1)interrupted():測試當前執行緒是否已經中斷。會清除打斷標記

2)isInterrupted():測試執行緒是否已經中斷。不會清除打斷標記。

4.1.2 打斷正常執行的執行緒

打斷正常執行的執行緒, 不會清空打斷狀態

public class TestInterrupt1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000000; i++) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread() + " 被打斷...");
                    break;
                }
                System.out.println("i=" + i);
            }
        }, "thead-1");

        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
    }
}

上面的示例雖然停止了執行緒,但如果for語句下面還有語句,還是會繼續執行的。

我們可以通過丟擲異常解決上面的問題:

public class TestInterrupt2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                for (int i = 0; i < 5000000; i++) {
                    Thread currentThread = Thread.currentThread();
                    if (currentThread.isInterrupted()) {
                        System.out.println("已經是停止狀態,我要退出!");

                        throw new InterruptedException();
                    }
                    System.out.println("i=" + i);
                }
                System.out.println("我是for迴圈下面的語句");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "thread-1");

        t1.start();
        TimeUnit.MILLISECONDS.sleep(500);
        t1.interrupt();
        System.out.println("end");
    }
}


  • 我們還可以通過使用 interrupt() 和 return 配合去停止執行緒,但通常建議使用“拋異常”的方法來實現執行緒的停止,因為在catch塊中還可以將異常向上拋,使執行緒停止的事件得以傳播。
4.1.3 打斷 sleep,wait,join 的執行緒

這幾個方法都會讓執行緒進入阻塞狀態

打斷 sleep 的執行緒, 會清空打斷狀態,以 sleep 為例

public class TestInterrupt3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
        System.out.println("打斷狀態:"  + t1.isInterrupted());
    }
}

輸出

打斷狀態:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at org.lovesosa.chapter3.TestInterrupt3.lambda$main$0(TestInterrupt3.java:12)
	at java.lang.Thread.run(Thread.java:748)

解決上面的問題,可以在 catch 塊中重新設定一下打斷標記即可

public class TestInterrupt3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        });

        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
        System.out.println("打斷狀態:"  + t1.isInterrupted());
    }
}

輸出

打斷狀態:true
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at org.lovesosa.chapter3.TestInterrupt3.lambda$main$0(TestInterrupt3.java:12)
	at java.lang.Thread.run(Thread.java:748)

4.2 通過stop() 暴力停止執行緒

使用stop()方法停止執行緒則是非常暴力的。

public class TestStop {

    static int num = 1;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
           while (true) {
               num++;
               System.out.println("num = " + num);
               try {
                   TimeUnit.MILLISECONDS.sleep(500);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

        t1.start();
        TimeUnit.MILLISECONDS.sleep(300);
        t1.stop();

    }
}


方法stop()已經被作廢,因為如果強制讓執行緒停止則有可能使一些清理性的工作得不到完成。另外一個情況就是對鎖定的物件進行了“解鎖”,導致資料得不到同步的處理,出現數據不一致的問題。

使用stop()釋放鎖將會給資料造成不一致性的結果。如果出現這樣的情況,程式處理的資料就有可能遭到破壞,最終導致程式執行的流程錯誤,一定要特別注意。

4.3 兩階段終止模式(Two Phase Termination)

在一個執行緒 T1 中如何“優雅”終止執行緒 T2?這裡的【優雅】指的是給 T2 一個料理後事的機會。

public class TPTInterrput {

    public static void main(String[] args) throws InterruptedException {
        TPTInterrput t1 = new TPTInterrput();
        t1.start();
        Thread.sleep(2000);
        t1.stop();
    }

    private Thread thread;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread currentThread = Thread.currentThread();
                if (currentThread.isInterrupted()) {
                    System.out.println(currentThread + "結束監控任務,開始料理後事");
                    return;
                }

                try {
                    Thread.sleep(100);
                    System.out.println("將結果儲存");
                } catch (InterruptedException e) {
                    currentThread.interrupt();
                }
                System.out.println(currentThread + "正在執行監控操作...");
            }
        }, "監控執行緒");
        thread.start();
    }

    public void stop() {
        thread.interrupt();
    }
}


也可以利用停止標記,實現大體類似。