香港公司需要網(wǎng)站備案朋友圈廣告推廣平臺
多線程是提升程序性能非常重要的一種方式,也是Java編程中的一項重要技術。在程序設計中,多線程就是指一個應用程序中有多條并發(fā)執(zhí)行的線索,每條線索都被稱作一個線程,它們會交替執(zhí)行,彼此間可以進行通信。
1. 進程與線程
進程是指正在運行的程序,是系統(tǒng)進行資源分配和調度的基本單位。為了有效利用系統(tǒng)資源和提高運行效率,在一個進程中還可以有若干同時并發(fā)運行的線程。每一個進程都至少存在一個線程。
當一個Java程序啟動時,就會產生一個進程,該進程中會默認創(chuàng)建一個線程,在這個線程上運行main()方法中的代碼,這樣的程序稱為單線程程序。單線程程序中只有一個線程在運行,效率相對較低。好比是售票大廳只開設一個售票窗口,所有人只能在一個窗口排除買票,整個售票過程效率較低;如果同時開設多個售票窗口售票,則可以提高售票效率。編寫的程序也是同樣,可以通過創(chuàng)建多線程程序來提高程序運行效率。
2. 線程的創(chuàng)建
Java中提供了三種方式來實現(xiàn)多線程。
(1)Thread類實現(xiàn)多線程
Thread類是java.lang包下的一個線程類,可通過繼承Thread類的方式來實現(xiàn)多線程,其使用方法是先創(chuàng)建一個Thread線程類的子類(子線程),同時重寫Thread類的run()方法;然后創(chuàng)建該子類的實例對象,并通過調用start()方法啟動線程。
【例11-1】通過Thread類實現(xiàn)多線程
運行結果如下圖所示。
通過運行結果可以看到,兩個線程對象交互執(zhí)行了各自重寫的run()方法。并不是按順序先執(zhí)行完第一個線程再執(zhí)行第二個線程。
(2)Runnable接口實現(xiàn)多線程
雖然可以通過繼承Thread類實現(xiàn)多線程,但這種使用方式有一定的局限性。因Java只支持單繼承,如果某個類已經繼承了其他類,就無法再繼承Thread類來實現(xiàn)多線程。這時可通過實現(xiàn)Runnable接口的方式來實現(xiàn)多線程。
①使用實現(xiàn)Runnable接口的方式來實現(xiàn)多線程的主要過程如下。
②創(chuàng)建一個Runnable接口的實現(xiàn)類,重寫接口中的run()方法。
③創(chuàng)建Runnable接口的實現(xiàn)類對象。
④使用Thread有參構造方法創(chuàng)建線程實例,并將Runnable接口的實現(xiàn)類的對象作為參數(shù)傳入。
⑤調用線程實例的start()方法啟動線程。
【例11-2】通過實現(xiàn)Runnable接口的方式來實現(xiàn)多線程
運行結果如下圖所示。
Callable接口實現(xiàn)多線程
通過Thread類和Runnable接口實現(xiàn)多線程時,需要重寫run()方法,但是由于該方法沒有返回值,因此無法從多個線程中獲取返回結果。為了解決這個問題,從JDK 5開始,Java提供了一個新的Callable接口,來滿足這種既能創(chuàng)建多線程又可以有返回值的需求。
Callable接口實現(xiàn)多線程是通過Thread類的有參構造方法傳入Runnable接口類型的參數(shù)來實現(xiàn)多線程,不同的是,這里傳入的是Runnable接口的子類FutureTask對象作為參數(shù),而FutureTask對象中則封裝帶有返回值的Callable接口實現(xiàn)類。
通過Callable接口實現(xiàn)多線程的過程如下。
①創(chuàng)建一個Callable接口的實現(xiàn)類,同時重寫Callable接口的call()方法;
②創(chuàng)建Callable接口的實現(xiàn)類對象;
③通過FutureTask線程結果處理類的有參構造方法來封裝Callable接口實現(xiàn)類對象;
④使用參數(shù)為FutureTask類對象的Thread有參構造方法創(chuàng)建Thread線程實例;
⑤調用線程實例的start()方法啟動線程。
【例11-3】通過Callable接口實現(xiàn)多線程
運行結果如下圖所示。
Callable接口方式實現(xiàn)的多線程是通過FutureTask類來封裝和管理返回結果的,該類的直接父接口是RunnableFuture。FutureTask類的繼承關系如下圖所示。
FutureTask本質是Runnable接口和Future接口的實現(xiàn)類,而Future則是用來管理線程執(zhí)行返回結果的。其中Future接口中有5個方法來對線程結果進行管理,如表1所示。
表1?Future接口的方法
方法聲明 | 功能描述 |
boolean cancel(boolean mayInterruptIfRunning) | 用于取消任務,參數(shù)mayInterruptIfRunning表示是否允許取消正在執(zhí)行卻沒有執(zhí)行完畢的任務,如果設置true,則表示可以取消正在執(zhí)行的任務 |
boolean isCancelled() | 判斷任務是否被取消成功,如果在任務正常完成前被取消成功,則返回?true |
boolean isDone() | 判斷任務是否已經完成,若任務完成,則返回true |
V get() | 用于獲取執(zhí)行結果,這個方法會發(fā)生阻塞,一直等到任務執(zhí)行完畢才返回執(zhí)行結果 |
V get(long timeout, TimeUnit unit) | 用于在指定時間內獲取執(zhí)行結果,如果在指定時間內,還沒獲取到結果,就直接返回null |
?
3. 線程的生命周期
在Java中,任何對象都有生命周期,線程也不例外,它也有自己的生命周期。當Thread對象創(chuàng)建完成時,線程的生命周期便開始了。當線程任務中代碼正常執(zhí)行完畢或者線程拋出一個未捕獲的異常(Exception)或者錯誤(Error)時,線程的生命周期便會結束。線程的整個生命周期分為6個狀態(tài),分別是NEW(新建狀態(tài))、RUNNABLE(可運行狀態(tài))、BLOCKED(阻塞狀態(tài))、WAITING(等待狀態(tài))、TIMED_WAITING(定時等待狀態(tài))和TERMINATED(終止狀態(tài))。線程的不同狀態(tài)表明了線程當前正在進行的活動。程序中,通過一些操作,可以使線程在不同狀態(tài)之間轉換,如下圖所示。
(1)NEW(新建狀態(tài))
創(chuàng)建一個線程對象后,該線程對象就處于新建狀態(tài),此時它不能運行,和其他Java對象一樣,僅僅由JVM為其分配了內存,沒有表現(xiàn)出任何線程的動態(tài)特征。
(2)RUNNABLE(可運行狀態(tài))
新建狀態(tài)的線程調用start()方法,就會進入可運行狀態(tài)。在RUNNABLE狀態(tài)內部又可細分成兩種狀態(tài):READY(就緒狀態(tài))和RUNNING(運行狀態(tài)),并且線程可以在這兩個狀態(tài)之間相互轉換。
RUNNABLE內部狀態(tài)轉換:
①就緒狀態(tài):線程對象調用start()方法之后,等待JVM的調度,此時線程并沒有運行;
②運行狀態(tài):線程對象獲得JVM調度,如果存在多個CPU,那么允許多個線程并行運行。
(3)BLOCKED(阻塞狀態(tài))
運行狀態(tài)的線程因為某些原因失去CPU的執(zhí)行權,會進入阻塞狀態(tài)。阻塞狀態(tài)的線程只能先進入就緒狀態(tài),不能直接進入運行狀態(tài)。
線程一般會在以下兩種情況時進入阻塞狀態(tài):
①當線程A運行過程中,試圖獲取同步鎖時,卻被線程B獲取;
②當線程運行過程中,發(fā)出IO請求時。
(4)WAITING(等待狀態(tài))
當運行狀態(tài)的線程調用了無時間參數(shù)限制的方法后,如wait()、join()等方法,就會轉換為等待狀態(tài)。
處于等待狀態(tài)中的線程不能立即爭奪CPU使用權,必須等待其他線程執(zhí)行特定的操作后,才有機會爭奪CPU使用權。例如調用wait()方法而處于等待狀態(tài)中的線程,必須等待其他線程調用notify()或者notifyAll()方法喚醒當前等待中的線程;調用join()方法而處于等待狀態(tài)中的線程,必須等待其他加入的線程終止。
(5)TIMED_WAITING(定時等待狀態(tài))
當運行狀態(tài)中的線程調用了有時間參數(shù)限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)等方法,就會轉換為定時等待狀態(tài)。
處于定時等待狀態(tài)中的線程不能立即爭奪CPU使用權,必須等待其他相關線程執(zhí)行完特定的操作或者限時時間結束后,才有機會再次爭奪CPU使用權。例如,調用了wait(long timeout) 方法而處于等待狀態(tài)中的線程,需要通過其他線程調用notify()或者notifyAll()方法喚醒當前等待中的線程,或者等待限時時間結束后也可以進行狀態(tài)轉換。
(6)TERMINATED(終止狀態(tài))
當線程的run()方法、call()方法正常執(zhí)行完畢或者線程拋出一個未捕獲的異常(Exception)、錯誤(Error),線程就進入終止狀態(tài)。一旦進入終止狀態(tài),線程將不再擁有運行的資格,也不能再轉換到其他狀態(tài),生命周期結束。
4. 線程的調度
程序中的多個線程是并發(fā)執(zhí)行的,但并不是同一時刻執(zhí)行,某個線程若想被執(zhí)行必須要得到CPU的使用權。Java虛擬機會按照特定的機制為程序中的每個線程分配CPU的使用權,這種機制被稱作線程的調度。
線程調度有兩種模型,分別是分時調度模型和搶占式調度模型。分時調度是指讓所有的線程輪流獲得CPU的使用權,并且平均分配每個線程占用的CPU時間片。搶占式調度是指讓可運行池中所有就緒狀態(tài)的線程爭搶CPU的使用權,而優(yōu)先級高的線程獲取CPU執(zhí)行權的概率大于優(yōu)先級低的線程。
Java虛擬機默認采用搶占式調度模型,多數(shù)情況下不需要去關心它,在某些特定的需求下需要改變這種模式時可由程序來控制 CPU的調度。
(1)設置線程的優(yōu)先級
在程序中如果要對線程進行調度,最直接的方式就是設置線程的優(yōu)先級。優(yōu)先級越高的線程獲得CPU執(zhí)行的機會越大,而優(yōu)先級越低的線程獲得CPU執(zhí)行的機會越小。
線程的優(yōu)先級用1~10之間的整數(shù)來表示,數(shù)字越大優(yōu)先級越高。除了可以直接使用數(shù)字表示線程的優(yōu)先級,還可以使用Thread類中提供的三個靜態(tài)常量(如表2所示)表示線程的優(yōu)先級。
表2?Thread類的優(yōu)先級常量
Thread類的靜態(tài)常量 | 功能描述 |
static int MAX_PRIORITY | 表示線程的最高優(yōu)先級,相當于值10 |
static int MIN_PRIORITY | 表示線程的最低優(yōu)先級,相當于值1 |
static int NORM_PRIORIY | 表示線程的普通優(yōu)先級,相當于值5 |
程序在運行期間,處于就緒狀態(tài)的每個線程都有自己的優(yōu)先級,例如main線程具有普通優(yōu)先級??梢酝ㄟ^Thread類的setPriority(int newPriority)方法對其進行設置,該方法中的參數(shù)newPriority接收的是1~10之間的整數(shù)或者Thread類的三個靜態(tài)常量。
【例11-4】線程優(yōu)先級的設置
運行結果如下圖所示。
說明:
雖然Java提供了10個線程優(yōu)先級,但是這些優(yōu)先級需要操作系統(tǒng)的支持,不同的操作系統(tǒng)對優(yōu)先級的支持是不一樣的,不能很好地和Java中線程優(yōu)先級一一對應。
(2)?線程休眠
如果想要人為地控制線程執(zhí)行順序,使正在執(zhí)行的線程暫停,將CPU使用權讓給其他線程,這時可以使用靜態(tài)方法sleep(long millis)。該方法可以讓當前正在執(zhí)行的線程暫停一段時間,進入休眠等待狀態(tài),這樣其他的線程就可以得到執(zhí)行的機會。sleep(long millis)方法會聲明拋出InterruptedException異常,因此在調用該方法時應該捕獲異常,或者聲明拋出該異常。
【例11-5】線程休眠
運行結果如下圖所示。
5. 多線程同步
多線程的并發(fā)執(zhí)行可以提高程序的效率,但是,當多個線程去訪問同一個資源時,也會引發(fā)一些安全問題。如下例中的多線程售票程序。
【例11-6】多線程售票
運行結果如下圖所示。
由運行結果可以看到,同一張票被出售了多次,這種現(xiàn)象是不應該出現(xiàn)的。出現(xiàn)這種問題的原因在于多個線程同時處理共享資源所導致的。為了解決這樣的問題,只需要保證某個資源在同一時刻只能被一個線程訪問即可,也即線程的同步,Java中提供了幾種不同的線程同步機制。
(1)同步代碼塊
當多個線程使用同一個共享資源時,可以將處理共享資源的代碼放置在一個使用synchronized關鍵字修飾的代碼塊中,這段代碼塊就被稱為同步代碼塊。使用格式如下。
? ? ?synchronized(lock){
? ? ? ? ? ? ?// 操作共享資源代碼塊 ...
? ? ? ? ? ? }
述代碼中,lock是一個鎖對象,可以是任意類型的對象,但多個線程共享的鎖對象必須是相同的。鎖對象的創(chuàng)建代碼不能放到run()方法中,否則每個線程運行到run()方法都會創(chuàng)建一個新對象,這樣每個線程都會有一個不同的鎖。
【例11-7】利用同步代碼塊實現(xiàn)線程同步
運行結果如下圖所示。
同步代碼塊的原理:
①當線程執(zhí)行同步代碼塊時,首先會檢查lock鎖對象的標志位;
②默認情況下標志位為1,此時線程會執(zhí)行Synchronized同步代碼塊,同時將鎖對象的標志位置為0;
③當一個新的線程執(zhí)行到這段同步代碼塊時,由于鎖對象的標志位為0,新線程會發(fā)生阻塞,等待當前線程執(zhí)行完同步代碼塊后;
④鎖對象的標志位被置為1,新線程才能進入同步代碼塊執(zhí)行其中的代碼,這樣循環(huán)往復,直到共享資源被處理完為止。
(2)同步方法
當把共享資源的操作放在同步代碼塊中時,便為這些操作加了同步鎖。同樣,也可以在方法前面使用synchronized關鍵字來修飾,被修飾的方法稱為同步方法,可實現(xiàn)和同步代碼塊同樣的功能,同步方法使用格式如下所示。
[修飾符] synchronized 返回值類型方法名([參數(shù)1,……]){
//方法體
}
被synchronized修飾的方法在某一時刻只允許一個線程訪問,訪問該方法的其他線程都會發(fā)生阻塞,直到當前線程訪問完畢后,其他線程才有機會執(zhí)行。
【例11-8】使用同步方法實現(xiàn)線程同步
運行結果如下圖所示。
?