1. 程式人生 > >Java基礎知識回顧之五 ----- 多執行緒

Java基礎知識回顧之五 ----- 多執行緒

前言

上一篇文章中,回顧了Java的集合。而在本篇文章中主要介紹多執行緒的相關知識。主要介紹的知識點為執行緒的介紹、多執行緒的使用、以及在多執行緒中使用的一些方法。

執行緒和程序

執行緒

表示程序中負責程式執行的執行單元,依靠程式進行執行。執行緒是程式中的順序控制流,只能使用分配給程式的資源和環境。

程序

表示資源的分配和排程的一個獨立單元,通常表示為執行中的程式。一個程序至少包含一個執行緒。

程序和執行緒的區別

  1. 程序至少有一個執行緒;它們共享程序的地址空間;而程序有自己獨立的地址空間;
  2. 程序是資源分配和擁有的單位,而同一個程序內的執行緒共享程序的資源;
  3. 執行緒是處理器排程的基本單位,但程序不是;

生命週期

執行緒和程序一樣分為五個階段:建立就緒執行阻塞終止

  • 新建狀態:使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式start() 這個執行緒。
  • 就緒狀態:當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,要等待JVM裡執行緒排程器的排程。
  • 執行狀態:如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
  • 阻塞狀態:如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。可以分為三種:
  • 等待阻塞:執行狀態中的執行緒執行 wait() 方法,使執行緒進入到等待阻塞狀態。
  • 同步阻塞:執行緒在獲取 synchronized 同步鎖失敗(因為同步鎖被其他執行緒佔用)。
  • 其他阻塞:通過呼叫執行緒的 sleep() 或 join() 發出了 I/O 請求時,執行緒就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待執行緒終止或超時,或者 I/O 處理完畢,執行緒重新轉入就緒狀態。
  • 死亡狀態:一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。

可以用下述圖來進行理解執行緒的生命週期:
 

注:上述圖來自http://www.runoob.com/wp-content/uploads/2014/01/java-thread.jpg。

在瞭解了執行緒和程序之後,我們再來簡單的瞭解下單執行緒和多執行緒。
單執行緒
程式中只存在一個執行緒,實際上主方法就是一個主執行緒。

多執行緒
多執行緒是指在同一程式中有多個順序流在執行。 簡單的說就是在一個程式中有多個任務執行。

那麼在什麼情況下用多執行緒呢?

一般來說,程式中有兩個以上的子系統需要併發執行的,這時候就需要利用多執行緒程式設計。通過對多執行緒的使用,可以編寫出高效的程式。

那麼是不是使用很多執行緒就能提高效率呢?

不一定的。因為程式中上下文的切換開銷也很重要,如果建立了太多的執行緒,CPU
花費在上下文的切換的時間將多於執行程式的時間!這時是會降低程式執行效率的。

所以有效利用多執行緒的關鍵是理解程式是併發執行而不是序列執行的。

執行緒的建立

一般來說,我們在對執行緒進行建立的時候,一般是繼承Thread 類或實現Runnable 介面。其實還有一種方式是實現 Callable介面,然後與Future 或執行緒池結合使用, 類似於Runnable介面,但是就功能上來說更為強大一些,也就是被執行之後,可以拿到返回值。

這裡我們分別一個例子使用繼承Thread 類、實現Runnable 介面和實現Callable介面與Future結合來進行建立執行緒。
程式碼示例:
注:執行緒啟動的方法是start而不是run。因為使用start方法整個執行緒處於就緒狀態,等待虛擬機器來進行排程。而使用run,也就是當作了一個普通的方法進行啟動,這樣虛擬機器不會進行執行緒排程,虛擬機器會執行這個方法直到結束後自動退出。

程式碼示例:

