




版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
本章學習目標:●
掌握線程創建的過程●
掌握線程的生命周期●掌了解線程同步機制以及線程通信●
了解線程的優先級●
掌握線程的同步與死鎖第十二章多線程第1節part線程概述
線程(Thread)在多任務處理應用程序中起著至關重要的作用。之前所接觸的應用程序都是采用單線程處理模式。單線程在某些功能方面會受到限制,無法同時處理多個互不干擾的任務,只有一個順序執行流;而多線程是同時有多個線程并發執行,同時完成多個任務,具有多個順序執行流,且執行流之間互不干擾。Java語言多多線程提供了非常優秀的支持,在程序中可以通過簡便的方式創建多線程。線程概述本節概述
在操作系統中,每個獨立運行的程序就是一個進程(Process),當一個程序進入內存運行時,即變成一個進程。進程是操作系統進行資源分配和調度的一個獨立單位,是具有獨立功能且處于運行過程中的程序。在Windows操作系統中,右擊任務欄,選擇“啟動任務管理器”菜單命令,可以打開“Windows任務管理器”窗口,該窗口中的“進程”選項卡中顯示系統當前正在運行的進程,如圖12.1所示。12.1.1線程和進程線程和進程
進程具有如下三個特征:
(1)獨立性:進程是操作系統中獨立存在的實體,擁有自己獨立的資源,每個進行都擁有自己私有的地址空間,其他進程不可以直接訪問該地址空間,除非進程本身允許的情況下才能進行訪問。
(2)動態性:程序只是一個靜態的指令集合,只有當程序進入內存運行時,才變成一個進程。進程是一個正在內存中運行的、動態的指令集合,進程具有自己的生命周期和各種不同狀態。
(3)并發性:多個進程可以在單個處理器上并發執行,多個進程之間互不影響。12.1.1線程和進程
目前的操作系統都支持多線程的并發,但在具體的實現細節上會采用不同的策略。對于一個CPU而言,在某一時間點只能執行一個進程,CPU會不斷在多個進程之間來回輪換執行。并發性(concurrency)和并行性(parallel)是兩個相似但又不同的概念:并發是指多個事件在同一時間間隔內發生,其實質是在一個CPU上同時運行多個進程,CPU要在多個進程之間切換。并發不是真正的同時發生,而是對有限物理資源進行共享以便提高效率。并行是指多個事件在同一時刻發生,其實質是多個進程同一時刻可在不同的CPU上同時執行,每個CPU運行一個進程。12.1.1線程和進程
并發就像一個人喂兩個孩子吃飯,輪換著每人喂一口,表面上兩個孩子都在吃飯;而并行就是兩個人喂兩個孩子吃飯,兩個孩子也同時在吃飯。并發和并行之間的區別如圖12.2所示。12.1.1線程和進程
線程是進程的組成部分,一個線程必須在一個進程之內,而一個進程可以擁有多個線程,一個進程中至少有一個線程。線程是最小的處理單位,線程可以擁有自己的堆棧、計數器和局部變量,當不能擁有系統資源,多個線程共享其所在進程的系統資源。線程可以完成一定的任務,使用多線程可以在一個程序中同時完成多個任務,在更低的層次中引入多任務處理。
多線程在多CPU的計算機中可以實現真正物理上的同時執行;而對于單CPU的計算機實現的只是邏輯上的同時執行,在每個時刻,真正執行的只有一個線程,由操作系統進行線程管理調度,但由于CPU的速度很快,讓人感到像是多個線程在同時執行。12.1.1線程和進程
多線程擴展了多進程的概念,使得同一個進程可以同時并發處理多個任務。因此,線程也被稱作輕量級進程。多進程與多線程是多任務的兩種類型,兩者之間的主要區別如下:
(1)進程之間的數據塊是相互獨立的,彼此互不影響,進程之間需要通過信號、管道等進行交互。
(2)多線程之間的數據塊可以共享,一個進程中的多個線程可以共享程序段、數據段等資源。多線程比多進程更便于資源共享,同時Java提供的同步機制還可以解決線程之間的數據完整性問題,使得多線程設計更易發揮作用。多線程編程的優點如下:
(1)多線程之間共享內存,節約系統資源成本;
(2)充分利用CPU,執行并發任務效率高;
(3)Java內置多線程功能支持,簡化編程模型
(4)GUI應用通過啟動單獨線程收集用戶界面事件,簡化異步事件處理,使GUI界面的交互性更好。12.1.1線程和進程Java線程模型提供線程所必需的功能支持,基本的Java線程模型有Thread類、Runnable接口、Callable接口和Future接口等,這些線程模型都是面向對象的。Thread類將線程所必需的功能進行封裝,其常用的方法如表12-1所示。12.1.2Java線程模型Java線程模型Thread類的run()方法是線程中最重要的方法,該方法用于執行線程要完成的任務;當創建一個線程時,要完成自己的任務,則需要重寫run()方法。此外,Thread類還提供了start()方法,該方法用于負責線程的啟動;當調用start()方法成功地啟動線程后,系統會自動調用Thread類的run()方法來執行線程。因此,任何繼承Thread類的線程都可以通過start()方法來啟動。Runnable接口用于標識某個Java類可否作為線程類,該接口只有一個抽象方法run(),即線程中最重要的執行體,用于執行線程中所要完成的任務。Runnable接口定義在java.lang包中,定義代碼如下所示。packagejava.lang;publicinterfaceRunnable{ publicabstractvoidrun();}12.1.2Java線程模型Callable接口是Java5新增的接口,該接口中提供一個call()方法作為線程的執行體。call()方法比run()方法功能更強大,call()方法可以有返回值,也可以聲明拋出異常。Callable接口定義在java.util.concurrent包中,定義代碼如下所示。packagejava.util.concurrent;publicinterfaceCallable<V>{ Vcall()throwsException;}12.1.2Java線程模型Future接口用來接收Callable接口中call()方法的返回值。Future接口提供一些方法用于控制與其關聯的Callable任務。Future接口提供的方法如表12-2所示。12.1.2Java線程模型Callable接口有泛型限制,該接口中的泛型形參類型與call()方法返回值的類型相同;而且Callable接口是函數式接口,因此從Java8開始可以使用Lambda表達式創建Callable對象。
每個能夠獨立運行的程序就是一個進程,每個進程至少包含一個線程,即主線程。在Java語言中,每個能夠獨立運行的Java程序都至少有一個主線程,且在程序啟動時,JVM會自動創建一個主線程來執行該程序中的main()方法。因此,主線程有以下兩個特點:
(1)一個進程肯定包含一個主線程
(2)主線程用來執行main()方法
下述程序在main()方法中,調用Thread類的靜態方法currentThread()來獲取主線程,代碼如下所示。12.1.3主線程主線程【代碼12.1】MainThreadExample.javapackagecom;publicclassMainThreadExample{ publicstaticvoidmain(String[]args){ //調用Thread類的currentThread()獲取當前線程 Threadt=Thread.currentThread(); //設置線程名 t.setName("MyThread"); System.out.println("主線程是:"+t); System.out.println("線程名:"+t.getName()); System.out.println("線程ID:"+t.getId()); }}12.1.3主線程
上述代碼中,通過Thread.currentThread()靜態方法來獲取當前線程對象,由于是在main()方法中,所以獲取的線程是主線程。調用setName()方法可以設置線程名,調用getId()方法可以獲取線程的Id號,調用getName()方法可以獲取線程的名字。
程序運行結果如下:
主線程是:Thread[MyThread,5,main]
線程名:MyThread
線程ID:112.1.3主線程第2節part線程的創建和啟動
基于Java線程模型,創建線程的方式有三種:
(1)第一種方式是繼承Thread類,重寫Thread類中的run()方法,直接創建線程。
(2)第二種方式是實現Runnable接口,再通過Thread類和Runnable的實現類間接創建一個線程。
(3)第三種方式是使用Callable接口或Future接口間接創建線程。
上述三種方式從本質上是一致的,最終都是通過Thread類來建立線程。提供Runnable、Callable和Future接口模型是由于Java不支持多繼承,如果一個線程類繼承了Thread類,則不能再繼承其他的類,因此可以通過實現接口的方式間接創建線程。
采用Runnable、Callable和Future接口的方式創建線程時,線程類還可以繼承其他類,且多個線程之間可以共享一個target目標對象,適合多個相同線程處理同一個資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的數據模型。線程的啟動和創建本節概述12.2.1繼承Thread類
通過繼承Thread類來創建并啟動線程的步驟如下:
(1)定義一個子類繼承Thread類,并重寫run()方法。
(2)創建子類的實例,即實例化線程對象。
(3)調用線程對象的start()方法啟動該線程。Thread類的start()方法將調用run()方法,該方法用于啟動線程并運行。因此start()方法不能多次調用,當多次調用td.start()方法時會拋出一個IllegalThreadStateException異常。下述案例示例通過繼承Thread類來創建并啟動線程的步驟,代碼如下所示。繼承Thread類繼承Thread類【代碼12.2】ThreadExample.javapackagecom;//繼承Thread類publicclassThreadExampleextendsThread{ //重寫run()方法 publicvoidrun(){ for(inti=0;i<10;i++){ //繼承Thread類時,直接使用this即可獲取當前線程對象 //調用getName()方法返回當前線程的名字 System.out.println(this.getName()+":"+i); } }12.2.1繼承Thread類 publicstaticvoidmain(String[]args){ //創建線程對象 ThreadExampletd=newThreadExample(); //調用start()方法啟動線程 td.start(); //主線程任務 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()獲取主線程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.1繼承Thread類
因為線程在CPU中的執行是由操作系統所控制,執行次序是不確定的,除非使用同步機制強制按特定的順序執行,所以程序代碼運行的結果會因調度次序不同而不同。程序執行結果可能如下:main:1101Thread-0:1main:1102Thread-0:2main:1103……..
在創建td線程對象時并未指定該線程的名字,因此所輸出的線程名是系統的默認值“Thread-0”。對于輸出結果,不同機器所執行的結果可能不同,在同一機器上多次運行同一個程序也可能生成不同結果。12.2.112.2.2實現Runable接口
創建線程的第二種方式是實現Runnable接口。Runnable接口中只有一個run()方法,一個類實現Runnable接口后,并不代表該類是個“線程”類,不能直接啟動線程,必須通過Thread類的實例來創建并啟動線程。通過Runnable接口創建并啟動線程的步驟如下:
(1)定義一個類實現Runnable接口,并實現該接口中的run()方法;
(2)創建一個Thread類的實例,將Runnable接口的實現類所創建的對象作為參數傳入Thread類的構造方法中;
(3)調用Thread對象的start()方法啟動該線程。
下述案例示例通過實現Runnable接口創建并啟動線程的步驟,代碼如下所示。實現Runable接口實現Runable接口【代碼12.3】RunnableExamble.javapackagecom;//實現Runnable接口publicclassRunnableExambleimplementsRunnable{ //重寫run()方法 publicvoidrun(){ //獲取當前線程的名字 for(inti=0;i<10;i++) //實現Runnable接口時,只能使用Thread.currentThread()獲取當前線程對象 //再調用getName()方法返回當前線程的名字 System.out.println(Thread.currentThread().getName()+":"+i); }12.2.2實現Runable接口 publicstaticvoidmain(String[]args){ //創建一個Thread類的實例,其參數是RunnableExamble類的對象 Threadtd=newThread(newRunnableExamble()); //調用start()方法啟動線程 td.start(); //主線程任務 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()獲取主線程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.2實現Runable接口
上述代碼定義了一個RunnableExamble類,該類實現了Runnable接口,并實現run()方法,這樣的類可以稱為線程任務類。直接調用Thread類或Runnable接口所創建的對象的run()方法是無法啟動線程的,必須通過Thread的start()方法才能啟動線程。程序執行的結果可能如下:main:1100Thread-0:0Thread-0:1Thread-0:2……..12.2.212.2.3使用Callable和Future接口
創建線程的第三種方式是使用Callable和Future接口。Callable接口提供一個call()方法作為線程的執行體,該方法的返回值使用Future接口來代表。從Java5開始,為Future接口提供一個FutureTask實現類,該類同時實現了Future和Runnable兩個接口,因此可以作為Thread類的target參數。使用Callable和Future接口的最大優勢在于可以在線程執行完成之后獲得執行結果。
使用Callable和Future接口創建并啟動線程的步驟如下:
(1)創建Callable接口的實現類,并實現call()方法,該方法將作為線程的執行體,并具有返回值;然后創建Callable實現類的實例。
(2)使用FutureTask類來包裝Callable對象,在FutureTask對象中封裝了Callable對象的call()方法的返回值。
(3)使用FutureTask對象作為Thread對象的target,創建并啟動新線程。
(4)調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值。
下述案例示例通過Callable和Future接口創建并啟動線程的步驟,代碼如下所示。使用Callable和Future接口使用Callable和Future接口【代碼12.4】CallableFutureExample.javapackagecom;importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;//創建Callable接口的實現類classTaskimplementsCallable<Integer>{ //實現call()方法,作為線程執行體 publicIntegercall()throwsException{ inti=0; for(;i<10;i++) System.out.println(Thread.currentThread().getName()+":"+i); //call()方法可以有返回值 returni; }}12.2.3使用Callable和Future接口publicclassCallableFutureExample{ publicstaticvoidmain(String[]args){ //使用FutureTask類包裝Callable實現類的實例 FutureTask<Integer>task=newFutureTask<>(newTask()); //創建線程,使用FutureTask對象task作為Thread對象的target, //并調用start()方法啟動線程 Threadtd=newThread(task,"子線程"); td.start(); //調用FutureTask對象task的get()方法獲取子線程執行結束后的返回值 try{ System.out.println("子線程返回值:"+task.get()); }catch(Exceptione){ e.printStackTrace(); } //主線程任務 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()獲取主線程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.3使用Callable和Future接口
上述代碼先定義一個Task類,該類實現Callable接口并重寫call()方法,call()的返回值為整型,因此Callable接口中對應的泛型限制為Integer,即Callable<Integer>。在main()方法中,先創建FutureTask<Integer>類的對象task,該對象包裝Task類;再創建Thread對象并啟動線程;最后調用FutureTask對象task的get()方法獲取子線程執行結束后的返回值。整個程序所實現的功能與前兩種方式一樣,只是增加了子線程返回值。
程序運行結果如下:
子線程:0
子線程:1
子線程:2……..
子線程返回值:10main:1100main:1101……12.2.3使用Callable和Future接口
從Java8開始,可以直接使用Lambda表達式創建Callable對象,下述案例示例通過Lambda表達式創建Callable對象,代碼如下所示。【代碼12.5】LambdaCallableExample.javapackagecom;importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;publicclassLambdaCallableExample{publicstaticvoidmain(String[]args){ //使用Lambda表達式創建Callable<Integer>對象 //使用FutureTask類包裝Callable對象 FutureTask<Integer>task=newFutureTask<>(
(Callable<Integer>)()->{ inti=0; for(;i<10;i++) System.out.println(Thread.currentThread().getName()+":"+i); //call()方法可以有返回值 returni; });12.2.3使用Callable和Future接口 //創建線程,使用FutureTask對象task作為Thread對象的target, //并調用start()方法啟動線程 Threadtd=newThread(task,"子線程"); td.start(); //調用FutureTask對象task的get()方法獲取子線程執行結束后的返回值 try{ System.out.println("子線程返回值:"+task.get()); }catch(Exceptione){ e.printStackTrace(); } //主線程任務 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()獲取主線程名字 System.out.println(Thread.currentThread().getName()+":"+i);}}
上述代碼加粗部分就是Lambda表達式,可以直接使用Lambda表達式創建Callable對象,而無須先創建Callable實現類,但Lambda表達式必須在jdk1.8版本后才可以運行。
在JavaAPI中,定義的FutureTask類實際上直接實現RunnableFuture接口,而RunnableFuture接口繼承Runnable和Future兩個接口,因此FutureTask類即實現了Runnable接口,又實現了Future接口。12.2.3第3節part線程的生命周期
線程具有生命周期,當線程被創建并啟動后,不會立即進入執行狀態,也不會一直處于執行狀態。在線程的生命周期中,要經過5種狀態:新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)。線程狀態之間的轉換如圖12-3所示。線程的生命周期本節介紹12.3.1新建和就緒狀態
當程序使用new關鍵字創建一個線程之后,該線程就處于新建狀態,此時與其他Java對象一樣,僅由JVM為其分配內存并初始化。新建狀態的線程沒有表現出任何動態特征,程序也不會執行線程的執行體。當線程對象調用start()方法之后,線程就處于就緒狀態,相當于“等待執行”。此時,調度程序就可以把CPU分配給該線程,JVM會為線程創建方法調用棧和程序計數器。處于就緒狀態的線程并沒有開始運行,只是表示該線程準備就緒等待執行。
注意只能對新建狀態的線程調用start()方法,即new完一個線程后,只能調用一次start()方法,否則將引發IllegalThreadStateException異常。
下述案例示例了新建線程重復調用start()方法引發異常,代碼如下所示。新建和就緒狀態新建和就緒狀態【代碼12.6】IllegalThreadExample.javapackagecom;publicclassIllegalThreadExample{ publicstaticvoidmain(String[]args){ //創建線程 Threadt=newThread(newRunnable(){ publicvoidrun(){ for(inti=0;i<10;i++) System.out.print(i+""); } }); t.start(); t.start(); }}
上述代碼三次調用start()方法,多次啟動線程,因此會引發IllegalThreadStateException異常。運行結果可能如下所示:Exceptioninthread"main"java.lang.IllegalThreadStateException0123456789 atjava.lang.Thread.start(UnknownSource) atcom.IllegalThreadExample.main(IllegalThreadExample.java:12)12.3.112.3.2運行和阻塞狀態
處于就緒狀態的線程獲得CPU后,開始執行run()方法的線程執行體,此時該線程處于運行狀態。如果計算機的CPU是單核的,則在任何時刻只有一個線程處于運行狀態。一個線程開始運行后,不可能一直處于運行狀態。線程在運行過程中需要被中斷,目的是使其他線程獲得執行的機會,線程調度的細節取決于底層平臺所采用的策略。
目前UNIX系統采用的是時間片算法策略,Windows系統采用的則是搶占式策略,另外一種小型設備(手機)則可能采用協作式調度策略。對于采用搶占式策略的系統而言,系統會給每個可執行的線程一個小時間段來處理任務;當該時間段用完后,系統就會剝奪該線程所占用的資源,讓其他線程獲得執行的機會。在選擇下一個線程時,系統會考慮線程的優先級。運行和阻塞狀態運行和阻塞狀態當線程出現以下情況時,會進入阻塞狀態:(1)調用sleep()方法,主動放棄所占用的處理器資源;(2)調用了一個阻塞式IO方法,在該方法返回之前,該線程被阻塞;(3)線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有;(4)執行條件還未滿足,調用wait()方法使線程進入等待狀態,等待其他線程的通知;(5)程序調用了線程的suspend()方法將該線程掛起,但該方法容易導致死鎖,因此應該盡量避免使用。12.3.2運行和阻塞狀態
正在執行的線程被阻塞之后,其他線程就可以獲得執行的機會,被阻塞的線程會在合適的時機重新進入就緒狀態,等待線程調度器再次調度。
當線程出現如下幾種情況時,線程可以解除阻塞進入就緒狀態:
(1)調用sleep()方法的線程經過了指定的時間;
(2)線程調用的阻塞式IO方法以經返回;
(3)線程成功地獲得了同步監視器;
(4)線程處于等待狀態,其他線程調用notify()或notifyAll()方法發出了一個通知時,則線程回到就緒狀態;
(5)處于掛起狀態的線程被調用了resume()恢復方法。12.3.2運行和阻塞狀態
在線程運行的過程中,可以通過sleep()方法使線程暫時停止運行,進入休眠狀態。在使用sleep()方法時需要注意以下兩點:
(1)sleep()方法的參數是以毫秒為基本單位,例如sleep(2000)則休眠2秒鐘;
(2)sleep()方法聲明了InterruptedException異常,因此調用sleep()方法時要么放在try…catch語句中捕獲該異常并處理,要么在方法后使用throws顯式聲明拋出該異常。
可以通過Thread類的isAlive()方法來判斷線程是否處于運行狀態。當線程處于就緒、運行和阻塞三種狀態時,isAlive()方法的返回值為true;當線程處于新建、死亡兩種狀態時,isAlive()方法的返回值為false。
下述案例示例了線程的創建、運行和死亡三個狀態,代碼如下所示。12.3.2運行和阻塞狀態【代碼12.7】ThreadLifeExample.javapackagecom;publicclassThreadLifeExampleextendsThread{ publicvoidrun(){ intsum=0; for(inti=0;i<=100;i++) sum+=i; System.out.println("sum="+sum); } publicstaticvoidmain(String[]args)throwsInterruptedException{ ThreadLifeExampletle=newThreadLifeExample(); System.out.println("新建狀態isAlive():"+tle.isAlive()); tle.start(); System.out.println("運行狀態isAlive():"+tle.isAlive()); Thread.sleep(2000); System.out.println("線程結束isAlive():"+tle.isAlive()); }}12.3.2運行和阻塞狀態
程序運行結果如下:
新建狀態isAlive():false
運行狀態isAlive():truesum=5050
線程結束isAlive():false
注意:線程調用wait()方法進入等待狀態后,需其他線程調用notify()或notifyAll()方法發出通知才能進入就緒狀態。使用suspend()和resume()方法可以掛起和喚醒線程,但這兩個方法可能會導致不安全因素。如果對某個線程調用interrupt()方法發出中斷請求,則該線程會根據線程狀態拋出InterruptedException異常,對異常進行處理時可以再次調度該線程。12.3.2死亡狀態線程結束后就處于死亡狀態,結束線程有以下三種方式:(1)線程執行完成run()或call()方法,線程正常結束;(2)線程拋出一個未捕獲的Exception或Error;(3)調用stop()方法直接停止線程,該方法容易導致死鎖,通常不推薦使用。死亡狀態12.3.2死亡狀態
主線程結束時,其他子線程不受任何影響,并不會隨主線程的結束而結束。一旦子線程啟動起來,子線程就擁有和主線程相同的地位,子線程不會受主線程的影響。
為了測試某個線程是否死亡,可以通過線程對象的isAlive()方法來獲得線程狀態,當方法返回值為false時,線程處于死亡或新建狀態。不要試圖對一個已經死亡的線程調用start()方法使其重新啟動,線程死亡就是死亡,該線程不可再次作為線程執行。Thread類中的join()方法可以讓一個線程等待另一個線程完成后,繼續執行原線程中的任務。當在某個程序執行流中調用其他線程的join()方法時,當前線程將被阻塞,直到另一個線程執行完為止。join()方法通常由使用線程的程序調用,當其他線程都執行結束后,再調用主線程進一步操作。
下述案例示例了join()方法的使用,代碼如下所示。12.3.2死亡狀態【代碼12.8】JoinExample.javapackagecom;classJoinThreadextendsThread{ publicJoinThread(){ super(); } publicJoinThread(Stringstr){ super(str); } publicvoidrun(){ for(inti=0;i<10;i++) System.out.println(this.getName()+":"+i); }}12.3.2死亡狀態publicclassJoinExample{ publicstaticvoidmain(String[]args){ //創建子線程 JoinThreadt1=newJoinThread("被Join的子線程"); //啟動子線程 t1.start(); //等待子線程執行完畢 try{ t1.join(); }catch(InterruptedExceptione){ //TODOAuto-generatedcatchblock e.printStackTrace(); } //輸出主線程名 System.out.println("主線程名為:"+Thread.currentThread().getName()); //子線程已經處于死亡狀態,其isAlive()方法返回值為false System.out.println("子線程死亡狀態isAlive():"+t1.isAlive()); //再次啟動子線程,拋出異常 t1.start(); }}12.3.2死亡狀態
上述代碼中開始調用了線程的join()方法,最后又對死亡狀態的線程再次調用start()方法,運行結果如下所示:…..
被Join的子線程:8
被Join的子線程:9
主線程名為:main
子線程死亡狀態isAlive():falseExceptioninthread"main"java.lang.IllegalThreadStateException atjava.lang.Thread.start(UnknownSource) atcom.JoinExample.main(JoinExample.java:33)
在上述代碼中,注銷掉join()方法的調用和對死亡狀態線程的start()方法的再次調用,運行結果可能如下:
主線程名為:main
被Join的子線程:0
被Join的子線程:1
子線程死亡狀態isAlive():true
被Join的子線程:2
被Join的子線程:3……12.3.2第4節part線程的優先級
每個線程執行時都具有一定的優先級,線程的優先級代表該線程的重要程度。當有多個線程同時處于可執行狀態并等待獲得CPU處理器時,系統將根據各個線程的優先級來調度各線程,優先級越高的線程獲得CPU時間的機會越多,而優先級低的線程則獲得較少的執行機會。
每個線程都有默認的優先級,其優先級都與創建該線程的父線程的優先級相同。在默認情況下,主線程具有普通優先級,由主線程創建的子線程也具有普通優先級。Thread類提供三個靜態常量來標識線程的優先級:
(1)MAX_PRIORITY:最高優先級,其值為10;
(2)NORM_PRIORITY:普通優先級,其值為5;
(3)MIN_PRIORITY:最低優先級,其值為1。線程的優先級線程的優先級Thread類提供了setPriority()方法來對線程的優先級進行設置,而getPriority()方法來獲取線程的優先級。setPriority()方法的參數是一個整數(1~10),也可以使用Thread類提供的三個優先級靜態常量。
線程的優先級高度依賴于操作系統,并不是所有的操作系統都支持Java的10個優先級,例如Windows2000僅提供7個優先級。因此,盡量避免直接使用整數給線程指定優先級,提倡使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三個優先級靜態常量。另外,優先級并不能保證線程的執行次序,因此應避免使用線程優先級作為構建任務執行順序的標準。
下述案例示例了線程優先級的設置及使用,代碼如下所示。線程的優先級【代碼12.9】PriorityExample.javapackagecom;classMyPriorityThreadextendsThread{ publicMyPriorityThread(){ super(); } publicMyPriorityThread(Stringname){ super(name); } publicvoidrun(){ for(inti=0;i<10;i++){ System.out.println(this.getName()+",其優先級是:"+this.getPriority() +",循環變量的值為:"+i); } }}線程的優先級publicclassPriorityExample{ publicstaticvoidmain(String[]args){ //輸出主線程的優先級 System.out.println("主線程的優先級:"+Thread.currentThread().getPriority()); //創建子線程,并設置不同優先級 MyPriorityThreadt1=newMyPriorityThread("高級"); t1.setPriority(Thread.MAX_PRIORITY); MyPriorityThreadt2=newMyPriorityThread("普通"); t2.setPriority(Thread.NORM_PRIORITY); MyPriorityThreadt3=newMyPriorityThread("低級"); t3.setPriority(Thread.MIN_PRIORITY); MyPriorityThreadt4=newMyPriorityThread("指定值級"); t4.setPriority(4); //啟動所有子線程 t1.start(); t2.start(); t3.start(); t4.start(); }}線程的優先級程序運行執行結果可能如下:主線程的優先級:5普通,其優先級是:5,循環變量的值為:0高級,其優先級是:10,循環變量的值為:0高級,其優先級是:10,循環變量的值為:1高級,其優先級是:10,循環變量的值為:2高級,其優先級是:10,循環變量的值為:3普通,其優先級是:5,循環變量的值為:1高級,其優先級是:10,循環變量的值為:4指定值,其優先級是:4,循環變量的值為:0…….通過運行結果可以看出,優先級越高的線程提前獲得執行機會就越多。
線程的優先級第5節part線程的同步
多線程訪問同一資源數據時,很容易出現線程安全問題。以多窗口出售車票為例,一旦多線程并發訪問,就可能出現問題,造成一票多售的現象。在Java中,提供了線程同步的概念以保證某個資源在某一時刻只能由一個線程訪問,以此保證共享數據的一致性。Java使用監控器(也稱對象鎖)實現同步。每個對象都有一個監控器,使用監控器可以保證一次只允許一個線程執行對象的同步語句。即在對象的同步語句執行完畢前,其他試圖執行當前對象的同步語句的線程都將處于阻塞狀態,只有線程在當前對象的同步語句執行完畢后,監控器才會釋放對象鎖,并讓優先級最高的阻塞線程處理同步語句。
線程同步通常采用三種方式:同步代碼塊、同步方法和同步鎖。線程的同步本節介紹12.5.1同步代碼塊
使用同步代碼塊實現同步功能,只需將對實例的訪問語句放入一個同步塊中,其語法格式如下:synchronized(object){ //需要同步的代碼塊}
其中:synchronized是同步關鍵字;object是同步監視器,其數據類型不能是基本數據類型。線程開始執行同步代碼之前,必須先獲得同步監視器的鎖定,并且,任何時刻只能有一個線程可以獲得對同步監視器的鎖定,當同步代碼塊執行完成后,該線程會釋放對該同步監視器的鎖定。
下述案例示例了同步代碼塊的聲明和使用,代碼如下所示。同步代碼塊同步代碼塊【代碼12.10】SynBlockExample.javapackagecom;//銀行帳戶類classBankAccount{ //銀行賬號 privateStringbankNo; //銀行余額 privatedoublebalance; //構造方法 publicBankAccount(StringbankNo,doublebalance){ this.bankNo=bankNo; this.balance=balance; } publicStringgetBankNo(){ returnbankNo; } publicvoidsetBankNo(StringbankNo){ this.bankNo=bankNo; } publicdoublegetBalance(){ returnbalance; } publicvoidsetBalance(doublebalance){ this.balance=balance; }}12.5.1同步代碼塊
publicclassSynBlockExampleextendsThread{ //銀行賬戶 privateBankAccountaccount; //操作金額,正數為存錢,負數為取錢 privatedoublemoney; publicSynBlockExample(Stringname,BankAccountaccount,doublemoney){ super(name); this.account=account; this.money=money; } //線程任務 publicvoidrun(){ synchronized(this.account){ //獲取目賬戶的金額 doubled=this.account.getBalance(); //如果操作的金額money<0,則代表取錢操作,//同時判斷賬戶金額是否低于取錢金額 if(money<0&&d<-money){ System.out.println(this.getName()+"操作失敗,余額不足!"); return; }else{12.5.1同步代碼塊 //對賬戶金額進行操作 d+=money; System.out.println(this.getName()+"操作成功,目前賬戶余額為:"+d); try{ //休眠10毫秒 Thread.sleep(10); }catch(InterruptedExceptione){ e.printStackTrace(); } //修改賬戶金額 this.account.setBalance(d);
} } }
publicstaticvoidmain(String[]args){ //創建一個銀行賬戶實例 BankAccountmyAccount=newBankAccount("101",5000); 12.5.1同步代碼塊 //創建多個線程,對賬戶進行存取錢操作 SynBlockExamplet1=newSynBlockExample("T1",myAccount,-3000); SynBlockExamplet2=newSynBlockExample("T2",myAccount,-3000); SynBlockExamplet3=newSynBlockExample("T3",myAccount,1000); //啟動線程 t1.start(); t2.start(); t3.start(); //等待所有子線程完成 try{ t1.join(); t2.join(); t3.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } //輸出賬戶信息 System.out.println("賬號:"+myAccount.getBankNo()+",余額:" +myAccount.getBalance()); }}12.5.1同步代碼塊
上述代碼在run()方法中,使用“synchronized(this.account){}”對銀行賬戶的操作代碼進行同步,保證某一時刻只能有一個線程訪問該賬戶,只有{}里面的代碼執行完畢,才釋放對該賬戶的鎖定。
程序運行結果如下所示:T1操作成功,目前賬戶余額為:2000.0T2操作失敗,余額不足!T3操作成功,目前賬戶余額為:3000.0
賬號:101,余額:3000.012.5.112.5.2同步方法
同步方法是使用synchronized關鍵字修飾的方法,其聲明的語法格式如下:[訪問修飾符]synchronized返回類型方法名([參數列表]){ //方法體}
其中:synchronized關鍵字修飾的實例方法無須顯式地指定同步監視器,同步方法的同步監視器是this,即該方法所屬的對象。一旦一個線程進入一個實例的任何同步方法,其他線程將不能進入該實例的所有同步方法,但該實例的非同步方法仍然能夠被調用。
使用同步方法可以非常方便地實現線程安全,一個具有同步方法的類被稱為“線程安全的類”,該類的對象可以被多個線程安全地訪問,且每個線程調用該對象的方法后都將得到正確的結果。下述案例示例了同步方法的聲明和使用,代碼如下所示。同步方法同步方法【代碼12.11】SynMethodExample.javapackagecom;//增加有同步方法的銀行帳戶類classSynMethod{ //銀行賬號 privateStringbankNo; //銀行余額 privatedoublebalance; //構造方法 publicSynMethod(StringbankNo,doublebalance){ this.bankNo=bankNo; this.balance=balance; } //同步方法,存取錢操作 publicsynchronizedvoidaccess(doublemoney){ //如果操作的金額money<0,則代表取錢操作,//同時判斷賬戶金額是否低于取錢金額 if(money<0&&balance<-money){ System.out.println(Thread.currentThread().getName()+"操作失敗,余額不足!"); return;//返回 }else{12.5.2同步方法 //對賬戶金額進行操作 balance+=money; System.out.println(Thread.currentThread().getName() +"操作成功,目前賬戶余額為:"+balance); try{ //休眠1毫秒 Thread.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); } } } publicStringgetBankNo(){ returnbankNo; } publicdoublegetBalance(){ returnbalance; }}12.5.2同步方法publicclassSynMethodExampleextendsThread{ //銀行賬戶 privateSynMethodaccount; //操作金額,正數為存錢,負數為取錢 privatedoublemoney; publicSynMethodExample(Stringname,SynMethodaccount,doublemoney){ super(name); this.account=account; this.money=money; } //線程任務 publicvoidrun(){ //調用account對象的同步方法 this.account.access(money); } publicstaticvoidmain(String[]args){ //創建一個銀行賬戶實例 SynMethodmyAccount=newSynMethod("1001",5000); //創建多個線程,對賬戶進行存取錢操作 SynMethodExamplet1=newSynMethodExample("T1",myAccount,-3000); 12.5.2同步方法 SynMethodExamplet2=newSynMethodExample("T2",myAccount,-3000); SynMethodExamplet3=newSynMethodExample("T3",myAccount,1000); //啟動線程 t1.start(); t2.start(); t3.start();
//等待所有子線程完成 try{ t1.join(); t2.join(); t3.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } //輸出賬戶信息 System.out.println("賬號:"+myAccount.getBankNo()+",余額:" +myAccount.getBalance()); }}12.5.2同步方法
程序運行結果如下:T1操作成功,目前賬戶余額為:2000.0T2操作失敗,余額不足!T3操作成功,目前賬戶余額為:3000.0
賬號:1001,余額:3000.0
注意:synchronized鎖定的是對象,而不是方法或代碼塊;synchronized也可以修飾類,當用synchronized修飾類時,表示這個類的所有方法都是synchronized的。12.5.212.5.3同步鎖
同步鎖Lock是一種更強大的線程同步機制,通過顯式定義同步鎖對象來實現線程同步。同步鎖提供了比同步代碼塊、同步方法更廣泛的鎖定操作,實現更靈活。Lock是控制多個線程對共享資源進行訪問的工具,能夠對共享資源進行獨占訪問。每次只能有一個線程對Lock對象加鎖,線程訪問共享資源之前需要先獲得Lock對象。某些鎖可能允許對共享資源并發訪問,如ReadWriteLock(讀寫鎖)。Lock和ReadWriteLock是Java5提供的關于鎖的兩個根接口,并為Lock提供了ReentrantLock(可重入鎖)實現類,為ReadWriteLock提供了ReentrantReadWriteLock實現類。從Java8開始,又新增了StampedeLock類,可以替代傳統的ReentrantReadWriteLock類。同步鎖同步鎖ReentrantLock類是常用的可重入同步鎖,該類對象可以顯式地加鎖、釋放鎖。使用ReentrantLock類的步驟如下:
(1)定義一個ReentrantLock鎖對象,該對象是final常量;privatefinalReentrantLocklock=newReentrantLock();
(2)在需要保證線程安全的代碼之前增加“加鎖”操作;lock.lock();
(3)在執行完線程安全的代碼后“釋放鎖”。lock.unlock();12.5.3同步鎖下述代碼示例了使用ReentrantLock鎖的基本步驟://1.定義鎖對象privatefinalReentrantLocklock=newReentrantLock();...//定義需要保證線程安全的方法publicvoidmyMethod(){//2.加鎖lock.lock();try{//需要保證線程安全的代碼...}finally{//3.釋放鎖lock.unlock();}}
其中:加鎖和釋放鎖都需要放在線程安全的方法中;lock.unlock()放在finally語句中,不管發生異常與否,都需要釋放鎖。12.5.3同步鎖下述案例示例了ReentrantLock同步鎖的使用,代碼如下所示。【代碼12.12】SynLockExample.javapackagecom;importjava.util.concurrent.locks.ReentrantLock;classSynLock{ privateStringbankNo; //銀行賬號 privatedoublebalance;//銀行余額 //定義鎖對象 privatefinalReentrantLocklock=newReentrantLock(); //構造方法 publicSynLock(StringbankNo,doublebalance){ this.bankNo=bankNo; this.balance=balance; } //存取錢操作 publicvoidaccess(doublemoney){ //加鎖 lock.lock(); try{ //如果操作的金額money<0,則代表取錢操作,12.5.3同步鎖 //同時判斷賬戶金額是否低于取錢金額 if(money<0&&balance<-money){ System.out.println(Thread.currentThread().getName() +"操作失敗,余額不足!"); //返回 return; }else{ //對賬戶金額進行操作 balance+=money; System.out.println(Thread.currentThread().getName() +"操作成功,目前賬戶余額為:"+balance); try{ //休眠1毫秒 Thread.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); } } }finally{ 12.5.3同步鎖 //釋放鎖 lock.unlock(); } } publicStringgetBankNo(){ returnbankNo; } publicdoublegetBalance(){ returnbalance; }}12.5.3同步鎖//使用同步鎖的類publicclassSynLockExampleextendsThread{ //銀行賬戶 privateSynLockaccount; //操作金額,正數為存錢,負數為取錢 privatedoublemoney; publicSynLockExample(Stringname,SynLockaccount,doublemoney){ super(name); this.account=account; this.money=money; } //線程任務 publicvoidrun(){ //調用account對象的access()方法 this.account.access(money); } publicstaticvoidmain(String[]args){ //創建一個銀行賬戶實例 SynLockmyAccount=newSynLock("1001",5000); //創建多個線程,對賬戶進行存取錢操作12.5.3同步鎖 SynLockExamplet1=newSynLockExample("T1",myAccount,-3000); SynLockExamplet2=newSynLockExample("T2",myAccount,-3000); SynLockExamplet3=newSynLockExample("T3",myAccount,1000); //啟動線程 t1.start(); t2.start(); t3.start(); //等待所有子線程完成 try{ t1.join(); t2.join(); t3.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } //輸出賬戶信息 System.out.println("賬號:"+myAccount.getBankNo()+",余額:" +myAccount.getBalance()); }}12.5.3同步鎖程序運行結果如下:T1操作成功,目前賬戶余額為:2000.0T2操作失
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 物聯網支持的裝載作業路徑規劃-洞察闡釋
- 水利工程大數據分析-洞察闡釋
- 測繪安全知識培訓
- 實施手術安全核查的內容及流程
- 安全生產隱患自查自糾情況
- 邊界計算驅動的桌面虛擬化-洞察闡釋
- 消防安全工作總結報告簡短
- 語義表示的魯棒性分析-洞察闡釋
- 潛能無限教育心理的探索與實踐
- 智慧物流平臺下的供應鏈協同研究-洞察闡釋
- 2025年6月22日四川省市直事業單位遴選筆試真題及答案解析
- 慶陽市隴東學院招聘事業編制筆試真題2024
- 心理學考試題及答案
- 護理領域的職業發展與前景展望
- 2025年天津高考數學試卷試題真題及答案詳解(精校打印)
- 2025上海濟光職業技術學院輔導員考試試題及答案
- 2024年江蘇三支一扶真題
- 主、被動防護網施工方案-圖文
- 2025年初中語文文學常識:常考100題匯編
- 君易和文化課件
- 藥食同源106種25年4月更新
評論
0/150
提交評論