![《Java程序設計教程》課件第十二章:多線程_第1頁](http://file4.renrendoc.com/view4/M00/33/11/wKhkGGZ6lTuAbIW6AAHvvkbqKl0062.jpg)
![《Java程序設計教程》課件第十二章:多線程_第2頁](http://file4.renrendoc.com/view4/M00/33/11/wKhkGGZ6lTuAbIW6AAHvvkbqKl00622.jpg)
![《Java程序設計教程》課件第十二章:多線程_第3頁](http://file4.renrendoc.com/view4/M00/33/11/wKhkGGZ6lTuAbIW6AAHvvkbqKl00623.jpg)
![《Java程序設計教程》課件第十二章:多線程_第4頁](http://file4.renrendoc.com/view4/M00/33/11/wKhkGGZ6lTuAbIW6AAHvvkbqKl00624.jpg)
![《Java程序設計教程》課件第十二章:多線程_第5頁](http://file4.renrendoc.com/view4/M00/33/11/wKhkGGZ6lTuAbIW6AAHvvkbqKl00625.jpg)
版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領
文檔簡介
本章學習目標:●
掌握線程創(chuàng)建的過程●
掌握線程的生命周期●掌了解線程同步機制以及線程通信●
了解線程的優(yōu)先級●
掌握線程的同步與死鎖第十二章多線程第1節(jié)part線程概述
線程(Thread)在多任務處理應用程序中起著至關重要的作用。之前所接觸的應用程序都是采用單線程處理模式。單線程在某些功能方面會受到限制,無法同時處理多個互不干擾的任務,只有一個順序執(zhí)行流;而多線程是同時有多個線程并發(fā)執(zhí)行,同時完成多個任務,具有多個順序執(zhí)行流,且執(zhí)行流之間互不干擾。Java語言多多線程提供了非常優(yōu)秀的支持,在程序中可以通過簡便的方式創(chuàng)建多線程。線程概述本節(jié)概述
在操作系統(tǒng)中,每個獨立運行的程序就是一個進程(Process),當一個程序進入內(nèi)存運行時,即變成一個進程。進程是操作系統(tǒng)進行資源分配和調(diào)度的一個獨立單位,是具有獨立功能且處于運行過程中的程序。在Windows操作系統(tǒng)中,右擊任務欄,選擇“啟動任務管理器”菜單命令,可以打開“Windows任務管理器”窗口,該窗口中的“進程”選項卡中顯示系統(tǒng)當前正在運行的進程,如圖12.1所示。12.1.1線程和進程線程和進程
進程具有如下三個特征:
(1)獨立性:進程是操作系統(tǒng)中獨立存在的實體,擁有自己獨立的資源,每個進行都擁有自己私有的地址空間,其他進程不可以直接訪問該地址空間,除非進程本身允許的情況下才能進行訪問。
(2)動態(tài)性:程序只是一個靜態(tài)的指令集合,只有當程序進入內(nèi)存運行時,才變成一個進程。進程是一個正在內(nèi)存中運行的、動態(tài)的指令集合,進程具有自己的生命周期和各種不同狀態(tài)。
(3)并發(fā)性:多個進程可以在單個處理器上并發(fā)執(zhí)行,多個進程之間互不影響。12.1.1線程和進程
目前的操作系統(tǒng)都支持多線程的并發(fā),但在具體的實現(xiàn)細節(jié)上會采用不同的策略。對于一個CPU而言,在某一時間點只能執(zhí)行一個進程,CPU會不斷在多個進程之間來回輪換執(zhí)行。并發(fā)性(concurrency)和并行性(parallel)是兩個相似但又不同的概念:并發(fā)是指多個事件在同一時間間隔內(nèi)發(fā)生,其實質是在一個CPU上同時運行多個進程,CPU要在多個進程之間切換。并發(fā)不是真正的同時發(fā)生,而是對有限物理資源進行共享以便提高效率。并行是指多個事件在同一時刻發(fā)生,其實質是多個進程同一時刻可在不同的CPU上同時執(zhí)行,每個CPU運行一個進程。12.1.1線程和進程
并發(fā)就像一個人喂兩個孩子吃飯,輪換著每人喂一口,表面上兩個孩子都在吃飯;而并行就是兩個人喂兩個孩子吃飯,兩個孩子也同時在吃飯。并發(fā)和并行之間的區(qū)別如圖12.2所示。12.1.1線程和進程
線程是進程的組成部分,一個線程必須在一個進程之內(nèi),而一個進程可以擁有多個線程,一個進程中至少有一個線程。線程是最小的處理單位,線程可以擁有自己的堆棧、計數(shù)器和局部變量,當不能擁有系統(tǒng)資源,多個線程共享其所在進程的系統(tǒng)資源。線程可以完成一定的任務,使用多線程可以在一個程序中同時完成多個任務,在更低的層次中引入多任務處理。
多線程在多CPU的計算機中可以實現(xiàn)真正物理上的同時執(zhí)行;而對于單CPU的計算機實現(xiàn)的只是邏輯上的同時執(zhí)行,在每個時刻,真正執(zhí)行的只有一個線程,由操作系統(tǒng)進行線程管理調(diào)度,但由于CPU的速度很快,讓人感到像是多個線程在同時執(zhí)行。12.1.1線程和進程
多線程擴展了多進程的概念,使得同一個進程可以同時并發(fā)處理多個任務。因此,線程也被稱作輕量級進程。多進程與多線程是多任務的兩種類型,兩者之間的主要區(qū)別如下:
(1)進程之間的數(shù)據(jù)塊是相互獨立的,彼此互不影響,進程之間需要通過信號、管道等進行交互。
(2)多線程之間的數(shù)據(jù)塊可以共享,一個進程中的多個線程可以共享程序段、數(shù)據(jù)段等資源。多線程比多進程更便于資源共享,同時Java提供的同步機制還可以解決線程之間的數(shù)據(jù)完整性問題,使得多線程設計更易發(fā)揮作用。多線程編程的優(yōu)點如下:
(1)多線程之間共享內(nèi)存,節(jié)約系統(tǒng)資源成本;
(2)充分利用CPU,執(zhí)行并發(fā)任務效率高;
(3)Java內(nèi)置多線程功能支持,簡化編程模型
(4)GUI應用通過啟動單獨線程收集用戶界面事件,簡化異步事件處理,使GUI界面的交互性更好。12.1.1線程和進程Java線程模型提供線程所必需的功能支持,基本的Java線程模型有Thread類、Runnable接口、Callable接口和Future接口等,這些線程模型都是面向對象的。Thread類將線程所必需的功能進行封裝,其常用的方法如表12-1所示。12.1.2Java線程模型Java線程模型Thread類的run()方法是線程中最重要的方法,該方法用于執(zhí)行線程要完成的任務;當創(chuàng)建一個線程時,要完成自己的任務,則需要重寫run()方法。此外,Thread類還提供了start()方法,該方法用于負責線程的啟動;當調(diào)用start()方法成功地啟動線程后,系統(tǒng)會自動調(diào)用Thread類的run()方法來執(zhí)行線程。因此,任何繼承Thread類的線程都可以通過start()方法來啟動。Runnable接口用于標識某個Java類可否作為線程類,該接口只有一個抽象方法run(),即線程中最重要的執(zhí)行體,用于執(zhí)行線程中所要完成的任務。Runnable接口定義在java.lang包中,定義代碼如下所示。packagejava.lang;publicinterfaceRunnable{ publicabstractvoidrun();}12.1.2Java線程模型Callable接口是Java5新增的接口,該接口中提供一個call()方法作為線程的執(zhí)行體。call()方法比run()方法功能更強大,call()方法可以有返回值,也可以聲明拋出異常。Callable接口定義在java.util.concurrent包中,定義代碼如下所示。packagejava.util.concurrent;publicinterfaceCallable<V>{ Vcall()throwsException;}12.1.2Java線程模型Future接口用來接收Callable接口中call()方法的返回值。Future接口提供一些方法用于控制與其關聯(lián)的Callable任務。Future接口提供的方法如表12-2所示。12.1.2Java線程模型Callable接口有泛型限制,該接口中的泛型形參類型與call()方法返回值的類型相同;而且Callable接口是函數(shù)式接口,因此從Java8開始可以使用Lambda表達式創(chuàng)建Callable對象。
每個能夠獨立運行的程序就是一個進程,每個進程至少包含一個線程,即主線程。在Java語言中,每個能夠獨立運行的Java程序都至少有一個主線程,且在程序啟動時,JVM會自動創(chuàng)建一個主線程來執(zhí)行該程序中的main()方法。因此,主線程有以下兩個特點:
(1)一個進程肯定包含一個主線程
(2)主線程用來執(zhí)行main()方法
下述程序在main()方法中,調(diào)用Thread類的靜態(tài)方法currentThread()來獲取主線程,代碼如下所示。12.1.3主線程主線程【代碼12.1】MainThreadExample.javapackagecom;publicclassMainThreadExample{ publicstaticvoidmain(String[]args){ //調(diào)用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()靜態(tài)方法來獲取當前線程對象,由于是在main()方法中,所以獲取的線程是主線程。調(diào)用setName()方法可以設置線程名,調(diào)用getId()方法可以獲取線程的Id號,調(diào)用getName()方法可以獲取線程的名字。
程序運行結果如下:
主線程是:Thread[MyThread,5,main]
線程名:MyThread
線程ID:112.1.3主線程第2節(jié)part線程的創(chuàng)建和啟動
基于Java線程模型,創(chuàng)建線程的方式有三種:
(1)第一種方式是繼承Thread類,重寫Thread類中的run()方法,直接創(chuàng)建線程。
(2)第二種方式是實現(xiàn)Runnable接口,再通過Thread類和Runnable的實現(xiàn)類間接創(chuàng)建一個線程。
(3)第三種方式是使用Callable接口或Future接口間接創(chuàng)建線程。
上述三種方式從本質上是一致的,最終都是通過Thread類來建立線程。提供Runnable、Callable和Future接口模型是由于Java不支持多繼承,如果一個線程類繼承了Thread類,則不能再繼承其他的類,因此可以通過實現(xiàn)接口的方式間接創(chuàng)建線程。
采用Runnable、Callable和Future接口的方式創(chuàng)建線程時,線程類還可以繼承其他類,且多個線程之間可以共享一個target目標對象,適合多個相同線程處理同一個資源的情況,從而可以將CPU、代碼和數(shù)據(jù)分開,形成清晰的數(shù)據(jù)模型。線程的啟動和創(chuàng)建本節(jié)概述12.2.1繼承Thread類
通過繼承Thread類來創(chuàng)建并啟動線程的步驟如下:
(1)定義一個子類繼承Thread類,并重寫run()方法。
(2)創(chuàng)建子類的實例,即實例化線程對象。
(3)調(diào)用線程對象的start()方法啟動該線程。Thread類的start()方法將調(diào)用run()方法,該方法用于啟動線程并運行。因此start()方法不能多次調(diào)用,當多次調(diào)用td.start()方法時會拋出一個IllegalThreadStateException異常。下述案例示例通過繼承Thread類來創(chuàng)建并啟動線程的步驟,代碼如下所示。繼承Thread類繼承Thread類【代碼12.2】ThreadExample.javapackagecom;//繼承Thread類publicclassThreadExampleextendsThread{ //重寫run()方法 publicvoidrun(){ for(inti=0;i<10;i++){ //繼承Thread類時,直接使用this即可獲取當前線程對象 //調(diào)用getName()方法返回當前線程的名字 System.out.println(this.getName()+":"+i); } }12.2.1繼承Thread類 publicstaticvoidmain(String[]args){ //創(chuàng)建線程對象 ThreadExampletd=newThreadExample(); //調(diào)用start()方法啟動線程 td.start(); //主線程任務 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()獲取主線程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.1繼承Thread類
因為線程在CPU中的執(zhí)行是由操作系統(tǒng)所控制,執(zhí)行次序是不確定的,除非使用同步機制強制按特定的順序執(zhí)行,所以程序代碼運行的結果會因調(diào)度次序不同而不同。程序執(zhí)行結果可能如下:main:1101Thread-0:1main:1102Thread-0:2main:1103……..
在創(chuàng)建td線程對象時并未指定該線程的名字,因此所輸出的線程名是系統(tǒng)的默認值“Thread-0”。對于輸出結果,不同機器所執(zhí)行的結果可能不同,在同一機器上多次運行同一個程序也可能生成不同結果。12.2.112.2.2實現(xiàn)Runable接口
創(chuàng)建線程的第二種方式是實現(xiàn)Runnable接口。Runnable接口中只有一個run()方法,一個類實現(xiàn)Runnable接口后,并不代表該類是個“線程”類,不能直接啟動線程,必須通過Thread類的實例來創(chuàng)建并啟動線程。通過Runnable接口創(chuàng)建并啟動線程的步驟如下:
(1)定義一個類實現(xiàn)Runnable接口,并實現(xiàn)該接口中的run()方法;
(2)創(chuàng)建一個Thread類的實例,將Runnable接口的實現(xiàn)類所創(chuàng)建的對象作為參數(shù)傳入Thread類的構造方法中;
(3)調(diào)用Thread對象的start()方法啟動該線程。
下述案例示例通過實現(xiàn)Runnable接口創(chuàng)建并啟動線程的步驟,代碼如下所示。實現(xiàn)Runable接口實現(xiàn)Runable接口【代碼12.3】RunnableExamble.javapackagecom;//實現(xiàn)Runnable接口publicclassRunnableExambleimplementsRunnable{ //重寫run()方法 publicvoidrun(){ //獲取當前線程的名字 for(inti=0;i<10;i++) //實現(xiàn)Runnable接口時,只能使用Thread.currentThread()獲取當前線程對象 //再調(diào)用getName()方法返回當前線程的名字 System.out.println(Thread.currentThread().getName()+":"+i); }12.2.2實現(xiàn)Runable接口 publicstaticvoidmain(String[]args){ //創(chuàng)建一個Thread類的實例,其參數(shù)是RunnableExamble類的對象 Threadtd=newThread(newRunnableExamble()); //調(diào)用start()方法啟動線程 td.start(); //主線程任務 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()獲取主線程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.2實現(xiàn)Runable接口
上述代碼定義了一個RunnableExamble類,該類實現(xiàn)了Runnable接口,并實現(xiàn)run()方法,這樣的類可以稱為線程任務類。直接調(diào)用Thread類或Runnable接口所創(chuàng)建的對象的run()方法是無法啟動線程的,必須通過Thread的start()方法才能啟動線程。程序執(zhí)行的結果可能如下:main:1100Thread-0:0Thread-0:1Thread-0:2……..12.2.212.2.3使用Callable和Future接口
創(chuàng)建線程的第三種方式是使用Callable和Future接口。Callable接口提供一個call()方法作為線程的執(zhí)行體,該方法的返回值使用Future接口來代表。從Java5開始,為Future接口提供一個FutureTask實現(xiàn)類,該類同時實現(xiàn)了Future和Runnable兩個接口,因此可以作為Thread類的target參數(shù)。使用Callable和Future接口的最大優(yōu)勢在于可以在線程執(zhí)行完成之后獲得執(zhí)行結果。
使用Callable和Future接口創(chuàng)建并啟動線程的步驟如下:
(1)創(chuàng)建Callable接口的實現(xiàn)類,并實現(xiàn)call()方法,該方法將作為線程的執(zhí)行體,并具有返回值;然后創(chuàng)建Callable實現(xiàn)類的實例。
(2)使用FutureTask類來包裝Callable對象,在FutureTask對象中封裝了Callable對象的call()方法的返回值。
(3)使用FutureTask對象作為Thread對象的target,創(chuàng)建并啟動新線程。
(4)調(diào)用FutureTask對象的get()方法來獲得子線程執(zhí)行結束后的返回值。
下述案例示例通過Callable和Future接口創(chuàng)建并啟動線程的步驟,代碼如下所示。使用Callable和Future接口使用Callable和Future接口【代碼12.4】CallableFutureExample.javapackagecom;importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;//創(chuàng)建Callable接口的實現(xiàn)類classTaskimplementsCallable<Integer>{ //實現(xiàn)call()方法,作為線程執(zhí)行體 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實現(xiàn)類的實例 FutureTask<Integer>task=newFutureTask<>(newTask()); //創(chuàng)建線程,使用FutureTask對象task作為Thread對象的target, //并調(diào)用start()方法啟動線程 Threadtd=newThread(task,"子線程"); td.start(); //調(diào)用FutureTask對象task的get()方法獲取子線程執(zhí)行結束后的返回值 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類,該類實現(xiàn)Callable接口并重寫call()方法,call()的返回值為整型,因此Callable接口中對應的泛型限制為Integer,即Callable<Integer>。在main()方法中,先創(chuàng)建FutureTask<Integer>類的對象task,該對象包裝Task類;再創(chuàng)建Thread對象并啟動線程;最后調(diào)用FutureTask對象task的get()方法獲取子線程執(zhí)行結束后的返回值。整個程序所實現(xiàn)的功能與前兩種方式一樣,只是增加了子線程返回值。
程序運行結果如下:
子線程:0
子線程:1
子線程:2……..
子線程返回值:10main:1100main:1101……12.2.3使用Callable和Future接口
從Java8開始,可以直接使用Lambda表達式創(chuàng)建Callable對象,下述案例示例通過Lambda表達式創(chuàng)建Callable對象,代碼如下所示。【代碼12.5】LambdaCallableExample.javapackagecom;importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;publicclassLambdaCallableExample{publicstaticvoidmain(String[]args){ //使用Lambda表達式創(chuàng)建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接口 //創(chuàng)建線程,使用FutureTask對象task作為Thread對象的target, //并調(diào)用start()方法啟動線程 Threadtd=newThread(task,"子線程"); td.start(); //調(diào)用FutureTask對象task的get()方法獲取子線程執(zhí)行結束后的返回值 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表達式創(chuàng)建Callable對象,而無須先創(chuàng)建Callable實現(xiàn)類,但Lambda表達式必須在jdk1.8版本后才可以運行。
在JavaAPI中,定義的FutureTask類實際上直接實現(xiàn)RunnableFuture接口,而RunnableFuture接口繼承Runnable和Future兩個接口,因此FutureTask類即實現(xiàn)了Runnable接口,又實現(xiàn)了Future接口。12.2.3第3節(jié)part線程的生命周期
線程具有生命周期,當線程被創(chuàng)建并啟動后,不會立即進入執(zhí)行狀態(tài),也不會一直處于執(zhí)行狀態(tài)。在線程的生命周期中,要經(jīng)過5種狀態(tài):新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)。線程狀態(tài)之間的轉換如圖12-3所示。線程的生命周期本節(jié)介紹12.3.1新建和就緒狀態(tài)
當程序使用new關鍵字創(chuàng)建一個線程之后,該線程就處于新建狀態(tài),此時與其他Java對象一樣,僅由JVM為其分配內(nèi)存并初始化。新建狀態(tài)的線程沒有表現(xiàn)出任何動態(tài)特征,程序也不會執(zhí)行線程的執(zhí)行體。當線程對象調(diào)用start()方法之后,線程就處于就緒狀態(tài),相當于“等待執(zhí)行”。此時,調(diào)度程序就可以把CPU分配給該線程,JVM會為線程創(chuàng)建方法調(diào)用棧和程序計數(shù)器。處于就緒狀態(tài)的線程并沒有開始運行,只是表示該線程準備就緒等待執(zhí)行。
注意只能對新建狀態(tài)的線程調(diào)用start()方法,即new完一個線程后,只能調(diào)用一次start()方法,否則將引發(fā)IllegalThreadStateException異常。
下述案例示例了新建線程重復調(diào)用start()方法引發(fā)異常,代碼如下所示。新建和就緒狀態(tài)新建和就緒狀態(tài)【代碼12.6】IllegalThreadExample.javapackagecom;publicclassIllegalThreadExample{ publicstaticvoidmain(String[]args){ //創(chuàng)建線程 Threadt=newThread(newRunnable(){ publicvoidrun(){ for(inti=0;i<10;i++) System.out.print(i+""); } }); t.start(); t.start(); }}
上述代碼三次調(diào)用start()方法,多次啟動線程,因此會引發(fā)IllegalThreadStateException異常。運行結果可能如下所示:Exceptioninthread"main"java.lang.IllegalThreadStateException0123456789 atjava.lang.Thread.start(UnknownSource) atcom.IllegalThreadExample.main(IllegalThreadExample.java:12)12.3.112.3.2運行和阻塞狀態(tài)
處于就緒狀態(tài)的線程獲得CPU后,開始執(zhí)行run()方法的線程執(zhí)行體,此時該線程處于運行狀態(tài)。如果計算機的CPU是單核的,則在任何時刻只有一個線程處于運行狀態(tài)。一個線程開始運行后,不可能一直處于運行狀態(tài)。線程在運行過程中需要被中斷,目的是使其他線程獲得執(zhí)行的機會,線程調(diào)度的細節(jié)取決于底層平臺所采用的策略。
目前UNIX系統(tǒng)采用的是時間片算法策略,Windows系統(tǒng)采用的則是搶占式策略,另外一種小型設備(手機)則可能采用協(xié)作式調(diào)度策略。對于采用搶占式策略的系統(tǒng)而言,系統(tǒng)會給每個可執(zhí)行的線程一個小時間段來處理任務;當該時間段用完后,系統(tǒng)就會剝奪該線程所占用的資源,讓其他線程獲得執(zhí)行的機會。在選擇下一個線程時,系統(tǒng)會考慮線程的優(yōu)先級。運行和阻塞狀態(tài)運行和阻塞狀態(tài)當線程出現(xiàn)以下情況時,會進入阻塞狀態(tài):(1)調(diào)用sleep()方法,主動放棄所占用的處理器資源;(2)調(diào)用了一個阻塞式IO方法,在該方法返回之前,該線程被阻塞;(3)線程試圖獲得一個同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有;(4)執(zhí)行條件還未滿足,調(diào)用wait()方法使線程進入等待狀態(tài),等待其他線程的通知;(5)程序調(diào)用了線程的suspend()方法將該線程掛起,但該方法容易導致死鎖,因此應該盡量避免使用。12.3.2運行和阻塞狀態(tài)
正在執(zhí)行的線程被阻塞之后,其他線程就可以獲得執(zhí)行的機會,被阻塞的線程會在合適的時機重新進入就緒狀態(tài),等待線程調(diào)度器再次調(diào)度。
當線程出現(xiàn)如下幾種情況時,線程可以解除阻塞進入就緒狀態(tài):
(1)調(diào)用sleep()方法的線程經(jīng)過了指定的時間;
(2)線程調(diào)用的阻塞式IO方法以經(jīng)返回;
(3)線程成功地獲得了同步監(jiān)視器;
(4)線程處于等待狀態(tài),其他線程調(diào)用notify()或notifyAll()方法發(fā)出了一個通知時,則線程回到就緒狀態(tài);
(5)處于掛起狀態(tài)的線程被調(diào)用了resume()恢復方法。12.3.2運行和阻塞狀態(tài)
在線程運行的過程中,可以通過sleep()方法使線程暫時停止運行,進入休眠狀態(tài)。在使用sleep()方法時需要注意以下兩點:
(1)sleep()方法的參數(shù)是以毫秒為基本單位,例如sleep(2000)則休眠2秒鐘;
(2)sleep()方法聲明了InterruptedException異常,因此調(diào)用sleep()方法時要么放在try…catch語句中捕獲該異常并處理,要么在方法后使用throws顯式聲明拋出該異常。
可以通過Thread類的isAlive()方法來判斷線程是否處于運行狀態(tài)。當線程處于就緒、運行和阻塞三種狀態(tài)時,isAlive()方法的返回值為true;當線程處于新建、死亡兩種狀態(tài)時,isAlive()方法的返回值為false。
下述案例示例了線程的創(chuàng)建、運行和死亡三個狀態(tài),代碼如下所示。12.3.2運行和阻塞狀態(tài)【代碼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("新建狀態(tài)isAlive():"+tle.isAlive()); tle.start(); System.out.println("運行狀態(tài)isAlive():"+tle.isAlive()); Thread.sleep(2000); System.out.println("線程結束isAlive():"+tle.isAlive()); }}12.3.2運行和阻塞狀態(tài)
程序運行結果如下:
新建狀態(tài)isAlive():false
運行狀態(tài)isAlive():truesum=5050
線程結束isAlive():false
注意:線程調(diào)用wait()方法進入等待狀態(tài)后,需其他線程調(diào)用notify()或notifyAll()方法發(fā)出通知才能進入就緒狀態(tài)。使用suspend()和resume()方法可以掛起和喚醒線程,但這兩個方法可能會導致不安全因素。如果對某個線程調(diào)用interrupt()方法發(fā)出中斷請求,則該線程會根據(jù)線程狀態(tài)拋出InterruptedException異常,對異常進行處理時可以再次調(diào)度該線程。12.3.2死亡狀態(tài)線程結束后就處于死亡狀態(tài),結束線程有以下三種方式:(1)線程執(zhí)行完成run()或call()方法,線程正常結束;(2)線程拋出一個未捕獲的Exception或Error;(3)調(diào)用stop()方法直接停止線程,該方法容易導致死鎖,通常不推薦使用。死亡狀態(tài)12.3.2死亡狀態(tài)
主線程結束時,其他子線程不受任何影響,并不會隨主線程的結束而結束。一旦子線程啟動起來,子線程就擁有和主線程相同的地位,子線程不會受主線程的影響。
為了測試某個線程是否死亡,可以通過線程對象的isAlive()方法來獲得線程狀態(tài),當方法返回值為false時,線程處于死亡或新建狀態(tài)。不要試圖對一個已經(jīng)死亡的線程調(diào)用start()方法使其重新啟動,線程死亡就是死亡,該線程不可再次作為線程執(zhí)行。Thread類中的join()方法可以讓一個線程等待另一個線程完成后,繼續(xù)執(zhí)行原線程中的任務。當在某個程序執(zhí)行流中調(diào)用其他線程的join()方法時,當前線程將被阻塞,直到另一個線程執(zhí)行完為止。join()方法通常由使用線程的程序調(diào)用,當其他線程都執(zhí)行結束后,再調(diào)用主線程進一步操作。
下述案例示例了join()方法的使用,代碼如下所示。12.3.2死亡狀態(tài)【代碼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死亡狀態(tài)publicclassJoinExample{ publicstaticvoidmain(String[]args){ //創(chuàng)建子線程 JoinThreadt1=newJoinThread("被Join的子線程"); //啟動子線程 t1.start(); //等待子線程執(zhí)行完畢 try{ t1.join(); }catch(InterruptedExceptione){ //TODOAuto-generatedcatchblock e.printStackTrace(); } //輸出主線程名 System.out.println("主線程名為:"+Thread.currentThread().getName()); //子線程已經(jīng)處于死亡狀態(tài),其isAlive()方法返回值為false System.out.println("子線程死亡狀態(tài)isAlive():"+t1.isAlive()); //再次啟動子線程,拋出異常 t1.start(); }}12.3.2死亡狀態(tài)
上述代碼中開始調(diào)用了線程的join()方法,最后又對死亡狀態(tài)的線程再次調(diào)用start()方法,運行結果如下所示:…..
被Join的子線程:8
被Join的子線程:9
主線程名為:main
子線程死亡狀態(tài)isAlive():falseExceptioninthread"main"java.lang.IllegalThreadStateException atjava.lang.Thread.start(UnknownSource) atcom.JoinExample.main(JoinExample.java:33)
在上述代碼中,注銷掉join()方法的調(diào)用和對死亡狀態(tài)線程的start()方法的再次調(diào)用,運行結果可能如下:
主線程名為:main
被Join的子線程:0
被Join的子線程:1
子線程死亡狀態(tài)isAlive():true
被Join的子線程:2
被Join的子線程:3……12.3.2第4節(jié)part線程的優(yōu)先級
每個線程執(zhí)行時都具有一定的優(yōu)先級,線程的優(yōu)先級代表該線程的重要程度。當有多個線程同時處于可執(zhí)行狀態(tài)并等待獲得CPU處理器時,系統(tǒng)將根據(jù)各個線程的優(yōu)先級來調(diào)度各線程,優(yōu)先級越高的線程獲得CPU時間的機會越多,而優(yōu)先級低的線程則獲得較少的執(zhí)行機會。
每個線程都有默認的優(yōu)先級,其優(yōu)先級都與創(chuàng)建該線程的父線程的優(yōu)先級相同。在默認情況下,主線程具有普通優(yōu)先級,由主線程創(chuàng)建的子線程也具有普通優(yōu)先級。Thread類提供三個靜態(tài)常量來標識線程的優(yōu)先級:
(1)MAX_PRIORITY:最高優(yōu)先級,其值為10;
(2)NORM_PRIORITY:普通優(yōu)先級,其值為5;
(3)MIN_PRIORITY:最低優(yōu)先級,其值為1。線程的優(yōu)先級線程的優(yōu)先級Thread類提供了setPriority()方法來對線程的優(yōu)先級進行設置,而getPriority()方法來獲取線程的優(yōu)先級。setPriority()方法的參數(shù)是一個整數(shù)(1~10),也可以使用Thread類提供的三個優(yōu)先級靜態(tài)常量。
線程的優(yōu)先級高度依賴于操作系統(tǒng),并不是所有的操作系統(tǒng)都支持Java的10個優(yōu)先級,例如Windows2000僅提供7個優(yōu)先級。因此,盡量避免直接使用整數(shù)給線程指定優(yōu)先級,提倡使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三個優(yōu)先級靜態(tài)常量。另外,優(yōu)先級并不能保證線程的執(zhí)行次序,因此應避免使用線程優(yōu)先級作為構建任務執(zhí)行順序的標準。
下述案例示例了線程優(yōu)先級的設置及使用,代碼如下所示。線程的優(yōu)先級【代碼12.9】PriorityExample.javapackagecom;classMyPriorityThreadextendsThread{ publicMyPriorityThread(){ super(); } publicMyPriorityThread(Stringname){ super(name); } publicvoidrun(){ for(inti=0;i<10;i++){ System.out.println(this.getName()+",其優(yōu)先級是:"+this.getPriority() +",循環(huán)變量的值為:"+i); } }}線程的優(yōu)先級publicclassPriorityExample{ publicstaticvoidmain(String[]args){ //輸出主線程的優(yōu)先級 System.out.println("主線程的優(yōu)先級:"+Thread.currentThread().getPriority()); //創(chuàng)建子線程,并設置不同優(yōu)先級 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(); }}線程的優(yōu)先級程序運行執(zhí)行結果可能如下:主線程的優(yōu)先級:5普通,其優(yōu)先級是:5,循環(huán)變量的值為:0高級,其優(yōu)先級是:10,循環(huán)變量的值為:0高級,其優(yōu)先級是:10,循環(huán)變量的值為:1高級,其優(yōu)先級是:10,循環(huán)變量的值為:2高級,其優(yōu)先級是:10,循環(huán)變量的值為:3普通,其優(yōu)先級是:5,循環(huán)變量的值為:1高級,其優(yōu)先級是:10,循環(huán)變量的值為:4指定值,其優(yōu)先級是:4,循環(huán)變量的值為:0…….通過運行結果可以看出,優(yōu)先級越高的線程提前獲得執(zhí)行機會就越多。
線程的優(yōu)先級第5節(jié)part線程的同步
多線程訪問同一資源數(shù)據(jù)時,很容易出現(xiàn)線程安全問題。以多窗口出售車票為例,一旦多線程并發(fā)訪問,就可能出現(xiàn)問題,造成一票多售的現(xiàn)象。在Java中,提供了線程同步的概念以保證某個資源在某一時刻只能由一個線程訪問,以此保證共享數(shù)據(jù)的一致性。Java使用監(jiān)控器(也稱對象鎖)實現(xiàn)同步。每個對象都有一個監(jiān)控器,使用監(jiān)控器可以保證一次只允許一個線程執(zhí)行對象的同步語句。即在對象的同步語句執(zhí)行完畢前,其他試圖執(zhí)行當前對象的同步語句的線程都將處于阻塞狀態(tài),只有線程在當前對象的同步語句執(zhí)行完畢后,監(jiān)控器才會釋放對象鎖,并讓優(yōu)先級最高的阻塞線程處理同步語句。
線程同步通常采用三種方式:同步代碼塊、同步方法和同步鎖。線程的同步本節(jié)介紹12.5.1同步代碼塊
使用同步代碼塊實現(xiàn)同步功能,只需將對實例的訪問語句放入一個同步塊中,其語法格式如下:synchronized(object){ //需要同步的代碼塊}
其中:synchronized是同步關鍵字;object是同步監(jiān)視器,其數(shù)據(jù)類型不能是基本數(shù)據(jù)類型。線程開始執(zhí)行同步代碼之前,必須先獲得同步監(jiān)視器的鎖定,并且,任何時刻只能有一個線程可以獲得對同步監(jiān)視器的鎖定,當同步代碼塊執(zhí)行完成后,該線程會釋放對該同步監(jiān)視器的鎖定。
下述案例示例了同步代碼塊的聲明和使用,代碼如下所示。同步代碼塊同步代碼塊【代碼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; //操作金額,正數(shù)為存錢,負數(shù)為取錢 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){ //創(chuàng)建一個銀行賬戶實例 BankAccountmyAccount=newBankAccount("101",5000); 12.5.1同步代碼塊 //創(chuàng)建多個線程,對賬戶進行存取錢操作 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){}”對銀行賬戶的操作代碼進行同步,保證某一時刻只能有一個線程訪問該賬戶,只有{}里面的代碼執(zhí)行完畢,才釋放對該賬戶的鎖定。
程序運行結果如下所示:T1操作成功,目前賬戶余額為:2000.0T2操作失敗,余額不足!T3操作成功,目前賬戶余額為:3000.0
賬號:101,余額:3000.012.5.112.5.2同步方法
同步方法是使用synchronized關鍵字修飾的方法,其聲明的語法格式如下:[訪問修飾符]synchronized返回類型方法名([參數(shù)列表]){ //方法體}
其中:synchronized關鍵字修飾的實例方法無須顯式地指定同步監(jiān)視器,同步方法的同步監(jiān)視器是this,即該方法所屬的對象。一旦一個線程進入一個實例的任何同步方法,其他線程將不能進入該實例的所有同步方法,但該實例的非同步方法仍然能夠被調(diào)用。
使用同步方法可以非常方便地實現(xiàn)線程安全,一個具有同步方法的類被稱為“線程安全的類”,該類的對象可以被多個線程安全地訪問,且每個線程調(diào)用該對象的方法后都將得到正確的結果。下述案例示例了同步方法的聲明和使用,代碼如下所示。同步方法同步方法【代碼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; //操作金額,正數(shù)為存錢,負數(shù)為取錢 privatedoublemoney; publicSynMethodExample(Stringname,SynMethodaccount,doublemoney){ super(name); this.account=account; this.money=money; } //線程任務 publicvoidrun(){ //調(diào)用account對象的同步方法 this.account.access(money); } publicstaticvoidmain(String[]args){ //創(chuàng)建一個銀行賬戶實例 SynMethodmyAccount=newSynMethod("1001",5000); //創(chuàng)建多個線程,對賬戶進行存取錢操作 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是一種更強大的線程同步機制,通過顯式定義同步鎖對象來實現(xiàn)線程同步。同步鎖提供了比同步代碼塊、同步方法更廣泛的鎖定操作,實現(xiàn)更靈活。Lock是控制多個線程對共享資源進行訪問的工具,能夠對共享資源進行獨占訪問。每次只能有一個線程對Lock對象加鎖,線程訪問共享資源之前需要先獲得Lock對象。某些鎖可能允許對共享資源并發(fā)訪問,如ReadWriteLock(讀寫鎖)。Lock和ReadWriteLock是Java5提供的關于鎖的兩個根接口,并為Lock提供了ReentrantLock(可重入鎖)實現(xiàn)類,為ReadWriteLock提供了ReentrantReadWriteLock實現(xiàn)類。從Java8開始,又新增了StampedeLock類,可以替代傳統(tǒng)的ReentrantReadWriteLock類。同步鎖同步鎖ReentrantLock類是常用的可重入同步鎖,該類對象可以顯式地加鎖、釋放鎖。使用ReentrantLock類的步驟如下:
(1)定義一個ReentrantLock鎖對象,該對象是final常量;privatefinalReentrantLocklock=newReentrantLock();
(2)在需要保證線程安全的代碼之前增加“加鎖”操作;lock.lock();
(3)在執(zhí)行完線程安全的代碼后“釋放鎖”。lock.unlock();12.5.3同步鎖下述代碼示例了使用ReentrantLock鎖的基本步驟://1.定義鎖對象privatefinalReentrantLocklock=newReentrantLock();...//定義需要保證線程安全的方法publicvoidmyMethod(){//2.加鎖lock.lock();try{//需要保證線程安全的代碼...}finally{//3.釋放鎖lock.unlock();}}
其中:加鎖和釋放鎖都需要放在線程安全的方法中;lock.unlock()放在finally語句中,不管發(fā)生異常與否,都需要釋放鎖。12.5.3同步鎖下述案例示例了ReentrantLock同步鎖的使用,代碼如下所示?!敬a12.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; //操作金額,正數(shù)為存錢,負數(shù)為取錢 privatedoublemoney; publicSynLockExample(Stringname,SynLockaccount,doublemoney){ super(name); this.account=account; this.money=money; } //線程任務 publicvoidrun(){ //調(diào)用account對象的access()方法 this.account.access(money); } publicstaticvoidmain(String[]args){ //創(chuàng)建一個銀行賬戶實例 SynLockmyAccount=newSynLock("1001",5000); //創(chuàng)建多個線程,對賬戶進行存取錢操作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. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
- 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 現(xiàn)代辦公環(huán)境下的家校協(xié)同教育模式探討
- 新課改下的小學數(shù)學教學策略變化與影響
- 算法優(yōu)化在嵌入式辦公系統(tǒng)中的實踐案例
- 針對學習障礙學生的專業(yè)輔導課程設置
- 個人倉儲租賃合同模板
- 上海市商品買賣合同范本
- 買賣合同爭議解決協(xié)議書模板
- 不動產(chǎn)附負擔租賃合同
- 個人培訓機構與教師簽訂勞動合同的法律效力解析
- 個人借車合同范本
- 《長津湖》電影賞析PPT
- 多維閱讀第10級 who is who 看看都是誰
- 滑雪運動介紹
- 高二下學期英語閱讀限時訓練(一)
- 半導體制造工藝-13薄膜沉積(下)綜述課件
- 大數(shù)據(jù)和人工智能知識考試題庫600題(含答案)
- 2021譯林版高中英語選擇性必修一單詞表
- 保健食品經(jīng)營環(huán)節(jié)檢查方法
- 幼兒園大班綜合《月亮姑娘做衣裳》微課件
- 顯微外科課件
- 教育哲學課件第一章-教育哲學的歷史發(fā)展
評論
0/150
提交評論