1. 程式人生 > >從零開始學習Java多線程(一)

從零開始學習Java多線程(一)

rup 總結 pan 重新 name屬性 計劃任務 src syn 線程停止

1. 什麽是進程?

對其概念需要自行goole,簡單理解就是:進程是計算機系統進行資源分配和調度的基本單位,是正在運行程序的實體;每一個進程都有它自己的內存空間和系統資源;進程是線程的容器。如:打開IDEA寫代碼是一個進程,打開有道詞典也是一個獨立的進程。

如果我們在用IDEA寫代碼的同時打開有道詞典那就是多進程,多進程具有獨立性,動態性,並發性,異步性。鑒於多數人混淆並行和並發,在此簡單介紹:

  • 並發:多個CPU實例同時執行一段代碼或處理邏輯,具有物理意義上的同時發生。
  • 並行:計算機通過調度算法爭奪CPU時間片繼而執行屬於自己的執行計劃,CPU的高效切換在轉瞬間完成,讓用戶感覺像是同時發生,實際上只是邏輯上的同時發生。

那麽QQ和網易雲是同時進行的嗎?取決於CPU的個數,單個CPU在某個時間點上只能做一件事情,而多核(多個CPU)可以做到同時進行。多進程的意義在於,提高了CPU使用率。值得一提的是,Java是不能夠通過調用系統資源來開啟一個進程的,例如在windows系統中,Java通過調用C語言底層代碼來開啟進程。

2. 什麽是線程?

線程:是進程中的單個順序控制流,計算機最小的執行單元,一條執行路徑。一個進程如果只有一條執行路徑,成為單線程程序;如果有多條執行路徑,則成為多線程程序;多線程共享該進程的全部資源。如:打開QQ後,好友聊天屬於一條線程,瀏覽QQ空間又屬於一條線程。

假如我們的計算機只有一個CPU,那麽CPU在某一個時刻只能執行一條指令,線程只有得到CPU時間片才能擁有使用權,才可以執行指令,那麽Java是如何對線程進行調用的呢?

線程調用的兩種模型:

  1. 分時調度模型 : 所有的線程輪流獲得CPU的使用權,平均分配每個線程占用CPU的時間片
  2. 搶占式調度模型:優先讓優先級高的線程使用CPU,如果優先級相同,那麽會從中隨機選取一個,優先級高的線程獲取的CPU時間片相對多一些。
  3. Java使用的是搶占式調度模型。
  4. 可利用API設置和獲取線程優先級。

    public final int getPriority()

    public final void setPriority(int newPriority)

現在大致了解進程和線程之間的關系後,再來看Java程序運行原理。

Java命令會啟動Java虛擬機,啟動JVM,等於啟動了一個進程。該進程會自動啟動一個"主線程",然後主線程去調用某個類的main方法,所有main方法運行在主線程中,在此之前的所有程序都是單線程的。Java虛擬機的啟動是多線程的,因為JVM啟動至少啟動了垃圾回收線程和主線程。

3. 多線程的意義

進程具有獨立性,多進程之間是沒有共享資源的,但是多線程可以共享內存資源,而且十分簡單。系統創建進程是需要為該進程重新分配系統資源,浪費了大量資源,但創建線程的代價要小很多,因此多線程實現多任務的並發要比多進程的效率高。

總結起來:

  1. 共享內存資源
  2. 並發效率高
  3. 多線程的作用不是提高執行速度,而是提高應用程序的使用率

而多線程的實際應用包括:

    • 瀏覽器必須能同時下載多個圖片
    • 一臺服務器必須n能同時響應多個用戶請求
    • JVM本身就在後臺提高了一個超級線程進行垃圾回收 

4. Java多線程實現

一)繼承Thread類,復寫run()方法

 1 /**
 2  * @author supiaol
 3  * @date 2019/3/7
 4  * @time 9:26
 5  */
 6 public class MyThread extends Thread {
 7 
 8     //多線程運行的代碼塊
 9     public void run() {
10         System.out.println("Thread is running");
11     }
12 
13     public static void main(String[] args) {
14 
15         MyThread myThread1 = new MyThread();
16         MyThread myThread2 = new MyThread();
17 
18         //運行多線程
19         myThread1.start();
20         myThread2.start();
21 
22     }
23 }

Thread類本質上是實現Runable接口的一個實例。Thread 類中有一些關鍵屬性,如:name屬性代表線程的名稱,可以通過Thread類的構造器中參數來指定線程名稱;priority屬性代表線程優先級,上文提高優先級高的線程搶占CPU時間可能性越大,默認優先級為5,最小值為1,最大值為10;daemon屬性表示線程是否是守護線程,target屬性代表要執行的任務。

下面是Thread類中常用的api:

1. run()方法 新建線程(新建狀態)

需要明確的是run()方法不是用來運行線程的,也不需要用戶調用,當線程獲得CPU執行時間,會進入run()方法執行代碼塊。

2. start()方法 啟動線程(就緒狀態)

