1. 程式人生 > 其它 >Java 多執行緒學習-多執行緒基礎

Java 多執行緒學習-多執行緒基礎

目錄

Java 多執行緒學習-多執行緒基礎

執行緒執行的一些概念

棧和棧幀

棧幀空間是執行緒獨享的,堆和方法區的記憶體空間是執行緒共享的

執行緒上下文切換

CPU通過時間片分配演算法來迴圈執行任務,當前任務執行一個時間片後會切換到下一個 任務。但是,在切換前會儲存上一個任務的狀態,以便下次切換回這個任務時,可以再載入這 個任務的狀態。所以任務從儲存到再載入的過程就是一次上下文切換。

JVM 使用程式計數器來記住下一條指令的執行地址,是執行緒私有的

執行緒上下文頻繁切換會影響效能,所以不是執行緒越多越好。這也是為什麼在cpu核心數量只有一個的系統上,併發的效果甚至不如序列。


執行緒優先順序

現代作業系統基本採用時分的形式排程執行的執行緒,作業系統會分出一個個時間片,執行緒會分配到若干時間片,當執行緒的時間片用完了就會發生執行緒排程,並等待著下次分配。執行緒分配到的時間片多少也就決定了執行緒使用處理器資源的多少,而執行緒優先順序就是決定執行緒需 要多或者少分配一些處理器資源的執行緒屬性。

Java 中 Thread 物件有一個整形變數 priority 來控制優先順序,可以通過 setPriority() 來設定這個值, 但是 Java 中執行緒優先順序的設定效果不明顯,作業系統可能會忽略這個執行緒優先順序的設定.


執行緒狀態

作業系統執行緒狀態(五種)

狀態名稱 說明
初始狀態 僅在語言層面建立了執行緒物件,還未與作業系統執行緒關聯
可執行狀態 指該執行緒已經被建立(與作業系統執行緒關聯),可以由CPU排程執行
執行狀態 指獲取了CPU時間片執行中的狀態
阻塞狀態 如果呼叫了阻塞API,這時執行緒實際不會用到CPU,會導致上下文切換,進入阻塞狀態。等操作完畢,由作業系統喚醒阻塞的執行緒,轉換至可執行狀態
終止狀態 表示執行緒已經執行完畢,生命週期已經結束,不會轉化為其他狀態

(圖片源自網路)

Java執行緒狀態(六種)

根據 Thread.State 列舉,分為六種狀態

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

Java執行緒狀態變遷

(圖片來自《Java併發程式設計的藝術》)


建立執行緒

方法一:直接使用 Thread ,覆蓋 run 方法

public void method1() {
    Thread t = new Thread() {
        @Override
        public void run() {
            //使用了Slf4j
            log.debug("running");
        }
    };
    t.start();
}

方法二:實現 Runnable介面

public void method2() {
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            log.debug("running");
        }
    };
    
    // jdk8 之後可以用lambda 表示式簡化上面的程式碼
    Runnable runnable1 = () -> {
        log.debug("running");  
    };
    
    Thread t = new Thread(runnable,"t");
    t.start();
}

方法一和方法二類似,但推薦使用方法二建立執行緒

  1. 用 Runnable 更容易與執行緒池等高階 API 配合

  2. 用 Runnable 讓任務類脫離了 Thread 繼承體系,更靈活,Java 中使用組合一般要優於使用繼承

    “有些時候,通過‘合成’技術用現有的類來構造新類。而繼承是最少見的一種做法。”

    “應提醒自己防止繼承的濫用。”(《Java程式設計思想》)

方法三:實現 Callable 介面

public static void method3() throws ExecutionException, InterruptedException {
    //FutureTask 能夠接收 Callable 型別的引數,用來處理有返回結果的情況
    FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {	//call 方法可以返回結果和丟擲異常
            log.debug("running");
            return 100;
        }
    });
    Thread t = new Thread(task, "t");
    t.start();
    //獲取返回結果
    task.get();
}

Java 中執行緒常見方法

start() 和 run()

  1. start() 是開啟一個執行緒,run() 是這個執行緒要執行的方法
  2. 能不能直接呼叫 run() 方法呢? 可以,但是執行這個 run() 方法的執行緒就是呼叫該方法的執行緒,而不是新建立一個執行緒去執行。
public  void testStartRun() {
    Thread t = new Thread(){
        @Override
        public void run() {
            try {
                sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    /*
    t.run();  //5秒後列印hello
    */  
    t.start(); //立刻列印hello
    System.out.println("hello");
}

sleep() 和 yield()

sleep

  1. sleep 會讓當前執行緒從 Running 進入 Timed Waiting 狀態 (阻塞)
  2. 其他執行緒可以使用 interrupt() 打斷正在睡眠的執行緒,sleep() 會丟擲 InterruptedException
  3. 睡眠結束後的執行緒未必會立刻得到執行

yield

  1. 呼叫 yield 會讓當前執行緒從 Runing 進入 Runnable 狀態,然後排程執行其他同優先順序的執行緒。如果這時沒有同優先順序的執行緒,那麼不能保證讓當前執行緒暫停。
  2. 具體實現依賴於作業系統的任務排程器。

區別

  1. sleep 後執行緒處於 Timed Waiting 狀態,cpu 不會分配時間片給這個執行緒
  2. yield 後執行緒處於Runnable 狀態,cpu可能分配時間片給它

應用

降低 cpu 佔用率,比如在while (true) 迴圈中可以呼叫 sleep 或 yield 來讓出cpu

join()

插入執行緒中,被插入的執行緒進入阻塞狀態需要等待插入執行緒執行結束。

可以指定一個 long 值,規定等待的時間

下面是一個使用 join 來實現同步的例子

同步與非同步

  • 需要等待結果返回,才能繼續執行就是同步
  • 不需要等待結果返回,就能繼續執行就是非同步
//方法外宣告一個變數
int r = 0;
public void testJoin() throws InterruptedException {
    Thread t = new Thread(){
        @Override
        public void run() {
            try {
                sleep(2000);
                r = 10;	//子執行緒修改 r 變數
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    //開啟子執行緒
    t.start();
    //如果去掉下面這行,列印結果為 r = 0
    t.join(); //主執行緒需要等待t執行緒結束才能繼續執行
    log.debug("r={}",r);	// r = 10
}

interrupt()

  • 如果該執行緒阻塞的呼叫wait(),join(),sleep(),那麼它的中斷狀態將被清除,並且將收到一個InterruptedException 。 否則,會將中斷狀態設定為 true
  • 呼叫isInterrupted()不會清除打斷標記,呼叫interrupted() 會清除打斷標記
  • 中斷執行緒時用到兩階段終止的設計模式
public void testInterrupt() throws InterruptedException {
    Thread t = new Thread() {
        @Override
        public void run() {
            while (true) {
                //用 isInterrupted() 方法測試這個執行緒是否被中斷
                boolean interrupted = isInterrupted();
                if (interrupted) {
                    log.debug("被打斷,退出迴圈");
                    break;
                }
            }
        }
    };
    t.start();
    //執行中斷
    t.interrupt();
    log.debug("打斷標記:{}",t.isInterrupted());
}
/**輸出結果
13:36:44.373 [Thread-0] DEBUG c.MethodTest - 被打斷,退出迴圈
13:36:44.373 [main] DEBUG c.MethodTest - 打斷標記:true
**/

守護執行緒 setDaemon()

  • 只要其他非守護執行緒執行結束了,則守護執行緒會強制結束

  • 垃圾回收器就是一種守護執行緒


檢視執行緒指令

JDK 提供了專門指令

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