Java多線程編程(1)--走進Java世界中的線程

来源:https://www.cnblogs.com/maconn/archive/2019/08/29/11428156.html
-Advertisement-
Play Games

線程是Java語言中不可或缺的重要功能,它能使複雜的非同步代碼變得更簡單,從而極大地簡化了複雜系統的開發。此外,要想充分發揮多處理器系統的強大計算能力,最簡單的方式就是使用線程。隨著處理器數量的持續增長,如何高效地使用併發正變得越來越重要。 ...


一.程式、進程和線程

  程式是一組指令的有序集合,也可以將其通俗地理解為若幹行代碼。它本身沒有任何運行的含義,它只是一個靜態的實體,它可能只是一個單純的文本文件,也有可能是經過編譯之後生成的可執行文件。
  從狹義來說,進程是正在運行的程式的實例;從廣義上來說,進程是一個具有一定獨立功能的程式關於某個數據集合的一次運行活動。進程是操作系統進行資源分配的基本單位。
  線程是進程中可獨立執行的最小單位,它也是處理器進行獨立調度和分派的基本單位。一個進程可以包含多個線程,每個線程執行自己的任務,同一個進程中的所有線程共用該進程中的資源,如記憶體空間、文件句柄等。

二.多線程編程簡介

什麼是多線程編程

  多線程編程技術是Java語言的重要特點。多線程編程的含義是將程式任務分成幾個並行的子任務,並將這些子任務交給多個線程去執行。
  多線程編程就是以線程為基本抽象單位的一種編程範式。但是,多線程編程又不僅僅是使用多個線程進行編程那麼簡單,其自身又有需要解決的問題。多線程編程和麵向對象編程是可以相容的,即我們可以在面向對象編程的基礎上實現多線程編程。事實上,Java平臺中的一個線程就是一個對象。

為什麼要使用多線程編程

  現在的電腦動輒就是多處理器核心的,而每一個線程同一時間只能運行在一個處理器上。如果只採用單線程進行開發,那麼就不能充分利用多核處理器的資源來提高程式的執行效率。而使用多線程進行編程時,不同的線程可以運行在不同的處理器上。這樣一來,不僅大大提高了對電腦資源的利用率,同時也提高了程式的執行效率。

三.Java線程API簡介

  java.lang.Thread類就是Java平臺對線程的實現。Thread類或其子類的一個實例就是一個線程。

1.線程的創建、啟動和運行

  在Java平臺中,創建一個線程就是創建一個Thread類(或其子類)的示例。每個線程都有其要執行的任務。線程的任務處理邏輯可以在Thread類的run方法中直接實現或者通過該方法進行調用,因此run方法相當於線程的任務處理邏輯的入口方法,它應該由Java虛擬機在運行相應線程時直接調用,而不應該由應用代碼進行調用。
  運行一個線程實際上就是讓Java虛擬機執行該線程的run方法,從而使任務處理邏輯代碼得以執行。如果一個線程沒有啟動,它的run方法是絕對不會被執行的。為此,首先需要啟動線程。Thread類的start方法的作用是啟動相應的線程。啟動一個線程的實質是請求虛擬機運行相應的線程,而這個線程具體何時能夠運行是由線程調度器(線程調度器是操作系統的一部分)決定的。因此,調用線程的start方法並不意味著線程已經開始運行,這個線程可能馬上開始運行,也有可能稍後才被運行,也有可能永遠不運行。
  下麵介紹兩種創建線程的方式(實際上還有其他方式,後續文章中會詳細介紹)。在此之前我們先來看一下Thread類的run方法的源碼:

