Java基礎知識回顧之五 ----- 多線程

来源:https://www.cnblogs.com/xuwujing/archive/2018/05/28/9102870.html
-Advertisement-
Play Games

前言 在 "上一篇" 文章中,回顧了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

版權聲明:
作者:虛無境
博客園出處:http://www.cnblogs.com/xuwujing
CSDN出處:http://blog.csdn.net/qazwsxpcm    
個人博客出處:http://www.panchengming.com


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、什麼是Servlet Servlet 運行在服務端的Java小程式, 是sun公司提供一套規範(介面) 主要功能: 用來處理客戶端請求 響應給瀏覽器的動態資源 servlet的實質就是java代碼, 通過java的API動態的向客戶端輸出內容 以後寫的程式就不在是在本地執行了。 而是編譯成位元組碼 ...
  • 就算你沒有用到過其他的設計模式,但是單例模式你肯定接觸過,比如,Spring 中 bean 預設就是單例模式的,所有用到這個 bean 的實例其實都是同一個。 單例模式的使用場景 什麼是單例模式呢,單例模式(Singleton)又叫單態模式,它出現目的是為了保證一個類在系統中只有一個實例,並提供一個 ...
  • 這一介面會對實現了它的類施加一個整體的順序.這一順序被認為是類的自然順序,類的比較方法compareTo()也被認為是自然比較方法 實現這一介面的對象中,List類對象使用Collections.sort方法實現自動排序(升序),數組使用Arrays.sort()方法實現升序排序.實現這一介面的對象 ...
  • Python代碼如下: ...
  • Java類庫中有為滿足不同需求而設計的不同的器,實際上就是不同的介面。最近學習了比較器、迭代器和文件過濾器這三個介面,我根據自己的理解做了一個不成熟的總結,假如有很多不准確甚至是錯誤的地方,希望大家多多賜教! 這三個介面在設計的時候,並不是只是聲明一個介面以及它裡面的方法,也在需要特定類“配合”這些 ...
  • 書名:流暢的Python作者:[巴西] Luciano Ramalho譯者:安道 吳珂ISBN:978-7-115-45415-7 需要學習的朋友可以通過網盤下載pdf版 http://tadown.com/fs/cyibbebnsahu08034/ 目標讀者本書的目標讀者是那些正在使用 Pytho ...
  • 在上一篇博客中已經介紹了django rest framework 對於認證的源碼流程,以及實現過程,當用戶經過認證之後下一步就是涉及到許可權的問題。比如訂單的業務只能VIP才能查看,所以這時候需要對許可權進行控制。下麵將介紹DRF的許可權控制源碼剖析。 這裡繼續使用之前的示例,加入相應的許可權,這裡先介紹 ...
  • 一個功能瀏覽器發送hello請求,伺服器接受請求並處理,響應Hello World字元串.1.創建一個maven工程;(jar)2.導入依賴Spring Boot相關的依賴 3.編寫一個主程式;啟動Spring Boot應用4.編寫相關Controller、Service 5.運行主程式測試6.簡化 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...