您好,登錄后才能下訂單哦!
JAVA中多線程編程方法的實例?這個問題可能是我們?nèi)粘W(xué)習(xí)或工作經(jīng)常見到的。希望通過這個問題能讓你收獲頗深。下面是小編給大家?guī)淼膮⒖純?nèi)容,讓我們一起來看看吧!
一、程序、進程、線程
程序是一組指令的有序集合,也可以將其通俗地理解為若干行代碼。它本身沒有任何運行的含義,它只是一個靜態(tài)的實體,它可能只是一個單純的文本文件,也有可能是經(jīng)過編譯之后生成的可執(zhí)行文件。
??從狹義來說,進程是正在運行的程序的實例;從廣義上來說,進程是一個具有一定獨立功能的程序關(guān)于某個數(shù)據(jù)集合的一次運行活動。進程是操作系統(tǒng)進行資源分配的基本單位。
??線程是進程中可獨立執(zhí)行的最小單位,它也是處理器進行獨立調(diào)度和分派的基本單位。一個進程可以包含多個線程,每個線程執(zhí)行自己的任務(wù),同一個進程中的所有線程共享該進程中的資源,如內(nèi)存空間、文件句柄等。
二、多線程編程簡介
1、什么是多線程編程
多線程編程技術(shù)是Java語言的重要特點。多線程編程的含義是將程序任務(wù)分成幾個并行的子任務(wù),并將這些子任務(wù)交給多個線程去執(zhí)行。
??多線程編程就是以線程為基本抽象單位的一種編程范式。但是,多線程編程又不僅僅是使用多個線程進行編程那么簡單,其自身又有需要解決的問題。多線程編程和面向?qū)ο缶幊淌强梢韵嗳莸?,即我們可以在面向?qū)ο缶幊痰幕A(chǔ)上實現(xiàn)多線程編程。事實上,Java平臺中的一個線程就是一個對象。
2、為什么要使用多線程編程
現(xiàn)在的計算機動輒就是多處理器核心的,而每一個線程同一時間只能運行在一個處理器上。如果只采用單線程進行開發(fā),那么就不能充分利用多核處理器的資源來提高程序的執(zhí)行效率。而使用多線程進行編程時,不同的線程可以運行在不同的處理器上。這樣一來,不僅大大提高了對計算機資源的利用率,同時也提高了程序的執(zhí)行效率。
三、JAVA線程API簡介
java.lang.Thread類就是Java平臺對線程的實現(xiàn)。Thread類或其子類的一個實例就是一個線程。
1、線程的創(chuàng)建、啟動、運行
在Java平臺中,創(chuàng)建一個線程就是創(chuàng)建一個Thread類(或其子類)的示例。每個線程都有其要執(zhí)行的任務(wù)。線程的任務(wù)處理邏輯可以在Thread類的run方法中直接實現(xiàn)或者通過該方法進行調(diào)用,因此run方法相當(dāng)于線程的任務(wù)處理邏輯的入口方法,它應(yīng)該由Java虛擬機在運行相應(yīng)線程時直接調(diào)用,而不應(yīng)該由應(yīng)用代碼進行調(diào)用。
??運行一個線程實際上就是讓Java虛擬機執(zhí)行該線程的run方法,從而使任務(wù)處理邏輯代碼得以執(zhí)行。如果一個線程沒有啟動,它的run方法是絕對不會被執(zhí)行的。為此,首先需要啟動線程。Thread類的start方法的作用是啟動相應(yīng)的線程。啟動一個線程的實質(zhì)是請求虛擬機運行相應(yīng)的線程,而這個線程具體何時能夠運行是由線程調(diào)度器(線程調(diào)度器是操作系統(tǒng)的一部分)決定的。因此,調(diào)用線程的start方法并不意味著線程已經(jīng)開始運行,這個線程可能馬上開始運行,也有可能稍后才被運行,也有可能永遠不運行。
??下面介紹兩種創(chuàng)建線程的方式(實際上還有其他方式,后續(xù)文章中會詳細介紹)。在此之前我們先來看一下Thread類的run方法的源碼:
// Code 1-1@Override public void run() { if (target != null) { target.run(); } }
這個run方法是在接口Runnable中定義的,它不接受參數(shù)也沒有返回值。事實上Runnable接口中也只有這一個方法,因此這個接口是一個函數(shù)式接口,這意味著我們可以在需要Runnable的地方使用lambda表達式。Thread類實現(xiàn)了這個接口,因此它必須實現(xiàn)這個方法。target是Thread類中的一個域,它的類型也是Runnable。target域表示這個線程需要執(zhí)行的內(nèi)容,而Thread類的run方法所做的也只是執(zhí)行target的run方法。
??我們剛剛提到,Java虛擬機會自動調(diào)用線程的run方法。但是,Thread類的run方法已經(jīng)定義好了,我們沒有辦法將自己需要執(zhí)行的代碼放在Thread類的run方法中。因此,我們可以考慮其他的方式來影響run方法的行為。第一種就是繼承Thread類并重寫run方法,這樣JVM在運行線程時就會調(diào)用我們重寫的run方法而不是Thread類的run方法;第二種方法是將我們要執(zhí)行的代碼傳遞給Thread類的target方法,而剛好Thread類有幾個構(gòu)造器可以直接對target進行賦值,這樣一來,JVM在調(diào)用run方法時執(zhí)行的仍然是我們傳遞的代碼。
??在Java平臺中,每個線程都可以擁有自己默認的名字,當(dāng)然我們也可以在構(gòu)造Thread類的實例時為我們的線程起一個名字,這個名字便于我們區(qū)分不同的線程。
??下面的代碼使用上述的兩種方式創(chuàng)建了兩個線程,它們要執(zhí)行的任務(wù)很簡單——打印一行歡迎信息,并且要包含自己的名字。
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()); } }
下面是這個程序運行時輸出的內(nèi)容:
1. Welcome, I'm Thread-0 2. Welcome, I'm Thread-1
多次運行這個程序,我們可以發(fā)現(xiàn)這個程序的輸出也有可能是:
2. Welcome, I'm Thread-1 1. Welcome, I'm Thread-0
這說明,雖然thread1的啟動在thread2之前,但這并不意味著thread1會在thread2之前被運行。
??不管采用哪種方式創(chuàng)建線程,一旦線程的run方法執(zhí)行(由JVM調(diào)用)結(jié)束,相應(yīng)線程的運行也就結(jié)束了。當(dāng)然,run方法執(zhí)行結(jié)束包括正常結(jié)束(run方法正常返回)和代碼中拋出異常而導(dǎo)致的終止。運行結(jié)束的線程所占用的資源(如內(nèi)存空間)會如同其他Java對象一樣被JVM回收。
??線程屬于“一次性用品”,我們不能通過重新調(diào)用一個已經(jīng)運行結(jié)束的線程的start方法來使其重新運行。事實上,start方法也只能夠被調(diào)用一次,多次調(diào)用同一個Thread實例的start方法會導(dǎo)致其拋出IllegalThreadStateException異常。
2、線程的屬性
線程的屬性包括線程的編號、名稱、類別和優(yōu)先級, 詳情如下表所示:
上面提到了守護線程和用戶線程的概念,這里對它們做一個簡要的說明。按照線程是否會阻止Java虛擬機正常停止,我們可以將Java中的線程分為守護線程(Daemon Thread)和用戶線程(User Thread,也稱非守護線程)。線程的daemon屬性用于表示相應(yīng)線程是否為守護線程。用戶線程會阻止Java虛擬機的正常停止,即一個Java虛擬機只有在其所有用戶線程都運行結(jié)束(即Thread.run()調(diào)用未結(jié)束)的情況下才能正常停止。而守護線程則不會影響Java虛擬機的正常停止,即應(yīng)用程序中有守護線程在運行也不影響Java虛擬機的正常停止。因此,守護線程通常用于執(zhí)行一些重要性不是很高的任務(wù),例如用于監(jiān)視其他線程的運行情況。
??當(dāng)然,如果Java虛擬機是被強制停止的,比如在Linux系統(tǒng)下使用kill命令強制終止一個Java虛擬機進程,那么即使是用戶線程也無法阻止Java虛擬機的停止。
3、Thread類常用方法
Java中的任何一段代碼總是執(zhí)行在某個線程之中。執(zhí)行當(dāng)前代碼的線程就被稱為當(dāng)前線程,Thread.currentThread()可以返回當(dāng)前線程。由于同一段代碼可能被不同的線程執(zhí)行,因此當(dāng)前線程是相對的,即Thread.currentThread()的返回值在代碼實際運行的時候可能對應(yīng)著不同的線程(對象)。
??join方法的作用相當(dāng)于執(zhí)行該方法的線程和線程調(diào)度器說:“我得先暫停一下,等到另外一個線程運行結(jié)束后我才能繼續(xù)?!?br/>??yield靜態(tài)方法的作用相當(dāng)于執(zhí)行該方法的線程對線程調(diào)度器說:“我現(xiàn)在不急,如果別人需要處理器資源的話先給它用吧。當(dāng)然,如果沒有其他人要用,我也不介意繼續(xù)占用?!?br/>??sleep靜態(tài)方法的作用相當(dāng)于執(zhí)行該方法的線程對線程調(diào)度器說:“我想小憩一會兒,過段時間再叫醒我繼續(xù)干活吧?!?/p>
4、Thread類中的廢棄方法
雖然這些方法并沒有相應(yīng)的替代品,但是可以使用其他辦法來實現(xiàn),我們會在后續(xù)文章中學(xué)習(xí)這部分內(nèi)容。
四、無處不在的線程
Java平臺本身就是一個多線程的平臺。除了Java開發(fā)人員自己創(chuàng)建和使用的線程,Java平臺中其他由Java虛擬機創(chuàng)建、使用的線程也隨處可見。當(dāng)然,這些線程也是各自有其處理任務(wù)。
??Java虛擬機啟動的時候會創(chuàng)建一個主線程(main線程),該線程負責(zé)執(zhí)行Java程序的入口方法(main方法)。下面的程序打印出主線程的名稱:
public class MainThreadDemo { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } }
??該程序會輸出“main”,這說明main方法是由一個名為“main”的線程調(diào)用的,這個線程就是主線程,它是由JVM創(chuàng)建并啟動的。
??在多線程編程中,弄清楚一段代碼具體是由哪個(或者哪種)線程去負責(zé)執(zhí)行的這點很重要,這關(guān)系到性能、線程安全等問題。本系列的后續(xù)文章會體現(xiàn)這點。
??Java 虛擬機垃圾回收器(Garbage Collector)負責(zé)對Java程序中不再使用的內(nèi)存空間進行回收,而這個回收的動作實際上也是通過專門的線程(垃圾回收線程)實現(xiàn)的,這些線程由Java虛擬機自行創(chuàng)建。
??為了提高Java代碼的執(zhí)行效率,Java虛擬機中的JIT(Just In Time)編譯器會動態(tài)地將Java字節(jié)碼編譯為Java虛擬機宿主機處理器可直接執(zhí)行的機器碼。這個動態(tài)編譯的過程實際上是由Java虛擬機創(chuàng)建的專門的線程負責(zé)執(zhí)行的。
??Java平臺中的線程隨處可見,這些線程各自都有其處理任務(wù)。
五、線程的層次關(guān)系
Java平臺中的線程不是孤立的,線程與線程之間總是存在一些聯(lián)系。假設(shè)線程A所執(zhí)行的代碼創(chuàng)建了線程B, 那么,習(xí)慣上我們稱線程B為線程A的子線程,相應(yīng)地線程A就被稱為線程B的父線程。例如, Code 1-2中的線程thread1和thread2是main線程的子線程,main線程是它們的父線程。子線程所執(zhí)行的代碼還可以創(chuàng)建其他線程,因此一個子線程也可以是其他線程的父線程。所以,父線程、子線程是一個相對的稱呼。理解線程的層次關(guān)系有助于我們理解Java應(yīng)用程序的結(jié)構(gòu),也有助于我們后續(xù)闡述其他概念。
??在Java平臺中,一個線程是否是一個守護線程默認取決于其父線程:默認情況下父線程是守護線程,則子線程也是守護線程;父線程是用戶線程,則子線程也是用戶線程。另外,父線程在創(chuàng)建子線程后啟動子線程之前可以調(diào)用該線程的setDaemon方法,將相應(yīng)的線程設(shè)置為守護線程(或者用戶線程)。
??一個線程的優(yōu)先級默認值為該線程的父線程的優(yōu)先級,即如果我們沒有設(shè)置或者更改一個線程的優(yōu)先級,那么這個線程的優(yōu)先級的值與父線程的優(yōu)先級的值相等。
??不過,Java平臺中并沒有API用于獲取一個線程的父線程,或者獲取一個線程的所有子線程。并且,父線程和子線程之間的生命周期也沒有必然的聯(lián)系。比如父線程運行結(jié)束后,子線程可以繼續(xù)運行,子線程運行結(jié)束也不妨礙其父線程繼續(xù)運行。
六、線程的生命周期狀態(tài)
?在Java平臺中,一個線程從其創(chuàng)建、啟動到其運行結(jié)束的整個生命周期可能經(jīng)歷若干狀態(tài)。如下圖所示:
線程的狀態(tài)可以通過Thread.getState()調(diào)用來獲取。Thread.getState()的返回值類型Thread.State,它是Thread類內(nèi)部的一個枚舉類型。Thread.State所定義的線程狀態(tài)包括以下幾種:
??NEW
:一個己創(chuàng)建而未啟動的線程處于該狀態(tài)。由于一個線程實例只能夠被啟動一次,因此一個線程只可能有一次處于該狀態(tài)。
??RUNNABLE
:該狀態(tài)可以被看成一個復(fù)合狀態(tài),它包括兩個子狀態(tài):READY和RUNNING,但實際上Thread.State中并沒有定義這兩種狀態(tài)。前者表示處于該狀態(tài)的線程可以被線程調(diào)度器進行調(diào)度而使之處于RUNNING狀態(tài)。后者表示處于該狀態(tài)的線程正在運行,即相應(yīng)線程對象的run方法所對應(yīng)的指令正在由處理器執(zhí)行。執(zhí)行Thread.yield()的線程,其狀態(tài)可能會由RUNNING轉(zhuǎn)換為READY。處于READY子狀態(tài)的線程也被稱為活躍線程。
??BLOCKED
:一個線程發(fā)起一個阻塞式I/0操作后,或者申請一個由其他線程持有的獨占資源(比如鎖)時,相應(yīng)的線程會處于該狀態(tài)。處于BLOCKED狀態(tài)的線程并不會占用處理器資源。當(dāng)阻塞式1/0操作完成后,或者線程獲得了其申請的資源,該線程的狀態(tài)又可以轉(zhuǎn)換為RUNNABLE。
??WAITING
:一個線程執(zhí)行了某些特定方法之后就會處于這種等待其他線程執(zhí)行另外一些特定操作的狀態(tài)。能夠使其執(zhí)行線程變更為WAITING狀態(tài)的方法包括:Object.wait()、Thread.join()和LockSupport.park(Object)。能夠使相應(yīng)線程從WAITING變更為RUNNABLE的相應(yīng)方法包括:Object.notify()/notifyAll()和LockSupport.unpark(Object))。
??TIMED_WAITING
:該狀態(tài)和WAITING類似,差別在于處于該狀態(tài)的線程并非無限制地等待其他線程執(zhí)行特定操作,而是處于帶有時間限制的等待狀態(tài)。當(dāng)其他線程沒有在指定時間內(nèi)執(zhí)行該線程所期望的特定操作時,該線程的狀態(tài)自動轉(zhuǎn)換為RUNNABLE。
??TERMINATED
:已經(jīng)執(zhí)行結(jié)束的線程處于該狀態(tài)。由于一個線程實例只能夠被啟動一次,因此一個線程也只可能有一次處于該狀態(tài)。run方法正常返回或者由于拋出異常而提前終止都會導(dǎo)致相應(yīng)線程處于該狀態(tài)。
??一個線程在其整個生命周期中,只可能有一次處于NEW狀態(tài)和TERMINATED狀態(tài)。
七、多線程編程的優(yōu)勢
多線程編程具有以下優(yōu)勢:
提高系統(tǒng)的吞吐率:多線程編程使得一個進程中可以有多個并發(fā)(即同時進行的)的操作。例如,當(dāng)一個線程因為I/0操作而處于等待時,其他線程仍然可以執(zhí)行其操作。
提高響應(yīng)性:在使用多線程編程的情況下,對于GUI軟件(如桌面應(yīng)用程序)而言,一個慢的操作(比如從服務(wù)器上下載一個大的文件)并不會導(dǎo)致軟件的界面出現(xiàn)被“凍住”的現(xiàn)象而無法響應(yīng)用戶的其他操作;對于Web應(yīng)用程序而言,一個請求的處理慢了并不會影響其他請求的處理。
充分利用多核處理器資源:如今多核處理器的設(shè)備越來越普及,就算是手機這樣的消費類設(shè)備也普遍使用多核處理器。實施恰當(dāng)?shù)亩嗑€程編程有助于我們充分利用設(shè)備的多核處理器資源,從而避免了資源浪費。
多線程編程也有自身的問題與風(fēng)險,包括以下幾個方面:
線程安全問題。多個線程共享數(shù)據(jù)的時候,如果沒有采取相應(yīng)的并發(fā)訪問控制措施,那么就可能產(chǎn)生數(shù)據(jù)一致性問題,如讀取臟數(shù)據(jù)(過期的數(shù)據(jù))、丟失更新(某些線程所做的更新被其他線程所做的更新覆蓋)等。
線程活性問題。一個線程從其創(chuàng)建到運行結(jié)束的整個生命周期會經(jīng)歷若于狀態(tài)。從單個線程的角度來看,RUNNABLE狀態(tài)是我們所期望的狀態(tài)。但實際上,代碼編寫不當(dāng)可能導(dǎo)致某些線程一直處于等待其他線程釋放鎖的狀態(tài)(BLOCKED狀態(tài)),這種情況稱為死鎖(Deadlock)。當(dāng)然,一直忙碌的線程也可能會出現(xiàn)問題,它可能面臨活鎖(Livelock)問題,即一個線程一直在嘗試某個操作但就是無法進展。另外,線程是一種稀缺的計算資源,一個系統(tǒng)所擁有的處理器數(shù)最相比于該系統(tǒng)中存在的線程數(shù)量而言總是少之又少的。某些情況下可能出現(xiàn)線程饑餓(Starvation)的問題,即某些線程永遠無法獲取處理器執(zhí)行的機會而永遠處于RUNNABLE狀態(tài)的READY子狀態(tài)。
上下文切換。處理器從執(zhí)行一個線程轉(zhuǎn)向執(zhí)行另外一個線程的時候操作系統(tǒng)所需要做的一個動作被稱為上下文切換。由于處理器資源的稀缺性,因此上下文切換可以被看作多線程編程的必然副產(chǎn)物,它增加了系統(tǒng)的消耗,不利于系統(tǒng)的吞吐率。
感謝各位的閱讀!看完上述內(nèi)容,你們對JAVA中多線程編程方法的實例大概了解了嗎?希望文章內(nèi)容對大家有所幫助。如果想了解更多相關(guān)文章內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。