// Code 1-1
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

  這個run方法是在介面Runnable中定義的,它不接受參數也沒有返回值。事實上Runnable介面中也只有這一個方法,因此這個介面是一個函數式介面,這意味著我們可以在需要Runnable的地方使用lambda表達式。Thread類實現了這個介面,因此它必須實現這個方法。target是Thread類中的一個域,它的類型也是Runnable。target域表示這個線程需要執行的內容,而Thread類的run方法所做的也只是執行target的run方法。
  我們剛剛提到,Java虛擬機會自動調用線程的run方法。但是,Thread類的run方法已經定義好了,我們沒有辦法將自己需要執行的代碼放在Thread類的run方法中。因此,我們可以考慮其他的方式來影響run方法的行為。第一種就是繼承Thread類並重寫run方法,這樣JVM在運行線程時就會調用我們重寫的run方法而不是Thread類的run方法;第二種方法是將我們要執行的代碼傳遞給Thread類的target方法,而剛好Thread類有幾個構造器可以直接對target進行賦值,這樣一來,JVM在調用run方法時執行的仍然是我們傳遞的代碼。
  在Java平臺中,每個線程都可以擁有自己預設的名字,當然我們也可以在構造Thread類的實例時為我們的線程起一個名字,這個名字便於我們區分不同的線程。
  下麵的代碼使用上述的兩種方式創建了兩個線程,它們要執行的任務很簡單——列印一行歡迎信息,並且要包含自己的名字。

// Code 1-2

public class WelcomeApp {
    public static void main(String[] args) {
        Thread thread1 = new WelcomeThread();
        Thread thread2 = new Thread(() -> System.out.println("2. Welcome, I'm " + Thread.currentThread().getName()));
        thread1.start();
        thread2.start();
    }
}

class WelcomeThread extends Thread {
    @Override
    public void run() {
        System.out.println("1. Welcome, I'm " + Thread.currentThread().getName());
    }
}

  下麵是這個程式運行時輸出的內容:

1. Welcome, I'm Thread-0
2. Welcome, I'm Thread-1

  多次運行這個程式,我們可以發現這個程式的輸出也有可能是:

2. Welcome, I'm Thread-1
1. Welcome, I'm Thread-0

  這說明,雖然thread1的啟動在thread2之前,但這並不意味著thread1會在thread2之前被運行。
  不管採用哪種方式創建線程,一旦線程的run方法執行(由JVM調用)結束,相應線程的運行也就結束了。當然,run方法執行結束包括正常結束(run方法正常返回)和代碼中拋出異常而導致的終止。運行結束的線程所占用的資源(如記憶體空間)會如同其他Java對象一樣被JVM回收。
  線程屬於“一次性用品”,我們不能通過重新調用一個已經運行結束的線程的start方法來使其重新運行。事實上,start方法也只能夠被調用一次,多次調用同一個Thread實例的start方法會導致其拋出IllegalThreadStateException異常。

2.線程的屬性

  線程的屬性包括線程的編號、名稱、類別和優先順序, 詳情如下表所示:

  上面提到了守護線程和用戶線程的概念,這裡對它們做一個簡要的說明。按照線程是否會阻止Java虛擬機正常停止,我們可以將Java中的線程分為守護線程(Daemon Thread)和用戶線程(User Thread,也稱非守護線程)。線程的daemon屬性用於表示相應線程是否為守護線程。用戶線程會阻止Java虛擬機的正常停止,即一個Java虛擬機只有在其所有用戶線程都運行結束(即Thread.run()調用未結束)的情況下才能正常停止。而守護線程則不會影響Java虛擬機的正常停止,即應用程式中有守護線程在運行也不影響Java虛擬機的正常停止。因此,守護線程通常用於執行一些重要性不是很高的任務,例如用於監視其他線程的運行情況。
  當然,如果Java虛擬機是被強制停止的,比如在Linux系統下使用kill命令強制終止一個Java虛擬機進程,那麼即使是用戶線程也無法阻止Java虛擬機的停止。
  

