您好,登錄后才能下訂單哦!
這篇文章主要講解了“線程的實現方式有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“線程的實現方式有哪些”吧!
方式一:繼承Thread類
package com.thread; //通過繼承Thread類實現自定義線程類 public class MyThread extends Thread { //線程體 @Override public void run() { System.out.println("Hello, I am the defined thread created by extends Thread"); } public static void main(String[] args){ //實例化自定義線程類實例 Thread thread = new MyThread(); //調用start()實例方法啟動線程 thread.start(); } } 123456789101112131415
優點:實現簡單,只需實例化繼承類的實例,即可使用線程 缺點:擴展性不足,Java是單繼承的語言,如果一個類已經繼承了其他類,就無法通過這種方式實現自定義線程
方式二:實現Runnable接口
package com.thread; public class MyRunnable implements Runnable { //線程體 @Override public void run() { System.out.println("Hello, I am the defined thread created by implements Runnable"); } public static void main(String[] args){ //線程的執行目標對象 MyRunnable myRunnable = new MyRunnable(); //實際的線程對象 Thread thread = new Thread(myRunnable); //啟動線程 thread.start(); } }
優點:
擴展性好,可以在此基礎上繼承其他類,實現其他必需的功能
對于多線程共享資源的場景,具有天然的支持,適用于多線程處理一份資源的場景
缺點:構造線程實例的過程相對繁瑣一點
方式三:實現Callable接口
package com.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "Hello, I am the defined thread created by implements Callable"; } public static void main(String[] args){ //線程執行目標 MyCallable myCallable = new MyCallable(); //包裝線程執行目標,因為Thread的構造函數只能接受Runnable接口的實現類,而FutureTask類實現了Runnable接口 FutureTask<String> futureTask = new FutureTask<>(myCallable); //傳入線程執行目標,實例化線程對象 Thread thread = new Thread(futureTask); //啟動線程 thread.start(); String result = null; try { //獲取線程執行結果 result = futureTask.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(result); } }
優點:
擴展性好
支持多線程處理同一份資源
具備返回值以及可以拋出受檢查異常
缺點:
相較于實現Runnable接口的方式,較為繁瑣
小結
我們對這三種方式進行分析,可以發現:方式一和方式二本質上都是通過實現Runnable接口并重寫run()方法,將接口實現類的實例傳遞給Thread線程類來執行線程體(run()方法中的實現),這里將Runnable接口實現類的實例作為線程執行目標,供線程Thread實例執行;對于方式三,其實也是這樣的,由于Thread類只能執行Runnable接口實現類的執行目標,所以需要對Callable接口的實現類進行包裝,包裝成Runnable接口的實現類(通過實現了Runnable接口的FutureTask類進行包裝),從而使得Thread類能夠接收Callable接口實現類的實例,可見這里使用了適配器模式!
綜上所述,三種實現方式都存在著一個使用范式,即首先實現線程執行目標對象(包含線程所要執行的任務),然后將目標對象作為構造參數以實例化Thread實例,來獲得線程!本質上都是實現一個線程體,由Thread來執行線程體,達到開啟線程執行任務的效果!但是,三種實現方式各有優缺點,使用時,應該結合具體需求來選用合適的實現方式進行開發!
線程的生命周期
經過上面的代碼演示,我們知道了線程如何實現,但是如果我們想要更好地使用線程,還需要對程序運行中線程的狀態以及狀態之間的轉換(即線程的生命周期)有所了解,這樣才能在多線程程序運行出現問題時,分析問題產生的原因,從而快速準確地定位并解決問題!
首先,看一下Thread類中給出的關于線程狀態的說明:
/** * 線程生命周期中的的六種狀態 * NEW:還沒有調用start()的線程實例所處的狀態 * RUNNABLE:正在虛擬機中執行的線程所處的狀態 * BLOCKED:等待在監視器鎖上的線程所處的狀態 * WAITING:等待其它線程執行特定操作的線程所處的狀態 * TIMED_WAITING:等待其它線程執行超時操作的線程所處的狀態 * TERMINATED:退出的線程所處的狀態 * 給定時間點,一個線程只會處于以下狀態中的一個,這些狀態僅僅是虛擬機層面的線程狀態,并不能反映任何操作系統中線程的狀態 */ public enum State { //還沒有調用start()開啟的線程實例所處的狀態 NEW, //正在虛擬機中執行或者等待被執行的線程所處的狀態,但是這種狀態也包含線程正在等待處理器資源這種情況 RUNNABLE, // 等待在監視器鎖上的線程所處的狀態,比如進入synchronized同步代碼塊或同步方法失敗 BLOCKED, // 等待其它線程執行特定操作的線程所處的狀態;比如線程執行了以下方法: Object.wait with no timeout、Thread.join with no timeout、 LockSupport.park WAITING, // 等待其它線程執行超時操作的線程所處的狀態;比如線程執行了以下方法: Thread.sleep、Object.wait with timeout //Thread.join with timeout、LockSupport.parkNanos、LockSupport.parkUntil TIMED_WAITING, //退出的線程所處的狀態 TERMINATED; }
新建(New):當線程實例被new出來之后,調用start()方法之前,線程實例處于新建狀態
可運行(Runnable):當線程實例調用start()方法之后,線程調度器分配處理器資源之前,線程實例處于可運行狀態或者線程調度器分配處理器資源給線程之后,線程實例處于運行中狀態,這兩種情況都屬于可運行狀態
等待(Waitting):當線程處于運行狀態時,線程執行了obj.wait()或Thread.join()方法、Thread.join、LockSupport.park以及Thread.sleep()時,線程處于等待狀態
超時等待(Timed Waitting):當線程處于運行狀態時,線程執行了obj.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil以及Thread.sleep(long)方法時,線程處于超時等待狀態
阻塞(Blocked):當線程處于運行狀態時,獲取鎖失敗,線程實例進入等待隊列,同時狀態變為阻塞
終止(Terminated):當線程執行完畢或出現異常提前結束時,線程進入終止狀態
線程的狀態轉換
上面也提到了,某一時間點線程的狀態只能是上述6個狀態中的其中一個;但是,線程在程序運行過程中的狀態是會發生變化的,由一個狀態轉變為另一個狀態,那么下面給出線程狀態轉換圖幫助我們清晰地理解線程的狀態轉變過程:
上面我們已經對線程的實現以及線程的狀態有了較為清晰的認識,那么通過上述內容,我們也可以發現其實有很多方法,我們并沒有詳細地介紹,比如start()、yield()、wait()、notify()、notifyAll()、sleep()、join()等等,這些方法大多來源于JDK中Thread類這一關鍵的線程類中,下面結合Thread類的源碼看一下,多線程編程中經常遇到的方法有哪些,以及這些方法的用途;
線程類Thread源碼
實例同步方法:join()
/** * 等待調用此方法的線程執行結束 * @throws InterruptedException 如果任何線程中斷了當前線程,將會拋出此異常,同時將中斷標志位清除 */ public final void join() throws InterruptedException { join(0); } /** * 最多等待millis毫秒,時間一到無論是否執行完畢,都會返回 * 如果millis為0,那么意味著一直等到線程執行完畢才會返回 * 此方法的實現是基于循環檢測當前線程是否存活來判斷是否調用當前實例的wait方法來實現的 * @param millis 等待時間 * @throws IllegalArgumentException 非法參數異常 * @throws InterruptedException 如果任何線程中斷了當前線程,將會拋出此異常,同時將中斷標志位清除 */ public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } } /** * 線程執行結束之前最多等待millis毫秒nanos納秒 * 此方法基于循環判斷isAlive返回值來決定是否調用wait方法來實現 * 隨著一個線程終止,將會調用notifyAll方法 * 所以建議不要在當前實例上調用 wait、 notify、 notifyAll */ public final synchronized void join(long millis, int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } join(millis); }
中斷方法以及檢測中斷方法和判活方法:
/** * 中斷當前線程 * 如果當前線程阻塞在Object的wait()、wait(long)、wait(long, int),或者 * join()、join(long)、join(long, int)以及sleep(long)、sleep(long, int)等方法 * 那么將會清除中斷標志位并受到一個中斷異常 * 非靜態方法 */ public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag b.interrupt(this); return; } } interrupt0(); } /** * 檢測當前線程是否已經被中斷,此方法會清除當前線程的中斷標志 * 也就是說,如果這個方法被連續調用兩次,并且第一次調用之前,線程被中斷過,那么第一次調用返回true,第二次返回false * @return <code>true</code> 如果當前線程已經被中斷,返回true,否則返回false * 靜態方法 */ public static boolean interrupted() { return currentThread().isInterrupted(true); } /** * 檢測當前線程是否已經被中斷,此方法不會清除當前線程的中斷標志 * 非靜態方法 */ public boolean isInterrupted() { return isInterrupted(false); } /** * 根據參數值決定是否在判斷中斷標志位之后清除標志位 * 實例方法 */ private native boolean isInterrupted(boolean ClearInterrupted); /** * 檢測一個線程是否還存活,存活指的是已經啟動但還沒有終止 * 實例方法 */ public final native boolean isAlive();
線程調度
Java線程的實現:Java線程模型是基于操作系統原生線程模型來實現的; 線程模型只對線程的并發規模和操作成本產生影響,對Java程序的編寫和運行過程來說,并沒有什么不同;
線程優先級
時分形式是現代操作系統采用的基本線程調度形式,操作系統將CPU資源分為一個個的時間片,并分配給線程,線程使用獲取的時間片執行任務,時間片使用完之后,操作系統進行線程調度,其他獲得時間片的線程開始執行;那么,一個線程能夠分配得到的時間片的多少決定了線程使用多少的處理器資源,線程優先級則是決定線程可以獲得多或少的處理器資源的線程屬性;
可以通過設置線程的優先級,使得線程獲得處理器執行時間的長短有所不同,但采用這種方式來實現線程獲取處理器執行時間的長短并不可靠(因為系統的優先級和Java中的優先級不是一一對應的,有可能Java中多個線程優先級對應于系統中同一個優先級);Java中有10個線程優先級,從1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默認優先級為5; 因此,程序的正確性不能夠依賴線程優先級的高低來判斷;
## 線程調度
線程調度是指系統為線程分配處理器使用權的過程;主要調度方式有:搶占式線程調度、協同式線程調度;
搶占式線程調度
每個線程由系統來分配執行時間,線程的切換不由線程本身決定;Java默認使用的線程調度方式是搶占式線程調度;我們可以通過Thread.yield()使當前正在執行的線程讓出執行時間,但是,卻沒有辦法使線程去獲取執行時間;
### 協同式線程調度
每個線程的執行時間由線程本身來控制,線程執行完任務后主動通知系統,切換到另一個線程上;
兩種線程調度方式的優缺點
協同式的優點:實現簡單,可以通過對線程的切換控制避免線程安全問題; 協同式的缺點:一旦當前線程出現問題,將有可能影響到其他線程的執行,最終可能導致系統崩潰; 搶占式的優點:一個線程出現問題不會影響到其他線程的執行(線程的執行時間是由系統分配的,因此,系統可以將處理器執行時間分配給其他線程從而避免一個線程出現故障導致整個系統崩潰的現象發生);
結論
在Java中,線程的調度策略主要是搶占式調度策略,正是因為搶占式調度策略,導致多線程程序執行過程中,實際的運行過程與我們邏輯上理解的順序存在較大的區別,也就是多線程程序的執行具有不確定性,從而會導致一些線程安全性問題的發生;那么,什么是線程安全呢?
線程安全
線程安全的定義
簡單來說,線程安全就是對于多個線程并發執行的操作不需要進行任何外部的控制,也不需要進行任何的協調,就能夠保證程序的執行結果與開發人員的預期結果保持一致,那么這個多線程程序就是線程安全的; 注意: 線程安全問題一定是基于多個線程之間存在訪問共享數據這一前提下的;如果多個線程之間不會訪問同一個變量,那么就不存在線程安全的問題;
線程安全的分類
線程安全這一概念并不僅僅分為線程安全和非線程安全,按照線程安全的強弱程度可以將各種共享變量的操作分為:不可變、絕對線程安全、相對線程安全、線程兼容以及線程對立這五種情況;
不可變:如果共享變量是不可變的對象,那么對該共享變量的多線程操作一定是線程安全的,因為對象是不可變的,所以任何線程都不可以改變共享變量的狀態,也就不會出現臟讀等現象;
如果共享變量是一個基本數據類型的變量,那么可以使用final關鍵字保證其是不可變的;
如果共享變量是一個對象,那么就需要保證對象的行為不會改變該對象的狀態,可以將一個類的所有字段使用final關鍵字修飾,那么就可以保證該類的對象是不可變的,如java.lang.String類;
絕對線程安全:不需要在調用端進行任何同步處理,就能保證代碼在多線程并發的場景下保證線程安全的,即多線程并發執行的結果符合預期的結果;Java API中標注為線程安全的類,大多數都不是絕對線程安全;
相對線程安全:Java API中標注為線程安全的類,大多數都是相對的線程安全,也就是通常意義上的線程安全,保證對共享變量單獨操作時是線程安全的,調用時可以不用額外的保障措施;例如Vector、HashTable或通過Collections的synchronizedCollection()方法包裝的集合等;
線程兼容:線程兼容指對象本身并不是線程安全的,但是**可以通過在調用端正確采用同步手段來保證對象在并發環境中可以安全地使用,是通常意義上的非線程安全;Java API中的大部分類都是線程兼容的,**例如ArrayList、HashMap等;
線程對立:無論調用端采用什么同步措施都不能保證多線程環境中的線程安全;線程對立很少出現;
線程安全問題的解決方法
介紹了線程的調度原理之后,其實可以分析出線程安全問題的起因在于多線程的執行順序具有不確定性,那么當多個線程同時操作一份資源就不出現意想不到的情況,而編譯器和處理器會對執行的指令進行重排序,這些因素導致了線程安全問題;
那么,在實際開發中,我們一般需要解決的都是上述的相對線程安全以及線程兼容這兩種線程安全性問題;那么,對于這兩類問題,又可以細分為可見性、原子性以及有序性這三類問題;這里暫且先不進行細分,就線程安全問題,我們給出常用解決措施;
線程安全問題重現
下面結合具體的代碼來看一下使用多線程編程時可能出現的線程安全問題:
package com.thread; public class ThreadSafe implements Runnable{ //靜態變量,所有對象共享 private static int count = 0; @Override public void run() { for(int i = 0 ; i < 100 ; i++){ count(); } } public void count(){ try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public static void main(String[] args) throws InterruptedException { ThreadSafe threadSafe1 = new ThreadSafe(); ThreadSafe threadSafe2 = new ThreadSafe(); Thread thread1 = new Thread(threadSafe1); Thread thread2 = new Thread(threadSafe2); thread1.start(); thread2.start(); Thread.currentThread().sleep(1000); System.out.println(count); } }
運行結果:
這一段代碼的目的是開啟兩個線程對同一個變量分別進行100次的累加,按照正常的邏輯(串行化執行),累加后的結果應該為200,但是實際輸出的結果卻是190,顯然這和我們的預期結果不同,這就是線程安全問題;我們分析一下,為什么會出現這樣的情況,之前提到過,多線程執行的時候代碼執行的順序具有不確定性,那么就可能出現,線程1(thread1)在獲取到count的值之后,CPU執行權被分配給了線程2(thread2),線程2獲取到的值與線程1獲取到的相同,那么兩個線程累加操作執行后,相當于只累加來一次,這樣就會導致線程不安全問題產生;那么,如何解決這個問題,我們可以利用Java中的synchronized關鍵字對線程體進行同步,代碼如下:
package com.thread; public class ThreadSafeTwo implements Runnable{ //靜態變量,所有對象共享 private static int count = 0; @Override public void run() { //這里對線程體進行同步 synchronized(ThreadSafeTwo.class){ for(int i = 0 ; i < 100 ; i++){ count(); } } } public void count(){ try { Thread.currentThread().sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public static void main(String[] args) throws InterruptedException { ThreadSafeTwo threadSafe = new ThreadSafeTwo(); Thread thread1 = new Thread(threadSafe); Thread thread2 = new Thread(threadSafe); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(count); } }
同步處理后代碼執行的結果如下:
顯然,經過同步后的代碼,就可以保證多線程并發執行的情況下,結果依然符合預期結果;關于synchronized關鍵字的實現原理將會另起一文進行分析,下面我們看一下,synchronized關鍵字的使用方式有哪些?
** synchronized關鍵字的使用方式**
synchronized同步代碼塊
鎖的對象為指定的對象
synchronized同步實例方法
鎖的對象為當前實例
synchronized同步靜態方法
鎖的對象為Class對象
線程安全的單例模式實現
package com.thread; public class SingleTonThreadSafe { //屬性私有化,volatile實現內存可見性、禁止指令重排序 private volatile static SingleTonThreadSafe singleTonThreadSafe = null; //無參構造函數私有化 private SingleTonThreadSafe(){} //靜態方法外部使用,獲取對象實例 public static SingleTonThreadSafe getInstance(){ //第一次判斷,避免不必要的加鎖 if(singleTonThreadSafe == null){ //同步實例化代碼塊 synchronized(SingleTonThreadSafe.class){ //再次檢測,避免其它線程已經實例化 if(singleTonThreadSafe == null){ //實例化,其他線程立即可見 singleTonThreadSafe = new SingleTonThreadSafe(); } } } return singleTonThreadSafe; } }
synchronized同步鎖的使用注意點
死鎖
定義:多個線程互相等待已被對方占有的鎖,同時都不釋放自己已經占有的鎖,導致線程之間陷入僵持,致使系統不可用
形成條件:互斥鎖、鎖只能主動釋放、循環等待
避免策略:順序加鎖、超時獲取自動放棄、死鎖檢測
活鎖
定義:線程等待被其他線程喚醒,但是實際沒有線程來喚醒,導致線程一直無法恢復到運行狀態
避免策略:編程時有等待,就必須有對應的喚醒
線程間通信
如果你的多線程程序僅僅是每個線程獨立完成各自的任務,相互之間并沒有交互和協作,那么,你的程序是無法發揮出多線程的優勢的,只有有交互的多線程程序才是有意義的程序,否則,還不如使用單線程執行多個方法實現程序來的簡單、易懂、有效!
那么,線程間進行交互通信的手段有哪些呢?下面,將給出常用的多線程通信的實現手段以及相應的代碼示例,并結合具體的代碼進行分析,對其中需要注意的地方進行突出提示;
等待通知機制
我們先看這樣一個場景:線程A修改了對象O的值,線程B感知到對象O的變化,執行相應的操作,這樣就是一個線程間交互的場景;可以看出,這種方式,相當于線程A是發送了消息,線程B接收到消息,進行后續操作,是不是很像生產者與消費者的關系?我們都知道,生產者與消費者模式可以實現解耦,使得程序結構上具備伸縮性;那么Java中如何實現這種功能呢?
一種簡單的方式是,線程B每隔一段時間就輪詢對象O是否發生變化,如果發生變化,就結束輪詢,執行后續操作;
但是,這種方式不能保證對象O的變更及時被線程B感知,同時,不斷地輪詢也會造成較大的開銷;分析這些問題的癥結在哪?其實,可以發現狀態的感知是拉取的,而不是推送的,因此才會導致這樣的問題產生;
那么,我們就會思考,如何將拉取變為推送來實現這樣的功能呢?
這就引出了Java內置的經典的等待/通知機制,通過查看Object類的源碼發現,該類中有三個方法,我們一般不會使用,但是在多線程編程中,這三個方法卻是能夠大放異彩的!那就是wait()/notify()/notifyAll();
/** * 調用此方法會導致當前線程進入等待狀態直到其它線程調用同一對象的notify()或者notifyAll()方法 * 當前線程必須擁有對象O的監視器,調用了對象O的此方法會導致當前線程釋放已占有的監視器,并且等待 * 其它線程對象O的notify()或者notifyAll()方法,當其它線程執行了這兩個方法中的一個之后,并且 * 當前線程獲取到處理器執行權,就可以嘗試獲取監視器,進而繼續后續操作的執行 * 推薦使用方式: * synchronized (obj) { * while (<condition does not hold>) * obj.wait(); * ... // Perform action appropriate to condition * } * @throws IllegalMonitorStateException 如果當前線程沒有獲取到對象O的監視器時,拋出異常 * @throws InterruptedException 如果在調用了此方法之后,其他線程調用notify()或者notifyAll() * 方法之前,線程被中斷,則會清除中斷標志并拋出異常 */ public final void wait() throws InterruptedException { wait(0); } /** * 喚醒等待在對象O的監視器上的一個線程,如果多個線程等待在對象O的監視器上,那么將會選擇其中的一個進行喚醒 * 被喚醒的線程只有在當前線程釋放鎖之后才能夠繼續執行. * 被喚醒的線程將會與其他線程一同競爭對象O的監視器鎖 * 這個方法必須在擁有對象O的監視器的線程中進行調用 * 同一個時刻,只能有一個線程擁有該對象的監視器 * @throws IllegalMonitorStateException 如果當前線程沒有獲取到對象O的監視器時,拋出異常 */ public final native void notify(); /** * 喚醒等待在對象O的監視器上的所有線程 * 被喚醒的線程只有在當前線程釋放鎖之后才能夠繼續執行. * 被喚醒的線程將會與其他線程一同競爭對象O的監視器鎖 * 這個方法必須在擁有對象O的監視器的線程中進行調用 * 同一個時刻,只能有一個線程擁有該對象的監視器 * @throws IllegalMonitorStateException 如果當前線程沒有獲取到對象O的監視器時,拋出異常 */ public final native void notifyAll();
下面看一下如何通過這三個方法實現經典的等待通知機制吧! 按照JDK中推薦的使用方式實現了等待通知樣例代碼如下:
package com.thread; public class WaitAndNotify { //輪詢標志位 private static boolean stop = false; //監視器對應的對象 private static Object monitor = new Object(); //等待線程 static class WaitThread implements Runnable{ @Override public void run() { synchronized(monitor){ //循環檢測標志位是否變更 while(!stop){ try { //標志位未變更,進行等待 monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //被喚醒后獲取到對象的監視器之后執行的代碼 System.out.println("Thread "+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("Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time"); } } } //通知線程 static class NotifyThread implements Runnable{ @Override public void run() { synchronized (monitor){ while(stop){ try { monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } stop = true; monitor.notify(); System.out.println("Thread "+ 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("Thread "+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(); } }
通過上述代碼,可以提煉出等待通知機制的經典模式:
等待方實現步驟:
加鎖同步
條件不滿足,進入等待,被喚醒之后,繼續檢查條件是否滿足(循環檢測)
條件滿足,退出循環,繼續執行后續代碼
對應的偽代碼:
synchronized(obj){ while(condition不滿足){ obj.wait(); } //后續操作 } 123456
通知方實現步驟:
加鎖同步
條件不滿足,跳過循環檢測
設置條件并喚醒線程
對應的偽代碼:
synchronized(obj){ while(condition不滿足){ obj.wait(); } 更新condition obj.notify(); //后續操作 }
生產者消費者模式
基于等待通知機制,我們可以很容易地寫出生產者消費者模式的代碼,下面給出一個實現樣例代碼:
package com.thread; public class ProducerAndConsumer { //商品庫存 private static int storeMount = 0; //監視器對應的對象 private static Object monitor = new Object(); //生產者線程 static class ProducerThread implements Runnable{ @Override public 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{ @Override public 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()即可,否則,會出現饑餓現象!
總結
以上就是本文敘述的所有內容,本文首先對于給出Java中線程調度形式,引出多線程編程中需要解決的線程安全問題,并分析線程安全問題,給出解決線程安全問題的常用手段(加鎖同步),最后,結合Java內置的等待通知機制,進行了樣例代碼的展示以及分析,給出了經典的等待通知機制的編程范式,最后,基于等待通知機制給出了生產者消費者模式的實現樣例,希望本文能給想要學習多線程編程的朋友一點幫助,如有不正確的地方,還望指出,十分感謝!
注意細節
線程分類
Thread.setDaemon(true)來設置線程屬性為守護線程,該操作必須在線程調用start()方法之前執行
守護線程中的finally代碼塊不一定會執行,因此不要寄托于守護線程中的finally代碼塊來完成資源的釋放
用戶線程:大多數線程都是用戶線程,用于完成業務功能
守護線程:支持型線程,主要用于后臺調度以及支持性工作,比如GC線程,當JVM中不存在非守護線程時,JVM將會退出
線程交互的方式
join
sleep/interrupt
wait/notify
啟動線程的方式
只能通過線程對象調用start()方法來啟動線程
start()方法的含義是,當前線程(父線程)同步告知虛擬機,只要線程規劃期空閑,就應該立即啟動調用了start()方法的線程
線程啟動前,應該設置線程名,以便使用Jstack分析程序中線程運行狀況時,起到提示性作用
終止線程的方式
調用之后不一定保證線程資源的釋放
調用后,線程不會釋放已經占有的資源,容易引發死鎖問題
線程通過調用目標線程的interrupt()方法對目標線程進行中斷標志,目標線程通過檢測自身的中斷標志位(interrupted()或isInterrupted())來響應中斷,進行資源的釋放以及最后的終止線程操作;
拋出InterruptedException異常的方法在拋出異常之前,都會將該線程的中斷標志位清除,然后拋出異常
中斷檢測機制
suspend()/resume()(棄用)
stop()(棄用)
鎖釋放的情況:
同步方法或同步代碼塊的執行結束(正常、異常結束)
同步方法或同步代碼塊鎖對象調用wait方法
鎖不會釋放的情況:
調用Thead類的靜態方法yield()以及sleep()
調用線程對象的suspend()
感謝各位的閱讀,以上就是“線程的實現方式有哪些”的內容了,經過本文的學習后,相信大家對線程的實現方式有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。