線程啟動的方法,調用start()方法後,系統會開啟一個新的線程用來執行用戶定義的任務,在此過程中,為線程分配系統資源。需要註意的是,調用start()方法後,並不會立即執行定義的任務,而是賦 予線程可以搶占CPU時間片的資格,只有得到CPU時間片才能執行計劃任務。

3. sleep()方法 睡眠線程(堵塞狀態)

線程睡眠,必須指定睡眠時間,在適當的位置調用sleep(),讓該線程睡眠,也就是交出CPU,讓CPU來執行其它任務。特別需要關註的是,sleep()方法不會釋放鎖或者監視器,也就是說如果當前線程持有某個對象的鎖,那麽即使調用sleep()方法,其他線程也無法訪問該對象,關於該方法和鎖的關系會在後續詳細說明和演示。

4.yield()方法 禮讓線程(堵塞狀態)

調用yield()方法同樣可以讓該線程交出CPU時間片,失去執行權,類似於sleep()方法,同樣不會釋放鎖對象或者監視器,而區別之處在於,yield()不能控制具體交出CPU的時間,而且交出的CPU時間片只能允許相同優先級的線程獲取。

5.join()方法 線程加入(堵塞狀態)

join方法有三個重載版本:

join()
join(long millis)     //參數為毫秒
join(long millis,int nanoseconds)    //第一參數為毫秒,第二個參數為納秒

它們的區別在於指定的參數,假如我們在main()所屬的主線程中調用另外一個從線程thread.join()方法,則main()方法失去執行權,只有等到thread線程執行完畢或者等待一定的時間後重新獲得執行權。如何調用無參join()方法,需要等待thread線程執行完畢,調用指定時間的帶參join()方法,則等到指定時間過後獲取執行權。

通過查看源碼發現,join實際上調用了wait()方法實現主線程等待,至於wait()方法,後面學習線程安全時候著重講述,在此先做了解。

 1 public final synchronized void join(long millis)
 2     throws InterruptedException {
 3         long base = System.currentTimeMillis();
 4         long now = 0;
 5 
 6         if (millis < 0) {
 7             throw new IllegalArgumentException("timeout value is negative");
 8         }
 9         //空參join 需要等待從線程執行完畢
10         if (millis == 0) {
11             while (isAlive()) {
12                 wait(0);
13             }
14         } else {
15             //帶參join,等待指定時間後重新獲得執行權
16             while (isAlive()) {
17                 long delay = millis - now;
18                 if (delay <= 0) {
19                     break;
20                 }
21                 wait(delay);
22                 now = System.currentTimeMillis() - base;
23             }
24         }
25     }

6. interrupt() 線程中斷(堵塞狀態)

顧名思義,interrupt即中斷的意思。調用interrupt()方法能夠使處於堵塞狀態的線程拋出異常,其實質上就是用來中斷處於堵塞狀態的線程,通常配合isInterrupted()方法來停止正常運行的線程。

7. stop()方法 線程停止(線程中斷)

stop方法是一個已經被廢棄的方法,自身不安全。因為調用stop方法會直接終止run方法的調用,並且拋出ThreadDeath異常,如果該線程調用stop方法之前持有某個對象鎖,之後會完全釋放鎖對象,導致對象狀態不一致。

8.destory() 方法 已被廢棄,不會用到。

(二) 實現Runnable接口,重寫run()方法。

 1 /**
 2  * @author supiaol
 3  * @date 2019/3/7
 4  * @time 14:49
 5  */
 6 public class MyThread extends OtherClass implements Runnable {
 7     @Override
 8     public void run() {
 9         System.out.println("Runnable realize multithreading");
10     }
11 
12     public static void main(String[] args) {
13         MyThread myThread1 = new MyThread();
14         MyThread myThread2 = new MyThread();
15 
16         new Thread(myThread1).start();
17         new Thread(myThread2).start();
18 
19     }
20 }

實現Runnable接口實現多現成的好處就在於彌補Java單繼承的缺陷。更適合多個相同程序的代碼去處理一個資源的情況,這樣線程同程序的代碼,數據有效分離,較好的體現了面向對象的設計思想。區別於繼承Thread類啟動線程,實現Runnable接口啟動線程時,需要將實現Runnable接口的實例作為target目標傳入Thread實例,然後調用start()方法啟動線程。

如果需要對線程設置名稱,可以通過線程對象調用setName方法進行設置,也可以通過Thread的構造方法設置,而getName()方法可以獲取線程名稱,也可以通過Thread.currentThread().getName()方法獲取當前線程的名稱。

(三) 基於線程池實現多線程,用到不多,在此不多介紹

5. 線程的生命周期

技術分享圖片

  1. 新建:創建線程對象,從new一個線程對象到調用start()方法之間都是新建狀態
  2. 就緒:調用start()方法後,線程對象已經啟動,但是還沒有獲取到CPU的執行權
  3. 運行:獲取到CPU時間片,開始執行run()方法中的代碼
  4. 堵塞:失去執行權,回到就緒狀態。
  5. 結束:代碼運行完畢,或者main方法執行完畢,線程消亡

以上就是一個線程完整的生命周期,一個線程最基本的生命周期包括:新建,就緒,運行,結束。

從零開始學習Java多線程(一)