Java 執行緒建立與常用方法

2022-06-04 15:00:53

程序與執行緒

程序

  • 程式由指令和資料組成,但這些指令要執行,資料要讀寫,就必須將指令載入至 CPU,資料載入至記憶體。在指令執行過程中還需要用到磁碟、網路等裝置。程序就是用來載入指令、管理記憶體、管理 IO 的
  • 當一個程式被執行,從磁碟載入這個程式的程式碼至記憶體,這時就開啟了一個程序

執行緒

  • 一個程序之內可以分為一到多個執行緒。
  • 一個執行緒就是一個指令流,將指令流中的一條條指令以一定的順序交給 CPU 執行
  • Java 中,執行緒作為最小排程單位,程序作為資源分配的最小單位。 在 windows 中程序是不活動的,只是作為執行緒的容器

程序與執行緒的區別

  • 程序基本上相互獨立的,而執行緒存在於程序內,是程序的一個子集
  • 程序擁有共用的資源,如記憶體空間等,供其內部的執行緒共用
  • 程序間通訊較為複雜
    • 同一臺計算機的程序通訊稱為 IPC(Inter-process communication)
    • 不同計算機之間的程序通訊,需要通過網路,並遵守共同的協定,例如 HTTP
  • 執行緒通訊相對簡單,因為它們共用程序內的記憶體,一個例子是多個執行緒可以存取同一個共用變數
  • 執行緒更輕量,執行緒上下文切換成本一般上要比程序上下文切換低

並行與並行

單核 cpu 下,執行緒實際還是 序列執行 的。作業系統中有一個元件叫做任務排程器,將 cpu 的時間片(windows下時間片最小約為 15 毫秒)分給不同的程式使用,只是由於 cpu 線上程間(時間片很短)的切換非常快,人類感覺是 同時執行的 。總結為一句話就是: 微觀序列,宏觀並行 。一般會將這種 執行緒輪流使用 CPU 的做法稱為並行 (concurrent)

多核 cpu下,每個 核(core) 都可以排程執行執行緒,這時候執行緒可以是並行的。

Java 執行緒

建立和執行執行緒

  • 直接使用 Thread

    package create;
    
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "c.ThreadCre")
    public class ThreadCre {
        public static void main(String[] args) {
    
            Thread t = new Thread(){
                @Override
                public void run() {
                    log.debug("running");
                }
            };
    
            t.start();
    
            log.debug("running");
    
        }
    }
    
  • 使用 Runnable 配合 Thread

    package create;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "c.RunnableCre")
    public class RunnableCre {
        public static void main(String[] args) {
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    log.debug("running");
                }
            };
    
            Thread t = new Thread(r,"t2");
    
            t.start();
        }
    }
    

    使用 lambda 方式簡化

    package create;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "c.RunnableCre")
    public class RunnableCre {
        public static void main(String[] args) {
            Runnable r = () -> { log.debug("running"); };
    
            Thread t = new Thread(r,"t2");
    
            t.start();
        }
    }
    
  • FutureTask 配合 Thread

    package create;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    @Slf4j(topic = "c.FutureTaskCre")
    public class FutureTaskCre {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    log.debug("running...");
                    Thread.sleep(1000);
                    return 100;
                }
            });
    
            Thread t = new Thread(task,"t1");
            t.start();
    
            log.debug("{}",task.get());
        }
    }
    

Thread 與 Runnable 的關係

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

執行緒執行的原理

棧與棧幀

每個執行緒啟動後,虛擬機器器就會為其分配一塊棧記憶體。

  • 每個棧由多個棧幀(Frame)組成,對應著每次方法呼叫時所佔用的記憶體
  • 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法

執行緒上下文切換

因為以下一些原因導致 cpu 不再執行當前的執行緒,轉而執行另一個執行緒的程式碼

  • 執行緒的 cpu 時間片用完
  • 垃圾回收
  • 有更高優先順序的執行緒需要執行
  • 執行緒自己呼叫了 sleep、yield、wait、join、park、synchronized、lock 等方法

當 Context Switch 發生時,需要由作業系統儲存當前執行緒的狀態,並恢復另一個執行緒的狀態,Java 中對應的概念就是程式計數器(Program Counter Register),它的作用是記住下一條 jvm 指令的執行地址,是執行緒私有的

  • 狀態包括程式計數器、虛擬機器器棧中每個棧幀的資訊,如區域性變數、運算元棧、返回地址等
  • Context Switch 頻繁發生會影響效能

常見方法

方法名 static 功能說明 注意
start() 啟動一個新執行緒,在新的執行緒執行 run 方法中的程式碼 start 方法只是讓執行緒進入就緒,裡面的程式碼不一定立刻執行(CPU的時間片還沒有分給它)。每個執行緒物件的 start 方法只能呼叫一次,否則會出現異常
run() 新執行緒啟動後會呼叫的方法 如果在構造 Thread 物件時傳遞了 Runnable 引數,則執行緒啟動後會呼叫 Runnable 中的 run 方法。但可以建立 Thread 的子類物件來覆蓋預設行為
join() 等待執行緒執行結束
join(long n) 等待執行緒執行結果,最多等待 n 毫秒
getId() 獲取執行緒長整型的 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的使用 主要是為了測試和偵錯

start 與 run

呼叫 run