3.Thread類的常用方法

  下麵列出了Thread類中常用的方法:

  Java中的任何一段代碼總是執行在某個線程之中。執行當前代碼的線程就被稱為當前線程,Thread.currentThread()可以返回當前線程。由於同一段代碼可能被不同的線程執行,因此當前線程是相對的,即Thread.currentThread()的返回值在代碼實際運行的時候可能對應著不同的線程(對象)。
  join方法的作用相當於執行該方法的線程和線程調度器說:“我得先暫停一下,等到另外一個線程運行結束後我才能繼續。”
  yield靜態方法的作用相當於執行該方法的線程對線程調度器說:“我現在不急,如果別人需要處理器資源的話先給它用吧。當然,如果沒有其他人要用,我也不介意繼續占用。”
  sleep靜態方法的作用相當於執行該方法的線程對線程調度器說:“我想小憩一會兒,過段時間再叫醒我繼續幹活吧。”

4.Thread類中的廢棄方法

  由於Java虛擬機實現得有些問題,因此Thread類的有些方法已經被廢棄了,在新寫的代碼中應該避免使用這些方法。部分廢棄的方法如下表所示:

  雖然這些方法並沒有相應的替代品,但是可以使用其他辦法來實現,我們會在後續文章中學習這部分內容。

四.無處不在的線程

  Java平臺本身就是一個多線程的平臺。除了Java開發人員自己創建和使用的線程,Java平臺中其他由Java虛擬機創建、使用的線程也隨處可見。當然,這些線程也是各自有其處理任務。
  Java虛擬機啟動的時候會創建一個主線程(main線程),該線程負責執行Java程式的入口方法(main方法)。下麵的程式列印出主線程的名稱:

// Code 1-3

public class MainThreadDemo {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

  該程式會輸出“main”,這說明main方法是由一個名為“main”的線程調用的,這個線程就是主線程,它是由JVM創建並啟動的。
  在多線程編程中,弄清楚一段代碼具體是由哪個(或者哪種)線程去負責執行的這點很重要,這關係到性能、線程安全等問題。本系列的後續文章會體現這點。
  Java 虛擬機垃圾回收器(Garbage Collector)負責對Java程式中不再使用的記憶體空間進行回收,而這個回收的動作實際上也是通過專門的線程(垃圾回收線程)實現的,這些線程由Java虛擬機自行創建。
  為了提高Java代碼的執行效率,Java虛擬機中的JIT(Just In Time)編譯器會動態地將Java位元組碼編譯為Java虛擬機宿主機處理器可直接執行的機器碼。這個動態編譯的過程實際上是由Java虛擬機創建的專門的線程負責執行的。
  Java平臺中的線程隨處可見,這些線程各自都有其處理任務。

五.線程的層次關係

  Java平臺中的線程不是孤立的,線程與線程之間總是存在一些聯繫。假設線程A所執行的代碼創建了線程B, 那麼,習慣上我們稱線程B為線程A的子線程,相應地線程A就被稱為線程B的父線程。例如, Code 1-2中的線程thread1和thread2是main線程的子線程,main線程是它們的父線程。子線程所執行的代碼還可以創建其他線程,因此一個子線程也可以是其他線程的父線程。所以,父線程、子線程是一個相對的稱呼。理解線程的層次關係有助於我們理解Java應用程式的結構,也有助於我們後續闡述其他概念。
  在Java平臺中,一個線程是否是一個守護線程預設取決於其父線程:預設情況下父線程是守護線程,則子線程也是守護線程;父線程是用戶線程,則子線程也是用戶線程。另外,父線程在創建子線程後啟動子線程之前可以調用該線程的setDaemon方法,將相應的線程設置為守護線程(或者用戶線程)。
  一個線程的優先順序預設值為該線程的父線程的優先順序,即如果我們沒有設置或者更改一個線程的優先順序,那麼這個線程的優先順序的值與父線程的優先順序的值相等。
  不過,Java平臺中並沒有API用於獲取一個線程的父線程,或者獲取一個線程的所有子線程。並且,父線程和子線程之間的生命周期也沒有必然的聯繫。比如父線程運行結束後,子線程可以繼續運行,子線程運行結束也不妨礙其父線程繼續運行。

六.線程的生命周期狀態

