主线程如何等待多线程完成 返回数据_多线程基础体系知识清单
作者:Object
來源:https://juejin.im/user/5d53e1f6f265da03af19cae0/posts
前言
本文會介紹Java中多線程與并發的基礎,適合初學者食用。
線程與進程的區別
在計算機發展初期,每臺計算機是串行地執行任務的,如果碰上需要IO的地方,還需要等待長時間的用戶IO,后來經過一段時間有了批處理計算機,其可以批量串行地處理用戶指令,但本質還是串行,還是不能并發執行。
如何解決并發執行的問題呢?于是引入了進程的概念,每個進程獨占一份內存空間,進程是內存分配的最小單位,相互間運行互不干擾且可以相互切換,現在我們所看到的多個進程“同時"在運行,實際上是進程高速切換的效果。
那么有了線程之后,我們的計算機系統看似已經很完美了,為什么還要進入線程呢?如果一個進程有多個子任務,往往一個進程需要逐個去執行這些子任務,但往往這些子任務是不相互依賴的,可以并發執行,所以需要CPU進行更細粒度的切換。所以就引入了線程的概念,線程隸屬于某一個進程,它共享進程的內存資源,相互間切換更快速。
進程與線程的區別:
Java中進程與線程的關系:
線程的start方法和run方法的區別
區別
Java中創建線程的方式有兩種,不管使用繼承Thread的方式還是實現Runnable接口的方式,都需要重寫run方法。調用start方法會創建一個新的線程并啟動,run方法只是啟動線程后的回調函數,如果調用run方法,那么執行run方法的線程不會是新創建的線程,而如果使用start方法,那么執行run方法的線程就是我們剛剛啟動的那個線程。
程序驗證
public?class?Main?{????public?static?void?main(String[]?args)?{????????Thread?thread?=?new?Thread(new?SubThread());????????thread.run();????????thread.start();????}}class?SubThread?implements?Runnable{????@Override????public?void?run()?{????????//?TODO?Auto-generated?method?stub????????System.out.println("執行本方法的線程:"+Thread.currentThread().getName());????}}Thread和Runnable的關系
Thread源碼
Runnable源碼
區別
通過上述源碼圖,不難看出,Thread是一個類,而Runnable是一個接口,Runnable接口中只有一個沒有實現的run方法,可以得知,Runnable并不能獨立開啟一個線程,而是依賴Thread類去創建線程,執行自己的run方法,去執行相應的業務邏輯,才能讓這個類具備多線程的特性。
使用繼承Thread方式和實現Runable接口方式分別創建子線程
使用繼承Thread類方式創建子線程
public?class?Main?extends?Thread{????public?static?void?main(String[]?args)?{????????Main?main?=?new?Main();????????main.start();????}????@Override????public?void?run()?{????????System.out.println("通過繼承Thread接口方式創建子線程成功,當前線程名:"+Thread.currentThread().getName());????}}運行結果:
使用實現Runnable接口方式創建子線程
public?class?Main{????public?static?void?main(String[]?args)?{????????SubThread?subThread?=?new?SubThread();????????Thread?thread?=?new?Thread(subThread);????????thread.start();????}}class?SubThread?implements?Runnable{????@Override????public?void?run()?{????????//?TODO?Auto-generated?method?stub????????System.out.println("通過實現Runnable接口創建子線程成功,當前線程名:"+Thread.currentThread().getName());????}}運行結果:
使用匿名內部類方式創建子線程
public?class?Main{????public?static?void?main(String[]?args)?{????????Thread?thread?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????//?TODO?Auto-generated?method?stub????????????????System.out.println("使用匿名內部類方式創建線程成功,當前線程名:"+Thread.currentThread().getName());????????????}????????});????????thread.start();????}}運行結果:
關系
如何實現處理多線程的返回值
通過剛才的學習,我們知道多線程的邏輯需要放到run方法中去執行,而run方法是沒有返回值的,那么遇到需要返回值的狀況就不好解決,那么如何實現子線程返回值呢?
主線程等待法
通過讓主線程等待,直到子線程運行完畢為止。
實現方式:
public?class?Main{????static?String?str;????public?static?void?main(String[]?args)?{????????Thread?thread?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????str="子線程執行完畢";????????????}????????});????????thread.start();????????//如果子線程還未對str進行賦值,則一直輪轉????????while(str==null)?{}????????System.out.println(str);????}}使用Thread中的join()方法
join()方法可以阻塞當前線程以等待子線程處理完畢。
實現方式:
public?class?Main{????static?String?str;????public?static?void?main(String[]?args)?{????????Thread?thread?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????str="子線程執行完畢";????????????}????????});????????thread.start();????????//如果子線程還未對str進行賦值,則一直輪轉????????try?{????????????thread.join();????????}?catch?(InterruptedException?e)?{????????????//?TODO?Auto-generated?catch?block????????????e.printStackTrace();????????}????????System.out.println(str);????}}join方法能做到比主線程等待法更精準的控制,但是join方法的控制粒度并不夠細。比如,我需要控制子線程將字符串賦一個特定的值時,再執行主線程,這種操作join方法是沒有辦法做到的。
通過Callable接口實現:通過FutureTask或者線程池獲取
在JDK1.5之前,線程是沒有返回值的,通常程序猿需要獲取子線程返回值頗費周折,現在Java有了自己的返回值線程,即實現了Callable接口的線程,執行了實現Callable接口的線程之后,可以獲得一個Future對象,在該對象上調用一個get方法,就可以執行子線程的邏輯并獲取返回的Object。
實現方式1(錯誤示例):
public?class?Main?implements?Callable{????@Override????public?String?call()?throws?Exception?{????????//?TODO?Auto-generated?method?stub????????String?str?=?"我是帶返回值的子線程";????????return?str;????}????public?static?void?main(String[]?args)?{????????Main?main?=?new?Main();????????try?{????????????String?str?=?main.call();????????????System.out.println(str);????????}?catch?(Exception?e)?{????????????//?TODO?Auto-generated?catch?block????????????e.printStackTrace();????????}????}}運行結果:
實現方式2(使用FutureTask):
public?class?Main?implements?Callable{????@Override????public?String?call()?throws?Exception?{????????//?TODO?Auto-generated?method?stub????????String?str?=?"我是帶返回值的子線程";????????return?str;????}????public?static?void?main(String[]?args)?{????????FutureTask?task?=?new?FutureTask(new?Main());????????new?Thread(task).start();????????try?{????????????if(!task.isDone())?{????????????????System.out.println("任務沒有執行完成");????????????}????????????System.out.println("等待中...");????????????Thread.sleep(3000);????????????System.out.println(task.get());????????}?catch?(InterruptedException?|?ExecutionException?e)?{????????????//?TODO?Auto-generated?catch?block????????????e.printStackTrace();????????}????}}運行結果:
實現方法3(使用線程池配合Future獲取):
public?class?Main?implements?Callable{????@Override????public?String?call()?throws?Exception?{????????//?TODO?Auto-generated?method?stub????????String?str?=?"我是帶返回值的子線程";????????return?str;????}????public?static?void?main(String[]?args)?throws?InterruptedException,?ExecutionException?{????????ExecutorService?newCacheThreadPool?=?Executors.newCachedThreadPool();?????????Future?future?=?newCacheThreadPool.submit(new?Main());????????if(!future.isDone())?{????????????System.out.println("線程尚未執行結束");????????}????????System.out.println("等待中");????????Thread.sleep(300);????????System.out.println(future.get());????????newCacheThreadPool.shutdown();????}}運行結果:
線程的狀態
Java線程主要分為以下六個狀態:新建態(new),運行態(Runnable),無限期等待(Waiting),限期等待(TimeWaiting),阻塞態(Blocked),結束(Terminated)。
新建(new)
新建態是線程處于已被創建但沒有被啟動的狀態,在該狀態下的線程只是被創建出來了,但并沒有開始執行其內部邏輯。
運行(Runnable)
運行態分為Ready和Running,當線程調用start方法后,并不會立即執行,而是去爭奪CPU,當線程沒有開始執行時,其狀態就是Ready,而當線程獲取CPU時間片后,從Ready態轉為Running態。
等待(Waiting)
處于等待狀態的線程不會自動蘇醒,而只有等待被其它線程喚醒,在等待狀態中該線程不會被CPU分配時間,將一直被阻塞。以下操作會造成線程的等待:
鎖:https://juejin.im/post/5d8da403f265da5b5d203bf4
限期等待(TimeWaiting)
處于限期等待的線程,CPU同樣不會分配時間片,但存在于限期等待的線程無需被其它線程顯式喚醒,而是在等待時間結束后,系統自動喚醒。以下操作會造成線程限時等待:
阻塞(Blocked)
當多個線程進入同一塊共享區域時,例如Synchronized塊、ReentrantLock控制的區域等,會去整奪鎖,成功獲取鎖的線程繼續往下執行,而沒有獲取鎖的線程將進入阻塞狀態,等待獲取鎖。
結束(Terminated)
已終止線程的線程狀態,線程已結束執行。
Sleep和Wait的區別
Sleep和Wait者兩個方法都可以使線程進入限期等待的狀態,那么這兩個方法有什么區別呢?
測試代碼:
public?class?Main{????public?static?void?main(String[]?args)?{????????Thread?threadA?=?new?Thread(new?ThreadA());????????Thread?threadB?=?new?Thread(new?ThreadB());????????threadA.setName("threadA");????????threadB.setName("threadB");????????threadA.start();????????threadB.start();????}????public?static?synchronized?void?print()?{????????System.out.println("當前線程:"+Thread.currentThread().getName()+"執行Sleep");????????try?{????????????Thread.sleep(1000);????????}?catch?(InterruptedException?e)?{????????????//?TODO?Auto-generated?catch?block????????????e.printStackTrace();????????}????????System.out.println("當前線程:"+Thread.currentThread().getName()+"執行Wait");????????try?{????????????Main.class.wait(1000);????????}?catch?(InterruptedException?e)?{????????????//?TODO?Auto-generated?catch?block????????????e.printStackTrace();????????}????????System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");????}}class?ThreadA?implements?Runnable{????@Override????public?void?run()?{????????//?TODO?Auto-generated?method?stub????????Main.print();????}}class?ThreadB?implements?Runnable{????@Override????public?void?run()?{????????//?TODO?Auto-generated?method?stub????????Main.print();????}}執行結果:
從上面的結果可以分析出:當線程A執行sleep后,等待一秒被喚醒后繼續持有鎖,執行之后的代碼,而執行wait之后,立即釋放了鎖,不僅讓出了CPU還讓出了鎖,而后線程B立即持有鎖開始執行,和線程A執行了同樣的步驟,當線程B執行wait方法之后,釋放鎖,然后線程A拿到鎖打印了第一個執行完畢,然后線程B打印執行完畢。
notify和notifyAll的區別
notify
notify可以喚醒一個處于等待狀態的線程,上代碼:
public?class?Main{????public?static?void?main(String[]?args)?{????????Object?lock?=?new?Object();????????Thread?threadA?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????synchronized?(lock)?{????????????????????try?{????????????????????????lock.wait();????????????????????}?catch?(InterruptedException?e)?{????????????????????????//?TODO?Auto-generated?catch?block????????????????????????e.printStackTrace();????????????????????}????????????????????print();????????????????}????????????}????????});????????Thread?threadB?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????synchronized?(lock)?{????????????????????print();????????????????????lock.notify();????????????????}????????????}????????});????????threadA.setName("threadA");????????threadB.setName("threadB");????????threadA.start();????????threadB.start();????}????public?static?void?print()?{????????????System.out.println("當前線程:"+Thread.currentThread().getName()+"執行print");????????????try?{????????????????Thread.sleep(1000);????????????}?catch?(InterruptedException?e)?{????????????????//?TODO?Auto-generated?catch?block????????????????e.printStackTrace();????????????}????????????System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");????}}執行結果:
代碼解釋:線程A在開始執行時立即調用wait進入無限等待狀態,如果沒有別的線程來喚醒它,它將一直等待下去,所以此時B持有鎖開始執行,并且在執行完畢時調用了notify方法,該方法可以喚醒wait狀態的A線程,于是A線程蘇醒,開始執行剩下的代碼。
notifyAll
notifyAll可以用于喚醒所有等待的線程,使所有處于等待狀態的線程都變為ready狀態,去重新爭奪鎖。
public?class?Main{????public?static?void?main(String[]?args)?{????????Object?lock?=?new?Object();????????Thread?threadA?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????synchronized?(lock)?{????????????????????try?{????????????????????????lock.wait();????????????????????}?catch?(InterruptedException?e)?{????????????????????????//?TODO?Auto-generated?catch?block????????????????????????e.printStackTrace();????????????????????}????????????????????print();????????????????}????????????}????????});????????Thread?threadB?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????synchronized?(lock)?{????????????????????print();????????????????????lock.notifyAll();????????????????}????????????}????????});????????threadA.setName("threadA");????????threadB.setName("threadB");????????threadA.start();????????threadB.start();????}????public?static?void?print()?{????????????System.out.println("當前線程:"+Thread.currentThread().getName()+"執行print");????????????try?{????????????????Thread.sleep(1000);????????????}?catch?(InterruptedException?e)?{????????????????//?TODO?Auto-generated?catch?block????????????????e.printStackTrace();????????????}????????????System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");????}}執行結果:
要喚醒前一個例子中的線程A,不光notify方法可以做到,調用notifyAll方法同樣也可以做到,那么兩者有什么區別呢?
區別
要說清楚他們的區別,首先要簡單的說一下Java synchronized的一些原理,在openjdk中查看java的源碼可以看到,java對象中存在monitor鎖,monitor對象中包含鎖池和等待池。
鎖池,假設有多個對象進入synchronized塊爭奪鎖,而此時已經有一個對象獲取到了鎖,那么剩余爭奪鎖的對象將直接進入鎖池中。
等待池,假設某個線程調用了對象的wait方法,那么這個線程將直接進入等待池,而等待池中的對象不會去爭奪鎖,而是等待被喚醒。
下面可以說notify和notifyAll的區別了:
notifyAll會讓所有處于等待池中的線程全部進入鎖池去爭奪鎖,而notify只會隨機讓其中一個線程去爭奪鎖。
yield方法
概念
????/**?????*?A?hint?to?the?scheduler?that?the?current?thread?is?willing?to?yield?????*?its?current?use?of?a?processor.?The?scheduler?is?free?to?ignore?this?????*?hint.?????*?????*??Yield?is?a?heuristic?attempt?to?improve?relative?progression?????*?between?threads?that?would?otherwise?over-utilise?a?CPU.?Its?use?????*?should?be?combined?with?detailed?profiling?and?benchmarking?to?????*?ensure?that?it?actually?has?the?desired?effect.?????*?????*?
?It?is?rarely?appropriate?to?use?this?method.?It?may?be?useful?????*?for?debugging?or?testing?purposes,?where?it?may?help?to?reproduce?????*?bugs?due?to?race?conditions.?It?may?also?be?useful?when?designing?????*?concurrency?control?constructs?such?as?the?ones?in?the?????*?{@link?java.util.concurrent.locks}?package.?????*/????public?static?native?void?yield();
yield源碼上有一段長長的注釋,其大意是說:當前線程調用yield方法時,會給當前線程調度器一個暗示,當前線程愿意讓出CPU的使用,但是它的作用應結合詳細的分析和測試來確保已經達到了預期的效果,因為調度器可能會無視這個暗示,使用這個方法是不那么合適的,或許在測試環境中使用它會比較好。
測試:
public?class?Main{????public?static?void?main(String[]?args)?{????????Thread?threadA?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????System.out.println("ThreadA正在執行yield");????????????????Thread.yield();????????????????System.out.println("ThreadA執行yield方法完成");????????????}????????});????????Thread?threadB?=?new?Thread(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????System.out.println("ThreadB正在執行yield");????????????????Thread.yield();????????????????System.out.println("ThreadB執行yield方法完成");????????????}????????});????????threadA.setName("threadA");????????threadB.setName("threadB");????????threadA.start();????????threadB.start();????}測試結果:
可以看出,存在不同的測試結果,這里選出兩張。
第一種結果:線程A執行完yield方法,讓出cpu給線程B執行。然后兩個線程繼續執行剩下的代碼。
第二種結果:線程A執行yield方法,讓出cpu給線程B執行,但是線程B執行yield方法后并沒有讓出cpu,而是繼續往下執行,此時就是系統無視了這個暗示。
interrupt方法
中止線程
interrupt函數可以中斷一個線程,在interrupt之前,通常使用stop方法來終止一個線程,但是stop方法過于暴力,它的特點是,不論被中斷的線程之前處于一個什么樣的狀態,都無條件中斷,這會導致被中斷的線程后續的一些清理工作無法順利完成,引發一些不必要的異常和隱患,還有可能引發數據不同步的問題。
溫柔的interrupt方法
interrupt方法的原理與stop方法相比就顯得溫柔的多,當調用interrupt方法去終止一個線程時,它并不會暴力地強制終止線程,而是通知這個線程應該要被中斷了,和yield一樣,這也是一種暗示,至于是否應該中斷,由被中斷的線程自己去決定。當對一個線程調用interrupt方法時:
線程池
線程池的引入是用來解決在日常開發的多線程開發中,如果開發者需要使用到非常多的線程,那么這些線程在被頻繁的創建和銷毀時,會對系統造成一定的影響,有可能系統在創建和銷毀這些線程所耗費的時間會比完成實際需求的時間還要長。
另外,在線程很多的狀況下,對線程的管理就形成了一個很大的問題,開發者通常要將注意力從功能上轉移到對雜亂無章的線程進行管理上,這項動作實際上是非常耗費精力的。
利用Executors創建不同的線程池滿足不同場景的需求
newFixThreadPool(int nThreads)指定工作線程數量的線程池。
newCachedThreadPool()處理大量中斷事件工作任務的線程池,
newSingleThreadExecutor()創建唯一的工作線程來執行任務,如果線程異常結束,會有另一個線程取代它。可保證順序執行任務。
newSingleThreadScheduledExecutor()與newScheduledThreadPool(int corePoolSize)定時或周期性工作調度,兩者的區別在于前者是單一工作線程,后者是多線程
newWorkStealingPool()內部構建ForkJoinPool,利用working-stealing算法,并行地處理任務,不保證處理順序。
Fork/Join框架:把大任務分割稱若干個小任務并行執行,最終匯總每個小任務后得到大任務結果的框架。
為什么要使用線程池
線程是稀缺資源,如果無限制地創建線程,會消耗系統資源,而線程池可以代替開發者管理線程,一個線程在結束運行后,不會銷毀線程,而是將線程歸還線程池,由線程池再進行管理,這樣就可以對線程進行復用。
所以線程池不但可以降低資源的消耗,還可以提高線程的可管理性。
使用線程池啟動線程
public?class?Main{????public?static?void?main(String[]?args)?{????????ExecutorService?newFixThreadPool?=?Executors.newFixedThreadPool(10);????????newFixThreadPool.execute(new?Runnable()?{????????????@Override????????????public?void?run()?{????????????????//?TODO?Auto-generated?method?stub????????????????System.out.println("通過線程池啟動線程成功");????????????}????????});????????newFixThreadPool.shutdown();????}}新任務execute執行后的判斷
要知道這個點首先要先說說ThreadPoolExecutor的構造函數,其中有幾個參數:
那么新任務提交后會執行下列判斷:
handler 線程池飽和策略
- AbortPolicy:直接拋出異常,默認。
- CallerRunsPolicy:用調用者所在的線程來執行任務。
- DiscardOldestPolicy:丟棄隊列中靠最前的任務,并執行當前任務。
- DiscardPolicy:直接丟棄任務
- 自定義。
線程池的大小如何選定
這個問題并不是什么秘密,在網上各大技術網站均有文章說明,我就拿一個最受認可的寫上吧
- CPU密集型:線程數 = 核心數或者核心數+1
- IO密集型:線程數 = CPU核數*(1+平均等待時間/平均工作時間)
當然這個也不能完全依賴這個公式,更多的是要依賴平時的經驗來操作,這個公式也只是僅供參考而已。
結語
本文提供了一些Java多線程和并發方面最最基礎的知識,適合初學者了解Java多線程的一些基本知識,如果想了解更多的關于并發方面的內容可以看:
https://juejin.im/post/5d8da403f265da5b5d203bf4
總結
以上是生活随笔為你收集整理的主线程如何等待多线程完成 返回数据_多线程基础体系知识清单的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pep8 python 编码规范_Pyt
- 下一篇: tensorflow gpu利用率为0_