java併發-線程生命週期

爲是麼需要線程

併發的發展歷史

  • 真空管和穿孔打卡
    最早的計算機只能解決簡單的數學運算問題,比如正弦、餘弦等。運行方式:程序員首先把程序寫到紙上,然後穿孔成卡片,再把卡片盒帶入到專門的輸入室。輸入室會有專門的操作員將卡片的程序輸入到計算機上。計算機運行完當前的任務以後,把計算結果從打印機上進行輸出,操作員再把打印出來的結果送入到輸出室,程序員就可以從輸出室取到結果。然後,操作員再繼續從已經送入到輸入室的卡片盒中讀入另一個任務重複上述的步驟。

    操作員在機房裏面來回調度資源,以及計算機同一個時刻只能運行一個程序,在程序輸入的過程中,計算機計算機和處理空閒狀態 。而當時的計算機是非常昂貴的,人們爲了減少這種資源的浪費。就採用了 批處理系統來解決

  • 晶體管和批處理系統
    批處理操作系統的運行方式:在輸入室收集全部的作業,然後用一臺比較便宜的計算機把它們讀取到磁帶上。然後把磁帶輸入到計算機,計算機通過讀取磁帶的指令來進行運算,最後把結果輸出磁帶上。批處理操作系統的好處在於,計算機會一直處於運算狀態,合理的利用了計算機資源。

    批處理操作系統雖然能夠解決計算機的空閒問題,但是當某一個作業因爲等待磁盤或者其他 I/O 操作而暫停時,那CPU 就只能阻塞直到該 I/O 完成,對於 CPU 操作密集型的程序,I/O 操作相對較少,因此浪費的時間也很少。但是對於 I/O 操作較多的場景來說,CPU 的資源是屬於嚴重浪費的。

  • 集成電路和多道程序設計
    多道程序設計的出現解決了這個問題,就是把內存分爲幾個部分,每一個部分放不同的程序。當一個程序需要等待I/O 操作完成時。那麼 CPU 可以切換執行內存中的另外一個程序。如果內存中可以同時存放足夠多的程序,那 CPU的利用率可以接近 100%。
    在這個時候,引入了第一個概念- 進程, 進程的本質是一個正在執行的程序,程序運行時系統會創建一個進程,並且給每個進程分配獨立的內存地址空間保證每個進程地址不會相互干擾。同時,在 CPU 對進程做時間片的切換時,保證進程切換過程中仍然要從進程切換之前運行的位置出開始執行。所以進程通常還會包括程序計數器、堆棧指針。

    有了進程以後,可以讓操作系統從宏觀層面實現多應用併發。而併發的實現是通過 CPU 時間片不端切換執行的。對於單核 CPU 來說,在任意一個時刻只會有一個進程在被CPU 調度

線程

  1. 在多核 CPU 中,利用多線程可以實現真正意義上的並行
    執行
  2. 在一個應用進程中,會存在多個同時執行的任務,如果其中一個任務被阻塞,將會引起不依賴該任務的任務也被阻塞。通過對不同任務創建不同的線程去處理,可以提升程序處理的實時性
  3. 線程可以認爲是輕量級的進程,所以線程的創建、銷燬比進程更快

在Java中,當我們啓動main函數時其實就啓動了一個JVM的進程,而main函數所在的線程就是這個進程中的一個線程,也稱主線程。

一個進程中有多個線程,多個線程共享進程的堆和方法區資源,但是每個線程有自己的程序計數器和棧區域。

每個線程都有自己的棧資源,用於存儲該線程的局部變量,這些局部變量是該線程私有的,其他線程是訪問不了的,除此之外棧還用來存放線程的調用棧幀。

堆是一個進程中最大的一塊內存,堆是被進程中的所有線程共享的,是進程創建時分配的,堆裏面主要存放使用new操作創建的對象實例。

方法區則用來存放JVM加載的類、常量及靜態變量等信息,也是線程共享的

線程的創建

繼承Thread類創建線程

public class ThreadTest {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("我是是子線程");
        }
    }
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

當創建完thread對象後該線程並沒有被啓動執行,直到調用了start方法後才真正啓動了線程。其實調用start方法後線程並沒有馬上執行而是處於就緒狀態,這個就緒狀態是指該線程已經獲取了除CPU資源外的其他資源,等待獲取CPU資源後纔會真正處於運行狀態。一旦run方法執行完畢,該線程就處於終止狀態。
使用繼承方式的好處是,在run()方法內獲取當前線程直接使用this就可以了,無須使用Thread.currentThread()方法;不好的地方是Java不支持多繼承,如果繼承了Thread類,那麼就不能再繼承其他類。另外任務與代碼沒有分離,當多個線程執行一樣的任務時需要多份任務代碼,而Runable則沒有這個限制。

實現 Runnable 接口創建線程

public class MyThread extends OtherClass implements Runnable {
	public void run {
		System.out.println("MyThread run ");
	}
}

實現 Callable 接口通過 FutureTask 包裝器來創建 Thread 線程

public class CallableTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "hello";
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1.創建異步任務
        FutureTask<String> futureTask = new FutureTask<>(new CallableTask());
        // 啓動線程
        new Thread(futureTask).start();
        // 阻塞等待任務執行完畢,並返回結果
        String result = futureTask.get();
        System.out.println(result);
        // 2.線程池方式
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<String> future = executorService.submit(new CallableTask());
        System.out.println(future.get());
        executorService.shutdown();
    }
}

線程生命週期

在這裏插入圖片描述
java.lang.Thread.State

public enum State {
   NEW,
   RUNNABLE,
   BLOCKED,
   WAITING,
   TIMED_WAITING,
   TERMINATED;
}

NEW:初始狀態,線程被構建,但是還沒有調用 start 方法
RUNNABLED:運行狀態,JAVA 線程把操作系統中的就緒和運行兩種狀態統一稱爲“運行中”
BLOCKED:阻塞狀態,表示線程進入等待狀態,也就是線程

因爲某種原因放棄了 CPU 使用權,阻塞也分爲幾種情況
➢ 等待阻塞:運行的線程執行 wait 方法,jvm 會把當前線程放入到等待隊列
➢ 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被其他線程鎖佔用了,那麼 jvm 會把當前的線程放入到鎖池中
➢ 其他阻塞:運行的線程執行 Thread.sleep 或者t.join方法,或者發出了 I/O 請求時,JVM 會把當前線程設置爲阻塞狀態,當 sleep 結束、join 線程終止、io 處理完畢則線程恢復

TIME_WAITING:超時等待狀態,超時以後自動返回
TERMINATED:終止狀態,表示當前線程執行完畢

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