  在Java平臺中,一個線程從其創建、啟動到其運行結束的整個生命周期可能經歷若幹狀態。如下圖所示:

  線程的狀態可以通過Thread.getState()調用來獲取。Thread.getState()的返回值類型Thread.State,它是Thread類內部的一個枚舉類型。Thread.State所定義的線程狀態包括以下幾種:
  NEW:一個己創建而未啟動的線程處於該狀態。由於一個線程實例只能夠被啟動一次,因此一個線程只可能有一次處於該狀態。
  RUNNABLE:該狀態可以被看成一個複合狀態,它包括兩個子狀態:READY和RUNNING,但實際上Thread.State中並沒有定義這兩種狀態。前者表示處於該狀態的線程可以被線程調度器進行調度而使之處於RUNNING狀態。後者表示處於該狀態的線程正在運行,即相應線程對象的run方法所對應的指令正在由處理器執行。執行Thread.yield()的線程,其狀態可能會由RUNNING轉換為READY。處於READY子狀態的線程也被稱為活躍線程。
  BLOCKED:一個線程發起一個阻塞式I/0操作後,或者申請一個由其他線程持有的獨占資源(比如鎖)時,相應的線程會處於該狀態。處於BLOCKED狀態的線程並不會占用處理器資源。當阻塞式1/0操作完成後,或者線程獲得了其申請的資源,該線程的狀態又可以轉換為RUNNABLE。
  WAITING:一個線程執行了某些特定方法之後就會處於這種等待其他線程執行另外一些特定操作的狀態。能夠使其執行線程變更為WAITING狀態的方法包括:Object.wait()、Thread.join()和LockSupport.park(Object)。能夠使相應線程從WAITING變更為RUNNABLE的相應方法包括:Object.notify()/notifyAll()和LockSupport.unpark(Object))。
  TIMED_WAITING:該狀態和WAITING類似,差別在於處於該狀態的線程並非無限制地等待其他線程執行特定操作,而是處於帶有時間限制的等待狀態。當其他線程沒有在指定時間內執行該線程所期望的特定操作時,該線程的狀態自動轉換為RUNNABLE。
  TERMINATED:已經執行結束的線程處於該狀態。由於一個線程實例只能夠被啟動一次,因此一個線程也只可能有一次處於該狀態。run方法正常返回或者由於拋出異常而提前終止都會導致相應線程處於該狀態。
  一個線程在其整個生命周期中,只可能有一次處於NEW狀態和TERMINATED狀態。

七.多線程編程的優勢

  多線程編程具有以下優勢:

  • 提高系統的吞吐率:多線程編程使得一個進程中可以有多個併發(即同時進行的)的操作。例如,當一個線程因為I/0操作而處於等待時,其他線程仍然可以執行其操作。
  • 提高響應性:在使用多線程編程的情況下,對於GUI軟體(如桌面應用程式)而言,一個慢的操作(比如從伺服器上下載一個大的文件)並不會導致軟體的界面出現被“凍住”的現象而無法響應用戶的其他操作;對於Web應用程式而言,一個請求的處理慢了並不會影響其他請求的處理。
  • 充分利用多核處理器資源:如今多核處理器的設備越來越普及,就算是手機這樣的消費類設備也普遍使用多核處理器。實施恰當的多線程編程有助於我們充分利用設備的多核處理器資源,從而避免了資源浪費。

  多線程編程也有自身的問題與風險,包括以下幾個方面:

  • 線程安全問題。多個線程共用數據的時候,如果沒有採取相應的併發訪問控制措施,那麼就可能產生數據一致性問題,如讀取臟數據(過期的數據)、丟失更新(某些線程所做的更新被其他線程所做的更新覆蓋)等。
  • 線程活性間題。一個線程從其創建到運行結束的整個生命周期會經歷若於狀態。從單個線程的角度來看,RUNNABLE狀態是我們所期望的狀態。但實際上,代碼編寫不當可能導致某些線程一直處於等待其他線程釋放鎖的狀態(BLOCKED狀態),這種情況稱為死鎖(Deadlock)。當然,一直忙碌的線程也可能會出現問題,它可能面臨活鎖(Livelock)問題,即一個線程一直在嘗試某個操作但就是無法進展。另外,線程是一種稀缺的計算資源,一個系統所擁有的處理器數最相比於該系統中存在的線程數量而言總是少之又少的。某些情況下可能出現線程饑餓(Starvation)的問題,即某些線程永遠無法獲取處理器執行的機會而永遠處於RUNNABLE狀態的READY子狀態。
  • 上下文切換。處理器從執行一個線程轉向執行另外一個線程的時候操作系統所需要做的一個動作被稱為上下文切換。由於處理器資源的稀缺性,因此上下文切換可以被看作多線程編程的必然副產物,它增加了系統的消耗,不利於系統的吞吐率。

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