public static void main(String[] args) {
 	Thread t1 = new Thread("t1") {
 		@Override
 		public void run() {
 			log.debug(Thread.currentThread().getName());
 			FileReader.read(Constants.MP4_FULL_PATH);
 		}
 	};
    
 	t1.run();
 	log.debug("do other things ...");
}

輸出

19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...

程式仍在 main 執行緒執行, FileReader.read() 方法呼叫還是同步的

總結

  • 直接呼叫 run 是在主執行緒中執行了 run,沒有啟動新的執行緒
  • 使用 start 是啟動新的執行緒,通過新的執行緒間接執行 run 中的程式碼

sleep 與 yield

sleep

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

yield

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

執行緒優先順序

  • 執行緒優先順序會提示(hint)排程器優先排程該執行緒,但它僅僅是一個提示,排程器可以忽略它
  • 如果 cpu 比較忙,那麼優先順序高的執行緒會獲得更多的時間片,但 cpu 閒時,優先順序幾乎沒作用

join

等待一個執行緒執行結束

等待多個執行緒的結果

情況一:

package testJoin;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.demo1")
public class demo1 {

    static int r = 0 , r1 = 0 , r2 = 0;

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

    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
                r1 = 10;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
                r1 = 20;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        log.debug("join begin");
        t1.join();
        log.debug("t1 join end");
        t2.join();
        log.debug("t2 join end");
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}",r1,r2,end-start);
    }
}

輸出:

14:18:02 [main] c.demo1 - join begin
14:18:03 [main] c.demo1 - t1 join end
14:18:04 [main] c.demo1 - t2 join end
14:18:04 [main] c.demo1 - r1: 20 r2: 0 cost: 2008

情況二:

package testJoin;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.demo1")
public class demo1 {

    static int r = 0 , r1 = 0 , r2 = 0;

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

    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
                r1 = 10;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
                r1 = 20;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        log.debug("join begin");
        t2.join();
        log.debug("t2 join end");
        t1.join();
        log.debug("t1 join end");
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}",r1,r2,end-start);
    }
}

輸出:

14:19:19 [main] c.demo1 - join begin
14:19:21 [main] c.demo1 - t2 join end
14:19:21 [main] c.demo1 - t1 join end
14:19:21 [main] c.demo1 - r1: 20 r2: 0 cost: 2006

另外 join 也可以帶引數,是有時效的等待。當到設定時間執行緒還未給出結果,直接向下執行,不再等待。如果設定時間還沒到但是執行緒已經執行完畢,則直接向下執行,不再等待。

interrupt

打斷 sleep,wait,join 的執行緒

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

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

package testInterrupt;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.demo1")
public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000);
                //注意:sleep,wait,join等被打斷並以異常形式表現出來後
                // 會把打斷標記重新置為 false(未打斷狀態)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打斷標記:{}",t1.isInterrupted());
    }
}

輸出:

15:08:12 [t1] c.demo1 - sleep...
15:08:13 [main] c.demo1 - interrupt
15:08:13 [main] c.demo1 - 打斷標記:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at testInterrupt.demo1.lambda$main$0(demo1.java:11)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

打斷正常執行的執行緒打斷標記置為:true

package testInterrupt;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.demo2")
public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true){
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted){
                    log.debug("被打斷了,退出迴圈");
                    break;
                }
            }
        },"t1");
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
    }
}

輸出:

15:17:40 [main] c.demo2 - interrupt
15:17:40 [t1] c.demo2 - 被打斷了,退出迴圈

打斷 park 執行緒

package testInterrupt;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

@Slf4j(topic = "c.demo4")
public class demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打斷狀態:{}",Thread.currentThread().isInterrupted());
        },"t1");

        t1.start();

        Thread.sleep(1000);
        t1.interrupt();
    }
}

輸出:

14:16:21 [t1] c.demo4 - park...
14:16:22 [t1] c.demo4 - unpark...
14:16:22 [t1] c.demo4 - 打斷狀態:true

兩階段終止模式

package testInterrupt;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.demo3")
public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination{
    private Thread monitor;

    //啟動監控執行緒
    public void start(){
        monitor = new Thread(() -> {
            while (true){
                Thread current = Thread.currentThread();
                if(current.isInterrupted()){
                    log.debug("料理後事");
                    break;
                }
                try {
                    Thread.sleep(1000);//情況1
                    log.debug("執行監控記錄");//情況2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //重新設定打斷標記
                    current.interrupt();
                }
            }
        });

        monitor.start();
    }

    //終止監控執行緒
    public void stop(){

        monitor.interrupt();
    }
}

輸出:

15:33:02 [Thread-0] c.TwoPhaseTermination - 執行監控記錄
15:33:03 [Thread-0] c.TwoPhaseTermination - 執行監控記錄
15:33:04 [Thread-0] c.TwoPhaseTermination - 執行監控記錄
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at testInterrupt.TwoPhaseTermination.lambda$start$0(demo3.java:29)
	at java.lang.Thread.run(Thread.java:748)
15:33:04 [Thread-0] c.TwoPhaseTermination - 料理後事

Process finished with exit code 0

不推薦的方法

還有一些不推薦使用的方法,這些方法已過時,容易破壞同步程式碼塊,造成執行緒死鎖

方法名 static 功能說明
stop() 停止執行緒執行
suspend() 掛起(暫停)執行緒執行
resume() 恢復執行緒執行