脱发篇-多线程基础(下)来看看你知道多少
看完了,發現對你有用的話點個贊吧! 持續努力更新學習中!!多線程其他的部分點擊我的頭像查看更多哦!
知識點
標注:在學習中需要修改的內容以及筆記全在這里 www.javanode.cn,謝謝!有任何不妥的地方望糾正
線程創建
1. 創建方式
- 繼續Thread類
- 實現Runable接口
- 實現Callable接口,并與Future、線程池結合使用,
1. 繼承Thread
Thread thread = new Thread(){@Overridepublic void run() {System.out.println("this is new thread");}};thread.start();2. 實現runable接口
Thread thread1 = new Thread(new Runnable() {public void run() {System.out.println("impl runnable thread");}});thread1.start();3. 實現Callable接口
/*** 3.實現callable接口,提交給ExecutorService返回的是異步執行的結果*/ExecutorService executorService = Executors.newSingleThreadExecutor();Future<String> submit = executorService.submit(new Callable<String>() {public String call() throws Exception {return "three new callable thread";}});String returnString = submit.get();System.out.println(returnString);2. 總結
- 實現Runnable接口比繼承Thread類所具有的優勢:
1):適合多個相同的程序代碼的線程去處理同一個資源
2):可以避免java中的單繼承的限制
3):增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立
4):線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類
線程狀態切換
新建狀態(New):新創建了一個線程對象。
就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start()方法。該狀態的線程位于可運行線程池中,變得可運行,等待獲取CPU的使用權。
運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
-
等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。(wait會釋放持有的鎖)
-
同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中。
-
其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)
線程調度
Java線程的實現:Java線程模型是基于操作系統原生線程模型來實現的;
線程模型只對線程的并發規模和操作成本產生影響,對Java程序的編寫和運行過程來說,并沒有什么不同。
1. 線程優先級
時分形式是現代操作系統采用的基本線程調度形式,操作系統將CPU資源分為一個個的時間片,并分配給線程,線程使用獲取的時間片執行任務,時間片使用完之后,操作系統進行線程調度,其他獲得時間片的線程開始執行;那么,一個線程能夠分配得到的時間片的多少決定了線程使用多少的處理器資源,線程優先級則是決定線程可以獲得多或少的處理器資源的線程屬性;
可以通過設置線程的優先級,使得線程獲得處理器執行時間的長短有所不同,但采用這種方式來實現線程獲取處理器執行時間的長短并不可靠(因為系統的優先級和Java中的優先級不是一一對應的,有可能Java中多個線程優先級對應于系統中同一個優先級);Java中有10個線程優先級,從1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默認優先級為5;因此,程序的正確性不能夠依賴線程優先級的高低來判斷;
2. 線程調度分類
線程調度是指系統為線程分配處理器使用權的過程;主要調度方式有:搶占式線程調度、協同式線程調度;
2.1 搶占式線程調度
每個線程由系統來分配執行時間,線程的切換不由線程本身決定;Java默認使用的線程調度方式是搶占式線程調度;我們可以通過Thread.yield()使當前正在執行的線程讓出執行時間,但是,卻沒有辦法使線程去獲取執行時間;
2.2 協同式線程調度
每個線程的執行時間由線程本身來控制,線程執行完任務后主動通知系統,切換到另一個線程上;
2.3 兩種線程調度方式的優缺點
協同式的優點:實現簡單,可以通過對線程的切換控制避免線程安全問題;
協同式的缺點:一旦當前線程出現問題,將有可能影響到其他線程的執行,最終可能導致系統崩潰;
搶占式的優點:一個線程出現問題不會影響到其他線程的執行(線程的執行時間是由系統分配的,因此,系統可以將處理器執行時間分配給其他線程從而避免一個線程出現故障導致整個系統崩潰的現象發生)
2.4 結論
Java中,線程的調度策略主要是搶占式調度策略,正是因為搶占式調度策略,導致多線程程序執行過程中,實際的運行過程與我們邏輯上理解的順序存在較大的區別,也就是多線程程序的執行具有不確定性,從而會導致一些線程安全性問題的發生;
3. 調度方式
3.1 調度的方式
3.2 深入理解(重要)
sleep()
sleep(long millis): 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行)
join()
join():指等待t線程終止。
join是Thread類的一個方法,啟動線程后直接調用,即join()的作用是:等待該線程終止,也就是在子線程調用了join()方法后面的代碼,只有等到子線程結束了才能執行。
案例:
在很多情況下,主線程生成并起動了子線程,如果子線程里要進行大量的耗時的運算,主線程往往將于子線程之前結束,但是如果主線程處理完其他的事務后,需要用到子線程的處理結果,也就是主線程需要等待子線程執行完成之后再結束,這個時候就要用到join()方法了。
代碼:
package cn.javanode.thread.joinUse;/*** @author xgt(小光頭)* @version 1.0* @date 2021-1-10 9:52*/ public class JoinUseRunnableThread {static class joinThrad implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() +" 線程運行開始!");for (int i = 0; i < 5; i++) {System.out.println("子線程"+Thread.currentThread().getName() +"運行 : "+i);try {Thread.sleep((int)Math.random()*10);} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) {System.out.println("main方法的線程開啟");Thread joinThread = new Thread(new joinThrad());joinThread.setName("JoinThread");joinThread.start();//添加join 子線程調用了join()方法后面的代碼,只有等到子線程結束了才能執行。try {joinThread.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main方法的線程結束");} }yield()
yield():暫停當前正在執行的線程對象,并執行其他線程
yield()做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。
結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
package cn.javanode.thread.yieldUse; /*** @author xgt(小光頭)* @version 1.0* @date 2021-1-10 10:57*/ public class ThreadYieldDemo {static class yieldThread implements Runnable{@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println(Thread.currentThread().getName()+"runing time="+i);if(i==30){Thread.yield();}}}}public static void main(String[] args) {//yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。Thread yt1 = new Thread(new yieldThread());yt1.setName("ytthread1");Thread yt2 = new Thread(new yieldThread());yt2.setName("ytthread2");yt1.start();yt2.start();}}3.3 補充
sleep()和yield()的區別
- sleep()使當前線程進入停滯狀態,所以執行sleep()的線程在指定的時間內肯定不會被執行;
- yield()只是使當前線程重新回到可執行狀態,所以執行yield()的線程有可能在進入到可執行狀態后馬上又被執行。
補充:
sleep 方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield 方法使當前線程讓出 CPU 占有權,但讓出的時間是不可設定的。實際上,yield()方法對應了如下操作:先檢測當前是否有相同優先級的線程處于同可運行狀態,如有,則把 CPU 的占有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱為“退讓”,它把運行機會讓給了同等優先級的其他線程
- sleep 方法允許較低優先級的線程獲得運行機會,
- yield() 方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 占有權。
補充:
在一個運行系統中,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那么,較低優先級線程只能等待所有較高優先級的線程運行結束,才有機會運行。
wait和sleep區別
共同點:
多線程的環境下,都可以在程序的調用處阻塞指定的毫秒數,并返回。
wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態 ,從而使線程立刻拋出InterruptedException。
如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。
需要注意的是,InterruptedException是線程自己從內部拋出的,并不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那么該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()后,就會立刻拋出InterruptedException 。
不同點:
補充:
sleep()方法
sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸占該進程所獲的CPU資源,以留一定時間給其他線程執行的機會;
sleep()是Thread類的Static(靜態)的方法;因此他不能改變對象的機鎖,所以當在一個Synchronized塊中調用Sleep()方法時,線程雖然休眠了,但是對象的機鎖并木有被釋放,其他線程無法訪問這個對象(即使睡著也持有對象鎖)。在sleep()休眠時間期滿后,該線程不一定會立即執行,這是因為其它線程可能正在運行而且沒有被調度為放棄執行,除非此線程具有更高的優先級。
wait())方法
wait()方法是Object類里的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到后還需要返還對象鎖);其他線程可以訪問;wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程。
wiat()必須放在synchronized block中,否則會在program runtime時扔出”java.lang.IllegalMonitorStateException“異常。
wait()和notify()、notifyAll()
這三個方法用于協調多個線程對共享數據的存取,所以必須在synchronized語句塊內使用。synchronized關鍵字用于保護共享數據,阻止其他線程對共享數據的存取,但是這樣程序的流程就很不靈活了,如何才能在當前線程還沒退出synchronized數據塊時讓其他線程也有機會訪問共享數據呢?此時就用這三個方法來靈活控制。wait() 方法使當前線程暫停執行并釋放對象鎖標示,讓其他線程可以進入synchronized數據塊,當前線程被放入對象等待池中。當調用notify()方法后,將從對象的等待池中移走一個任意的線程并放到鎖標志等待池中,只有鎖標志等待池中線程能夠獲取鎖標志;如果鎖標志等待池中沒有線程,則notify()不起作用。notifyAll() 從對象等待池中移走所有等待那個對象的線程并放到鎖標志等待池中。(下面的線程間通信部分會細說)
wait,notify 和notifyAll 這些方法為什么不在 thread類里面
Java提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程來獲得 由于 wait notify和notifyAll 都是鎖級別的的操作,所以把他們定義在Object類中因為鎖屬于對象。
線程間通信(重要)
如果你的多線程程序僅僅是每個線程獨立完成各自的任務,相互之間并沒有交互和協作,那么,你的程序是無法發揮出多線程的優勢的,只有有交互的多線程程序才是有意義的程序,否則,還不如使用單線程執行多個方法實現程序來的簡單、易懂、有效!
1. java等待通知機制
場景:線程A修改了對象O的值,線程B感知到對象O的變化,執行相應的操作,這樣就是一個線程間交互的場景;可以看出,這種方式,相當于線程A是發送了消息,線程B接收到消息,進行后續操作,是不是很像生產者與消費者的關系?我們都知道,生產者與消費者模式可以實現解耦,使得程序結構上具備伸縮性;
- 一種簡單的方式是,線程B每隔一段時間就輪詢對象O是否發生變化,如果發生變化,就結束輪詢,執行后續操作;
缺點: 這種方式不能保證對象O的變更及時被線程B感知,同時,不斷地輪詢也會造成較大的開銷;分析這些問題的癥結在哪?其實,可以發現狀態的感知是拉取的,而不是推送的,因此才會導致這樣的問題產生
- Java內置的經典的等待/通知機制
那就是wait()/notify()/notifyAll(),重要 便于理解例子
/** 如果在調用了此方法之后,其他線程調用notify()或者notifyAll()方法之前,線程被中斷,則會清除中斷標志并拋出異常* 當前線程必須擁有對象O的監視器,調用了對象O的此方法會導致當前線程釋放已占有的監視器,并且等待* 其它線程對象O的notify()或者notifyAll()方法,當其它線程執行了這兩個方法中的一個之后,并且* 當前線程獲取到處理器執行權,就可以嘗試獲取監視器,進而繼續后續操作的執行*/public final void wait() throws InterruptedException {wait(0);}/**喚醒等待在對象O的監視器上的一個線程,如果多個線程等待在對象O的監視器上,那么將會選擇其中的一個進行喚醒* 被喚醒的線程只有在當前線程釋放鎖之后才能夠繼續執行.* 被喚醒的線程將會與其他線程一同競爭對象O的監視器鎖* 這個方法必須在擁有對象O的監視器的線程中進行調用* 同一個時刻,只能有一個線程擁有該對象的監視器*/public final native void notify();/***喚醒等待在對象O的監視器上的所有線程* 被喚醒的線程只有在當前線程釋放鎖之后才能夠繼續執行.* 被喚醒的線程將會與其他線程一同競爭對象O的監視器鎖* 這個方法必須在擁有對象O的監視器的線程中進行調用* 同一個時刻,只能有一個線程擁有該對象的監視器*/public final native void notifyAll();2. 經典的等待/通知機制代碼
package cn.javanode.thread.JavaWaitAndConsumer;public class WaitAndNotify {//輪詢標志位private static boolean stop = false;//監視器對應的對象private static Object monitor = new Object();//等待線程static class WaitThread implements Runnable{@Overridepublic void run() {synchronized(monitor){//循環檢測標志位是否變更while(!stop){try {//標志位未變更,進行等待 鎖釋放,整個線程等待monitor.wait();} catch (InterruptedException e) {e.printStackTrace();}}//被喚醒后獲取到對象的監視器之后執行的代碼System.out.println("1Thread "+Thread.currentThread().getName()+" is awakened at first time");stop = false;}//休眠1秒之后,線程角色轉換為喚醒線程try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//與上述代碼相反的邏輯synchronized(monitor){while(stop){try {monitor.wait();} catch (InterruptedException e) {e.printStackTrace();}}monitor.notify();stop = true;System.out.println("2Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");}}}//通知線程static class NotifyThread implements Runnable{@Overridepublic void run() {synchronized (monitor){while(stop){try {monitor.wait();} catch (InterruptedException e) {e.printStackTrace();}}stop = true;monitor.notify();System.out.println("3Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (monitor){while(!stop){try {monitor.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("4Thread "+Thread.currentThread().getName()+" is awakened at first time");}}}public static void main(String[] args){Thread waitThread = new Thread(new WaitThread());waitThread.setName("waitThread");Thread notifyThread = new Thread(new NotifyThread());notifyThread.setName("notifyThread");waitThread.start();notifyThread.start();}}通過上述代碼,可以提煉出等待通知機制的經典模式:
等待方實現步驟:
- 加鎖同步
- 條件不滿足,進入等待,被喚醒之后,繼續檢查條件是否滿足(循環檢測)
- 條件滿足,退出循環,繼續執行后續代碼
通知方實現步驟:
- 加鎖同步
- 條件不滿足,跳過循環檢測
- 設置條件并喚醒線程
3. 生產者消費者代碼
package cn.javanode.thread.JavaWaitAndConsumer;public class ProducerAndConsumer {//商品庫存private static int storeMount = 0;//監視器對應的對象private static Object monitor = new Object();//生產者線程static class ProducerThread implements Runnable{@Overridepublic void run() {try {produce();} catch (InterruptedException e) {e.printStackTrace();}}public void produce() throws InterruptedException {while(true){synchronized(monitor){//循環檢測庫存是否大于0,大于0表示還有商品可以消費,線程等待消費者消費商品while(storeMount > 0){monitor.wait();}//被喚醒后獲取到對象的監視器之后執行的代碼System.out.println("Thread "+Thread.currentThread().getName()+" begin produce goods");//生產商品storeMount = 1;//喚醒消費者monitor.notify();Thread.sleep(1000);}}}}//消費者線程static class ConsumerThread implements Runnable{@Overridepublic void run() {try {consume();} catch (InterruptedException e) {e.printStackTrace();}}public void consume() throws InterruptedException {while(true){synchronized (monitor){//檢測庫存是否不為0,如果不為0,那么有商品可供消費,否則等待生產者生產商品while(storeMount == 0){monitor.wait();}//消費商品storeMount = 0;//喚醒生產者線程monitor.notify();System.out.println("Thread "+Thread.currentThread().getName()+" begin consume goods");Thread.sleep(1000);}}}}public static void main(String[] args){Thread producerThread = new Thread(new ProducerThread());producerThread.setName("producerThread");Thread consumerThread = new Thread(new ConsumerThread());consumerThread.setName("consumerThread");producerThread.start();consumerThread.start();}}上述代碼示例演示了一個生產者生產商品和一個消費者消費商品的場景,對于一個生產者多個消費者、多個生產者一個消費者、多個生產者多個消費者等場景,只需要將喚醒的方法換為notifyAll()即可,否則,會出現饑餓現象!
4. 總結
以上就是本文敘述的所有內容,本文首先對于給出Java中線程調度形式,引出多線程編程中需要解決的線程安全問題,并分析線程安全問題,給出解決線程安全問題的常用手段(加鎖同步),最后,結合Java內置的等待通知機制,進行了樣例代碼的展示以及分析,給出了經典的等待通知機制的編程范式,最后,基于等待通知機制給A出了生產者消費者模式的實現樣例,希望本文能給想要學習多線程編程的朋友一點幫助,如有不正確的地方,還望指出,十分感謝!
注意細節(了解)
- 線程分類
- 用戶線程:大多數線程都是用戶線程,用于完成業務功能
- 守護線程:支持型線程,主要用于后臺調度以及支持性工作,比如GC線程,當JVM中不存在非守護線程時,JVM將會退出
- Thread.setDaemon(true)來設置線程屬性為守護線程,該操作必須在線程調用start()方法之前執行
- 守護線程中的finally代碼塊不一定會執行,因此不要寄托于守護線程中的finally代碼塊來完成資源的釋放
- 線程交互的方式
- join
- sleep/interrupt
- wait/notify
- 啟動線程的方式
- 只能通過線程對象調用start()方法來啟動線程
- start()方法的含義是,當前線程(父線程)同步告知虛擬機,只要線程規劃期空閑,就應該立即啟動調用了start()方法的線程
- 線程啟動前,應該設置線程名,以便使用Jstack分析程序中線程運行狀況時,起到提示性作用
- 終止線程的方式
- 中斷檢測機制
- 線程通過調用目標線程的interrupt()方法對目標線程進行中斷標志,目標線程通過檢測自身的中斷標志位(interrupted()或isInterrupted())來響應中斷,進行資源的釋放以及最后的終止線程操作;
- 拋出InterruptedException異常的方法在拋出異常之前,都會將該線程的中斷標志位清除,然后拋出異常
- suspend()/resume()(棄用)
- 調用后,線程不會釋放已經占有的資源,容易引發死鎖問題
- stop()(棄用)
- 調用之后不一定保證線程資源的釋放
- 中斷檢測機制
- 鎖釋放的情況:
- 同步方法或同步代碼塊的執行結束(正常、異常結束)
- 同步方法或同步代碼塊鎖對象調用wait方法
- 鎖不會釋放的情況:
- 調用Thead類的靜態方法yield()以及sleep()
- 調用線程對象的suspend()
治療脫發秘籍
總結
以上是生活随笔為你收集整理的脱发篇-多线程基础(下)来看看你知道多少的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 最新sql注入原因以及预防方案
- 下一篇: 最新详细的JMM内存模型(三天熬夜血肝)