-Advertisement-
Play Games
更多相關文章
  • 近期練習flask寫個blog, 安裝flask擴展時 報ERROR: Could not install packages due to an EnvironmentError: [Errno 13] Permission denied: '/Library/Python/2.7/site-pac ...
  • effective java 3th 序,java語言的簡單與複雜,以及本書出現的理由 ...
  • 如何解決驗證碼的問題,用什麼模塊,聽過哪些人工打碼平臺? PIL、pytesser、tesseract模塊 平臺的話有:(打碼平臺特殊,不保證時效性) 雲打碼 掙碼 斐斐打碼 若快打碼 超級鷹本文首發於Python黑洞網,博客園同步更新 ...
  • 一、說明 網關的核心概念就是路由配置和路由規則,而作為所有請求流量的入口,在實際生產環境中為了保證高可靠和高可用,是儘量要避免重啟的,所以實現動態路由是非常有必要的;本文主要介紹實現的思路,並且以 為數據源來講解   二、實現要點 要實現動態路由只需關註下麵4個點 1. 網關啟動時, 的數 ...
  • "鏈接" A 理解一下題意,然後玩幾組樣例就能發現,實際上就是$k$個$i$等價於$1$個$i 1$。所以就類似於$k$進位進行進位,如果最後$0$位上不是$0$,那麼就存在劃分方案。否則就不存在劃分方案。 輸出第一次劃分方案就記錄一下每個數字是不是後面的數字湊出來的。如果是的話就像後面數字連邊。這 ...
  • 說明 實現網頁登錄後跳轉應該分為兩類:即 登錄成功後跳轉 和 登錄失敗再次登錄成功後跳轉 。參考網上內容,基本都只實現了第一類。而沒有實現第二類。 實現 為了能讓登錄失敗後再次登錄成功後還能實現跳轉。我這裡採用了笨辦法, 即: 無論登錄成功與否,都將跳轉鏈接在前後端進行傳遞 ,這樣跳轉鏈接就不會在登 ...
  • 現在有如下幾個模塊: A.py 功能:定義全局變數,供其他模塊使用 B.py 功能:列印A.py中的變數 main.py 運行後我們發現: A模塊中的name被修改了,但在B模塊中列印的還是原來的值。 A模塊中的lists添加了新的元素,顯示正常。 原因: 使用from A import name ...
  • 最近網站PC端集成微信掃碼登錄,踩了不少坑,在此記錄下實現過程和註意事項。 本文目錄 一、微信開放平臺操作步驟1.創建“網站應用”2.獲取AppID和AppSecret二、開髮指南三、開發實戰1、pom.xml引入jar包2、配置文件添加對應的配置3、初始化配置4、控制層核心代碼四、運行效果1.構造 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...