public class Test {
    public static void main(String[] args) {
        ThreadTest threadTest=new ThreadTest();
        threadTest.start();

        RunalbeTest runalbeTest=new RunalbeTest();
        Thread thread=new Thread(runalbeTest);
        thread.start();
        
        CallableTest callableTest=new CallableTest();
        FutureTask<Integer> ft = new FutureTask<Integer>(callableTest);  
        Thread thread2=new Thread(ft);
        thread2.start();
        try {
            System.out.println("返回值:"+ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class ThreadTest extends Thread{
     @Override
     public void run() {
        System.out.println("這是一個Thread的執行緒!");
    }
}

class RunalbeTest implements Runnable{
     @Override
     public void run() {
        System.out.println("這是一個Runnable的執行緒!");
    }
}

class CallableTest implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
         System.out.println("這是一個Callable的執行緒!");  
        return 2;
    }
}

執行結果:

    這是一個Thread的執行緒!
    這是一個Runnable的執行緒!
    這是一個Callable的執行緒!
    返回值:2

通過上述示例程式碼中,我們發現使用繼承 Thread 類的方式建立執行緒時,編寫最為簡單。而使用Runnable、Callable 介面的方式建立執行緒的時候,需要通過Thread類的構造方法Thread(Runnable target) 構造出物件,然後呼叫start方法來執行執行緒程式碼。順便說下,其實Thread類實際上也是實現了Runnable介面的一個類。

但是在這裡,我推薦大家建立單執行緒的時候使用繼承 Thread 類方式建立,多線執行緒的時候使用Runnable、Callable 介面的方式來建立建立執行緒。
至於為什麼呢?在下面中的描述已給出理由。

  • 繼承 Thread 類建立的執行緒,可以直接使用Thread類中的方法,比如休眠直接就可以使用sleep方法,而不必在前面加個Thread;獲取當前執行緒Id,只需呼叫getId就行,而不必使用Thread.currentThread().getId() 這麼一長串的程式碼。但是使用Thread 類建立的執行緒,也有其侷限性。比如資源不能共享,無法放入執行緒池中等等。
  • 使用Runnable、Callable 介面的方式建立的執行緒,可以實現資源共享,增強程式碼的複用性,並且可以避免單繼承的侷限性,可以和執行緒池完美結合。但是也有不好的,就是寫起來不太方便,使用其中的方法不夠簡介。

總的來說就是,單執行緒建議用繼承 Thread 類建立,多執行緒建議- 使用Runnable、Callable 介面的方式建立。

執行緒的一些常用方法

yield

使用yield方法表示暫停當前正在執行的執行緒物件,並執行其他執行緒。

程式碼示例:

public class YieldTest {
    public static void main(String[] args) {
        Test1 t1 = new Test1("張三");
        Test1 t2 = new Test1("李四");
        new Thread(t1).start();
        new Thread(t2).start();
    }
}

class Test1 implements Runnable {
    private String name;
    public Test1(String name) {
        this.name=name;
    }
    @Override
    public void run() {
        System.out.println(this.name + " 執行緒執行開始!");  
        for (int i = 1; i <= 5; i++) {
            System.out.println(""+this.name + "-----" + i);  
            // 當為3的時候,讓出資源
            if (i == 3) {
                Thread.yield();
            }
        }
        System.out.println(this.name + " 執行緒執行結束!");  
    }
}

執行結果一:

    張三 執行緒執行開始!
    張三-----1
    張三-----2
    張三-----3
    李四 執行緒執行開始!
    李四-----1
    李四-----2
    李四-----3
    張三-----4
    張三-----5
    張三 執行緒執行結束!
    李四-----4
    李四-----5
    李四 執行緒執行結束!

執行結果二:

張三 執行緒執行開始!
李四 執行緒執行開始!
李四-----1
李四-----2
李四-----3
張三-----1
張三-----2
張三-----3
李四-----4
李四-----5
李四 執行緒執行結束!
張三-----4
張三-----5
張三 執行緒執行結束!

上述中的例子我們可以看到,啟動兩個執行緒之後,哪個執行緒先執行到3,就會讓出資源,讓另一個執行緒執行。
在這裡順便說下,yieldsleep的區別。

  • yield: yield只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。
  • sleep:sleep使當前執行緒進入停滯狀態,所以執行sleep()的執行緒在指定的時間內肯定不會被執行;

join

使用join方法指等待某個執行緒終止。也就是說當子執行緒呼叫了join方法之後,後面的程式碼只有等待該執行緒執行完畢之後才會執行。

如果不好理解,這裡依舊使用一段程式碼來進行說明。
這裡我們建立兩個執行緒,並使用main方法執行。順便提一下,其實main方法也是個執行緒。如果直接執行的話,可能main方法執行完畢了,子執行緒還沒執行完畢,這裡我們就讓子執行緒使用join方法使main方法最後執行。

程式碼示例:

public class JoinTest {
    public static void main(String[] args) {
         System.out.println(Thread.currentThread().getName()+ "主執行緒開始執行!");  
         Test2 t1=new Test2("A");  
         Test2 t2=new Test2("B");  
         t1.start();  
         t2.start();  
          try {  
              t1.join();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            try {  
                t2.join();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }    
         System.out.println(Thread.currentThread().getName()+ "主執行緒執行結束!");  
    }

}

class Test2 extends Thread{  
    public Test2(String name) {  
        super(name);  
    }  
    public void run() {  
         System.out.println(this.getName() + " 執行緒執行開始!");  
       for (int i = 0; i < 5; i++) {  
           System.out.println("子執行緒"+this.getName() + "執行 : " + i);  
           try {  
               sleep(new Random().nextInt(10));  
           } catch (InterruptedException e) {  
               e.printStackTrace();  
           }  
       }  
       System.out.println(this.getName() + " 執行緒執行結束!");  
   }
}

執行結果:

    main主執行緒開始執行!
    B 執行緒執行開始!
    子執行緒B執行 : 0
    A 執行緒執行開始!
    子執行緒A執行 : 0
    子執行緒A執行 : 1
    子執行緒B執行 : 1
    子執行緒B執行 : 2
    子執行緒B執行 : 3
    子執行緒B執行 : 4
    B 執行緒執行結束!
    子執行緒A執行 : 2
    子執行緒A執行 : 3
    子執行緒A執行 : 4
    A 執行緒執行結束!
    main主執行緒執行結束!

上述示例中的結果顯然符合我們的預期。

priority

使用setPriority表示設定執行緒的優先順序。
每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY。
執行緒的優先順序有繼承關係,比如A執行緒中建立了B執行緒,那麼B將和A具有相同的優先順序。
JVM提供了10個執行緒優先順序,但與常見的作業系統都不能很好的對映。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式

  • static int MAX_PRIORITY 執行緒可以具有的最高優先順序,取值為10。
  • static int MIN_PRIORITY 執行緒可以具有的最低優先順序,取值為1。
  • static int NORM_PRIORITY 分配給執行緒的預設優先順序,取值為5。

但是設定優先順序並不能保證執行緒一定先執行。我們可以通過一下程式碼來驗證。

程式碼示例:

public class PriorityTest {
  public static void main(String[] args) {
        Test3 t1 = new Test3("張三");
        Test3 t2 = new Test3("李四");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

class Test3 extends Thread {
    public Test3(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println(this.getName() + " 執行緒執行開始!");  
        for (int i = 1; i <= 5; i++) {
            System.out.println("子執行緒"+this.getName() + "執行 : " + i); 
            try {  
                sleep(new Random().nextInt(10));  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } 
        }
        System.out.println(this.getName() + " 執行緒執行結束!");  
    }
}

執行結果一:

李四 執行緒執行開始!
子執行緒李四執行 : 1
張三 執行緒執行開始!
子執行緒張三執行 : 1
子執行緒張三執行 : 2
子執行緒李四執行 : 2
子執行緒李四執行 : 3
子執行緒李四執行 : 4
子執行緒張三執行 : 3
子執行緒李四執行 : 5
李四 執行緒執行結束!
子執行緒張三執行 : 4
子執行緒張三執行 : 5
張三 執行緒執行結束!

執行結果二:

張三 執行緒執行開始!
子執行緒張三執行 : 1
李四 執行緒執行開始!
子執行緒李四執行 : 1
子執行緒張三執行 : 2
子執行緒張三執行 : 3
子執行緒李四執行 : 2
子執行緒張三執行 : 4
子執行緒李四執行 : 3
子執行緒張三執行 : 5
子執行緒李四執行 : 4
張三 執行緒執行結束!
子執行緒李四執行 : 5
李四 執行緒執行結束!

執行結果三:

李四 執行緒執行開始!
子執行緒李四執行 : 1
張三 執行緒執行開始!
子執行緒張三執行 : 1
子執行緒李四執行 : 2
子執行緒李四執行 : 3
子執行緒李四執行 : 4
子執行緒張三執行 : 2
子執行緒張三執行 : 3
子執行緒張三執行 : 4
子執行緒李四執行 : 5
子執行緒張三執行 : 5
李四 執行緒執行結束!
張三 執行緒執行結束!

執行緒中一些常用的方法

執行緒中還有許多方法,但是這裡並不會全部細說。只簡單的列舉了幾個方法使用。更多的方法使用可以檢視相關的API文件。這裡我也順便總結了一些關於這些方法的描述。

  1. sleep:在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行);不會釋放物件鎖。
  2. join:指等待t執行緒終止。
  3. yield:暫停當前正在執行的執行緒物件,並執行其他執行緒。
  4. setPriority:設定一個執行緒的優先順序。
  5. interrupt:一個執行緒是否為守護執行緒。
  6. wait:強迫一個執行緒等待。它是Object的方法,也常常和sleep作為比較。需要注意的是wait會釋放物件鎖,讓其它的執行緒可以訪問;使用wait必須要進行異常捕獲,並且要對當前所呼叫,即必須採用synchronized中的物件。
  7. isAlive: 判斷一個執行緒是否存活。
  8. activeCount: 程式中活躍的執行緒數。
  9. enumerate: 列舉程式中的執行緒。
  10. currentThread: 得到當前執行緒。
  11. setDaemon: 設定一個執行緒為守護執行緒。(使用者執行緒和守護執行緒的區別在於,是否等待主執行緒依賴於主執行緒結束而結束)。
  12. setName: 為執行緒設定一個名稱。
  13. notify(): 通知一個執行緒繼續執行。它也是Object的一個方法,經常和wait方法一起使用。

結語

其實這篇文章很久之前都已經打好草稿了,但是由於各種原因,只到今天才寫完。雖然也只是簡單的介紹了一下多執行緒的相關知識,也只能算個入門級的教程吧。不過寫完之後,感覺自己又重新複習了一遍多執行緒,對多執行緒的理解又加深了一些。
話已盡此,不在多說。
原創不易,如果感覺不錯,希望給個推薦!您的支援是我寫作的最大動力!

參考:https://blog.csdn.net/evankaka/article/details/44153709#t1

相關推薦

Java基礎知識回顧 ----- 執行

前言 在上一篇文章中,回顧了Java的集合。而在本篇文章中主要介紹多執行緒的相關知識。主要介紹的知識點為執行緒的介紹、多執行緒的使用、以及在多執行緒中使用的一些方法。 執行緒和程序 執行緒 表示程序中負責程式執行的執行單元,依靠程式進行執行。執行緒是程式中的順序控制流,只能使用分配給程式的資源和環境。 程序

Java基礎知識回顧 ----- 線程

結束 tin tails 進程和線程 ali 而不是 理由 run 博客園 前言 在上一篇文章中,回顧了Java的集合。而在本篇文章中主要介紹多線程的相關知識。主要介紹的知識點為線程的介紹、多線程的使用、以及在多線程中使用的一些方法。 線程和進程 線程 表示進程中負責程序執

Java基礎知識回顧三 ----- 封裝、繼承和

get flex 防止 應用 需要 當前 nim lex aging 前言 在上一篇中回顧了java的修飾符和String類,這篇就來回顧下Java的三大特性:封裝、繼承、多態。 封裝 什麽是封裝 在面向對象程式設計方法中,封裝是指一種將抽象性函式接口的實現細節部份包裝、

Java基礎知識回顧型性

Java基礎知識回顧之封裝性 Java基礎知識回顧之繼承性 Java基礎知識回顧之多型性 簡介 多型的核心就是型別的一致性。物件上的每一個引用和靜態的型別檢查器都要確認這樣的依附(多個子類繼承一個父類)。 多型性嚴格來講有兩種描述形式 方法的多型性

Java基礎知識回顧六 ----- IO流

.net 是否 簡單 取數據 高效 它的 .cn 回顧 們的 前言 在上一篇文章中,回顧了Java的多線程。而在本篇文章中主要介紹Java IO的相關知識。 IO的介紹 什麽是IO? IO的名稱又來是Input與Output的縮寫,也就是輸入流和輸出流。輸入流用於從源讀取

Java基礎知識回顧七 ----- 總結篇

停止 pub mina 特定 文本 定義 們的 value 鍵值 前言 在之前Java基礎知識回顧中,我們回顧了基礎數據類型、修飾符和String、三大特性、集合、多線程和IO。本篇文章則對之前學過的知識進行總結。除了簡單的復習之外,還會增加一些相應的理解。 基礎數據類型

Java基礎知識回顧四 ----- 集合List、Map和Set

linked 訪問速度 因此 比較 foreach循環 代碼示例 的區別 不同的 寫法 前言 在上一篇中回顧了Java的三大特性:封裝、繼承和多態。本篇則來介紹下集合。 集合介紹 我們在進行Java程序開發的時候,除了最常用的基礎數據類型和String對象外,也經常會用到集

Java基礎知識回顧二 ----- 修飾符和String

表達式 概述 xxx 實驗 有時 原則 得到 私有 ali 前言 在上一篇中,回顧了Java的基本數據類型 ,這篇就來回顧下Java中的一些修飾符以及String。 修飾符介紹 Java修飾符主要分為兩類: 訪問修飾符 非訪問修飾符 其中訪問修飾符主要包括 privat

Java基礎知識回顧介面

抽象類與介面的區別: No. 區別 抽象類 介面 1 關鍵字 abstract class interface 2

Java基礎知識回顧抽象類

簡介 如果是需要了解抽象類,就需要知道什麼普通類。對於普通類,可以直接產生例項化物件,並且在普通類之中可以包含有構造方法、普通方法、static 方法、常量、變數等內容。而所謂的抽象類就是指在普通類的結構中增加抽象方法的組成部分。 在所有的普通方法上都會有一個“{}”,這個表示方

Java基礎知識回顧物件比較

說明 如果是需要判斷兩個數字是否相等,可以使用" == ",如果是判斷字串是否相等,是使用 “equals()”。如果是現在判斷一個自定義的類,要想判斷兩個物件是否相等,那麼應該是需要比較在類物件之中所有屬性內容的比較,只有所有屬性內容相等,才算相等。 直接比較屬性 如果我們

Java基礎知識回顧Object類

簡介 類Object是類層次結構的根類。每個類都使用Object作為超類。所有物件(包括陣列)都實現這個類的所有方法。我們接觸到的元素有:物件、陣列、介面等,這麼多的元素為了方便統一,就可以使用 Object。 任何一個類在定義的時候如果沒有明確的繼承一個父類的話,那麼他就是Ob

Java基礎知識回顧static應用與總結

相關關鍵字的說明: this關鍵字 final關鍵字 static關鍵字修飾屬性 static關鍵字修飾方法 static關鍵字應用與總結 前面兩篇分別介紹了static的屬性以及方法。本篇就做一個收尾,介紹下剩下的。 在之前的總結: 不管多少個物件,都使用同一個 s

Java基礎知識回顧static修飾方法

相關關鍵字的說明: this關鍵字 final關鍵字 static關鍵字修飾屬性 static關鍵字修飾方法 static關鍵字應用與總結 前面一篇已經介紹了部分關於 static 的用法,主要是介紹的是 static 修飾變數的一些知識。現在的這篇介紹下 static 修飾方法。

Java基礎知識回顧static修飾屬性

相關關鍵字的說明: this關鍵字 final關鍵字 static關鍵字修飾屬性 static關鍵字修飾方法 static關鍵字應用與總結 static 關鍵字,我們在開發用的還是比較多的。在《Java程式設計思想》有下面一段話 static 方法就是沒有 this 的方

Java基礎知識回顧final關鍵字

相關關鍵字的說明: this關鍵字 final關鍵字 static關鍵字修飾屬性 static關鍵字修飾方法 static關鍵字應用與總結 簡介 在 Java 中我們把 final 稱為終結器,在 Java 中可以使用 final 定義類、方法、屬性。通過 final 修飾以後,

Java基礎知識回顧this關鍵字

相關關鍵字的說明: this關鍵字 final關鍵字 static關鍵字修飾屬性 static關鍵字修飾方法 static關鍵字應用與總結 具體內容 在 Java 程式中,當區域性變數和全域性變數資料型別和名稱都相同的時,此時全域性變數會被隱藏而變得不能使用。即:區域性變數會覆蓋

Java基礎知識回顧可變引數

簡介 從 JDK 1.5 中,引入了可變引數的新特性。如果說要設計一個方法,這個方法可以接收任意多個整型資料。可以直接用(資料型別 ...屬性名)來解決。 使用可變引數時要注意,一個方法只能有一個可變引數,當方法含有多個引數時,可變引數要放在最後面。 public int a

Java基礎知識回顧方法過載

定義 方法過載(Overloading)的定義:如果有兩個方法的方法名相同,但引數不一致,哪麼可以說一個方法是另一個方法的過載。 具體說明如下: 方法名相同 方法的引數型別,引數個不一樣 方法的返回型別可以不相同 方法的修飾符可以不相同 main

Java基礎知識回顧繼承性

Java基礎知識回顧之封裝性 Java基礎知識回顧之繼承性 Java基礎知識回顧之多型性 簡介 繼承的最大特徵是解決程式碼的重用問題。特徵是,一個父類可以擁有多個子類。子類繼承父類的特性,並且子類可以有自己的"擴充套件特性"。 實現的方式 通過 class A exten