天天看點

卷妹帶你回顧Java基礎(一)每日更新Day8

卷妹帶你回顧Java基礎(一)每日更新Day8

👩‍💻部落格首頁:京與舊鋪的部落格首頁

✨歡迎關注🖱點贊🎀收藏⭐留言✒

🔮本文由京與舊鋪原創

😘系列專欄:java學習

👕參考網站:牛客網

💻首發時間:🎞2022年8月14日🎠

🎨你做三四月的事,八九月就會有答案,一起加油吧

🀄如果覺得部落客的文章還不錯的話,請三連支援一下部落客哦

🎧最後的話,作者是一個新人,在很多方面還做的不好,歡迎大佬指正,一起學習哦,沖沖沖

💬推薦一款模拟面試、刷題神器👉​​點選進入網站​​

🛒導航小助手🎪

文章目錄

  • ​​卷妹帶你回顧Java基礎(一)每日更新Day8​​
  • ​​🛒導航小助手🎪​​
  • ​​4.1 建立線程有哪幾種方式?​​
  • ​​4.2 說說Thread類的常用方法​​
  • ​​4.3 run()和start()有什麼差別?​​
  • ​​4.4 線程是否可以重複啟動,會有什麼後果?​​
  • ​​4.5 介紹一下線程的生命周期​​
  • ​​4.6 如何實作線程同步?​​
  • ​​4.7 說一說Java多線程之間的通信方式​​
  • ​​4.8 說一說Java同步機制中的wait和notify​​
  • ​​4.9 說一說sleep()和wait()的差別​​
  • ​​4.10 說一說notify()、notifyAll()的差別​​

4.1 建立線程有哪幾種方式?

參考答案

建立線程有三種方式,分别是繼承Thread類、實作Runnable接口、實作Callable接口。

通過繼承Thread類來建立并啟動線程的步驟如下:

  1. 定義Thread類的子類,并重寫該類的run()方法,該run()方法将作為線程執行體。
  2. 建立Thread子類的執行個體,即建立了線程對象。
  3. 調用線程對象的start()方法來啟動該線程。

通過實作Runnable接口來建立并啟動線程的步驟如下:

  1. 定義Runnable接口的實作類,并實作該接口的run()方法,該run()方法将作為線程執行體。
  2. 建立Runnable實作類的執行個體,并将其作為Thread的target來建立Thread對象,Thread對象為線程對象。
  3. 調用線程對象的start()方法來啟動該線程。

通過實作Callable接口來建立并啟動線程的步驟如下:

  1. 建立Callable接口的實作類,并實作call()方法,該call()方法将作為線程執行體,且該call()方法有傳回值。然後再建立Callable實作類的執行個體。
  2. 使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的傳回值。
  3. 使用FutureTask對象作為Thread對象的target建立并啟動新線程。
  4. 調用FutureTask對象的get()方法來獲得子線程執行結束後的傳回值。

擴充閱讀

通過繼承Thread類、實作Runnable接口、實作Callable接口都可以實作多線程,不過實作Runnable接口與實作Callable接口的方式基本相同,隻是Callable接口裡定義的方法有傳回值,可以聲明抛出異常而已。是以可以将實作Runnable接口和實作Callable接口歸為一種方式。

采用實作Runnable、Callable接口的方式建立多線程的優缺點:

  • 線程類隻是實作了Runnable接口或Callable接口,還可以繼承其他類。
  • 在這種方式下,多個線程可以共享同一個target對象,是以非常适合多個相同線程來處理同一份資源的情況,進而可以将CPU、代碼和資料分開,形成清晰的模型,較好地展現了面向對象的思想。
  • 劣勢是,程式設計稍稍複雜,如果需要通路目前線程,則必須使用Thread.currentThread()方法。

采用繼承Thread類的方式建立多線程的優缺點:

  • 劣勢是,因為線程類已經繼承了Thread類,是以不能再繼承其他父類。
  • 優勢是,編寫簡單,如果需要通路目前線程,則無須使用Thread.currentThread()方法,直接使用this即可獲得目前線程。

鑒于上面分析,是以一般推薦采用實作Runnable接口、Callable接口的方式來建立多線程。

4.2 說說Thread類的常用方法

參考答案

Thread類常用構造方法:

  • Thread()
  • Thread(String name)
  • Thread(Runnable target)
  • Thread(Runnable target, String name)

其中,參數 name為線程名,參數 target為包含線程體的目标對象。

Thread類常用靜态方法:

  • currentThread():傳回目前正在執行的線程;
  • interrupted():傳回目前執行的線程是否已經被中斷;
  • sleep(long millis):使目前執行的線程睡眠多少毫秒數;
  • yield():使目前執行的線程自願暫時放棄對處理器的使用權并允許其他線程執行;

Thread類常用執行個體方法:

  • getId():傳回該線程的id;
  • getName():傳回該線程的名字;
  • getPriority():傳回該線程的優先級;
  • interrupt():使該線程中斷;
  • isInterrupted():傳回該線程是否被中斷;
  • isAlive():傳回該線程是否處于活動狀态;
  • isDaemon():傳回該線程是否是守護線程;
  • setDaemon(boolean on):将該線程标記為守護線程或使用者線程,如果不标記預設是非守護線程;
  • setName(String name):設定該線程的名字;
  • setPriority(int newPriority):改變該線程的優先級;
  • join():等待該線程終止;
  • join(long millis):等待該線程終止,至多等待多少毫秒數。

4.3 run()和start()有什麼差別?

參考答案

run()方法被稱為線程執行體,它的方法體代表了線程需要完成的任務,而start()方法用來啟動線程。

調用start()方法啟動線程時,系統會把該run()方法當成線程執行體來處理。但如果直接調用線程對象的run()方法,則run()方法立即就會被執行,而且在run()方法傳回之前其他線程無法并發執行。也就是說,如果直接調用線程對象的run()方法,系統把線程對象當成一個普通對象,而run()方法也是一個普通方法,而不是線程執行體。

4.4 線程是否可以重複啟動,會有什麼後果?

參考答案

隻能對處于建立狀态的線程調用start()方法,否則将引發IllegalThreadStateException異常。

擴充閱讀

當程式使用new關鍵字建立了一個線程之後,該線程就處于建立狀态,此時它和其他的Java對象一樣,僅僅由Java虛拟機為其配置設定記憶體,并初始化其成員變量的值。此時的線程對象沒有表現出任何線程的動态特征,程式也不會執行線程的線程執行體。

當線程對象調用了start()方法之後,該線程處于就緒狀态,Java虛拟機會為其建立方法調用棧和程式計數器,處于這個狀态中的線程并沒有開始運作,隻是表示該線程可以運作了。至于該線程何時開始運作,取決于JVM裡線程排程器的排程。

4.5 介紹一下線程的生命周期

參考答案

線上程的生命周期中,它要經過建立(New)、就緒(Ready)、運作(Running)、阻塞(Blocked)和死亡(Dead)5種狀态。尤其是當線程啟動以後,它不可能一直“霸占”着CPU獨自運作,是以CPU需要在多條線程之間切換,于是線程狀态也會多次在運作、就緒之間切換。

當程式使用new關鍵字建立了一個線程之後,該線程就處于建立狀态,此時它和其他的Java對象一樣,僅僅由Java虛拟機為其配置設定記憶體,并初始化其成員變量的值。此時的線程對象沒有表現出任何線程的動态特征,程式也不會執行線程的線程執行體。

當線程對象調用了start()方法之後,該線程處于就緒狀态,Java虛拟機會為其建立方法調用棧和程式計數器,處于這個狀态中的線程并沒有開始運作,隻是表示該線程可以運作了。至于該線程何時開始運作,取決于JVM裡線程排程器的排程。

如果處于就緒狀态的線程獲得了CPU,開始執行run()方法的線程執行體,則該線程處于運作狀态,如果計算機隻有一個CPU,那麼在任何時刻隻有一個線程處于運作狀态。當然,在一個多處理器的機器上,将會有多個線程并行執行;當線程數大于處理器數時,依然會存在多個線程在同一個CPU上輪換的現象。

當一個線程開始運作後,它不可能一直處于運作狀态,線程在運作過程中需要被中斷,目的是使其他線程獲得執行的機會,線程排程的細節取決于底層平台所采用的政策。對于采用搶占式政策的系統而言,系統會給每個可執行的線程一個小時間段來處理任務。當該時間段用完後,系統就會剝奪該線程所占用的資源,讓其他線程獲得執行的機會。當發生如下情況時,線程将會進入阻塞狀态:

  • 線程調用sleep()方法主動放棄所占用的處理器資源。
  • 線程調用了一個阻塞式IO方法,在該方法傳回之前,該線程被阻塞。
  • 線程試圖獲得一個同步螢幕,但該同步螢幕正被其他線程所持有。
  • 線程在等待某個通知(notify)。
  • 程式調用了線程的suspend()方法将該線程挂起。但這個方法容易導緻死鎖,是以應該盡量避免使用該方法。

針對上面幾種情況,當發生如下特定的情況時可以解除上面的阻塞,讓該線程重新進入就緒狀态:

  • 調用sleep()方法的線程經過了指定時間。
  • 線程調用的阻塞式IO方法已經傳回。
  • 線程成功地獲得了試圖取得的同步螢幕。
  • 線程正在等待某個通知時,其他線程發出了一個通知。
  • 處于挂起狀态的線程被調用了resume()恢複方法。

線程會以如下三種方式結束,結束後就處于死亡狀态:

  • run()或call()方法執行完成,線程正常結束。
  • 線程抛出一個未捕獲的Exception或Error。
  • 直接調用該線程的stop()方法來結束該線程,該方法容易導緻死鎖,通常不推薦使用。

擴充閱讀

線程5種狀态的轉換關系,如下圖所示:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-z2NPbMhU-1660799181479)(https://uploadfiles.nowcoder.com/images/20220224/4107856_1645689998870/B6700F052B8C52C28D140E41896513FE)]

4.6 如何實作線程同步?

參考答案

  1. 同步方法

    即有synchronized關鍵字修飾的方法,由于java的每個對象都有一個内置鎖,當用此關鍵字修飾方法時, 内置鎖會保護整個方法。在調用該方法前,需要獲得内置鎖,否則就處于阻塞狀态。需要注意, synchronized關鍵字也可以修飾靜态方法,此時如果調用該靜态方法,将會鎖住整個類。

  2. 同步代碼塊

    即有synchronized關鍵字修飾的語句塊,被該關鍵字修飾的語句塊會自動被加上内置鎖,進而實作同步。需值得注意的是,同步是一種高開銷的操作,是以應該盡量減少同步的内容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。

  3. ReentrantLock

    Java 5新增了一個java.util.concurrent包來支援同步,其中ReentrantLock類是可重入、互斥、實作了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行為和語義,并且擴充了其能力。需要注意的是,ReentrantLock還有一個可以建立公平鎖的構造方法,但由于能大幅度降低程式運作效率,是以不推薦使用。

  4. volatile

    volatile關鍵字為域變量的通路提供了一種免鎖機制,使用volatile修飾域相當于告訴虛拟機該域可能會被其他線程更新,是以每次使用該域就要重新計算,而不是使用寄存器中的值。需要注意的是,volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。

  5. 原子變量

    在java的util.concurrent.atomic包中提供了建立了原子類型變量的工具類,使用該類可以簡化線程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在應用程式中(如以原子方式增加的計數器),但不能用于替換Integer。可擴充Number,允許那些處理機遇數字類的工具和實用工具進行統一通路。

4.7 說一說Java多線程之間的通信方式

參考答案

在Java中線程通信主要有以下三種方式:

  1. wait()、notify()、notifyAll()

    如果線程之間采用synchronized來保證線程安全,則可以利用wait()、notify()、notifyAll()來實作線程通信。這三個方法都不是Thread類中所聲明的方法,而是Object類中聲明的方法。原因是每個對象都擁有鎖,是以讓目前線程等待某個對象的鎖,當然應該通過這個對象來操作。并且因為目前線程可能會等待多個線程的鎖,如果通過線程來操作,就非常複雜了。另外,這三個方法都是本地方法,并且被final修飾,無法被重寫。

    wait()方法可以讓目前線程釋放對象鎖并進入阻塞狀态。notify()方法用于喚醒一個正在等待相應對象鎖的線程,使其進入就緒隊列,以便在目前線程釋放鎖後競争鎖,進而得到CPU的執行。notifyAll()用于喚醒所有正在等待相應對象鎖的線程,使它們進入就緒隊列,以便在目前線程釋放鎖後競争鎖,進而得到CPU的執行。

    每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列。就緒隊列存儲了已就緒(将要競争鎖)的線程,阻塞隊列存儲了被阻塞的線程。當一個阻塞線程被喚醒後,才會進入就緒隊列,進而等待CPU的排程。反之,當一個線程被wait後,就會進入阻塞隊列,等待被喚醒。

  2. await()、signal()、signalAll()

    如果線程之間采用Lock來保證線程安全,則可以利用await()、signal()、signalAll()來實作線程通信。這三個方法都是Condition接口中的方法,該接口是在Java 1.5中出現的,它用來替代傳統的wait+notify實作線程間的協作,它的使用依賴于 Lock。相比使用wait+notify,使用Condition的await+signal這種方式能夠更加安全和高效地實作線程間協作。

    Condition依賴于Lock接口,生成一個Condition的基本代碼是lock.newCondition() 。 必須要注意的是,Condition 的 await()/signal()/signalAll() 使用都必須在lock保護之内,也就是說,必須在lock.lock()和lock.unlock之間才可以使用。事實上,await()/signal()/signalAll() 與 wait()/notify()/notifyAll()有着天然的對應關系。即:Conditon中的await()對應Object的wait(),Condition中的signal()對應Object的notify(),Condition中的signalAll()對應Object的notifyAll()。

  3. BlockingQueue

    Java 5提供了一個BlockingQueue接口,雖然BlockingQueue也是Queue的子接口,但它的主要用途并不是作為容器,而是作為線程通信的工具。BlockingQueue具有一個特征:當生産者線程試圖向BlockingQueue中放入元素時,如果該隊列已滿,則該線程被阻塞;當消費者線程試圖從BlockingQueue中取出元素時,如果該隊列已空,則該線程被阻塞。

    程式的兩個線程通過交替向BlockingQueue中放入元素、取出元素,即可很好地控制線程的通信。線程之間需要通信,最經典的場景就是生産者與消費者模型,而BlockingQueue就是針對該模型提供的解決方案。

4.8 說一說Java同步機制中的wait和notify

參考答案

wait()、notify()、notifyAll()用來實作線程之間的通信,這三個方法都不是Thread類中所聲明的方法,而是Object類中聲明的方法。原因是每個對象都擁有鎖,是以讓目前線程等待某個對象的鎖,當然應該通過這個對象來操作。并且因為目前線程可能會等待多個線程的鎖,如果通過線程來操作,就非常複雜了。另外,這三個方法都是本地方法,并且被final修飾,無法被重寫,并且隻有采用synchronized實作線程同步時才能使用這三個方法。

wait()方法可以讓目前線程釋放對象鎖并進入阻塞狀态。notify()方法用于喚醒一個正在等待相應對象鎖的線程,使其進入就緒隊列,以便在目前線程釋放鎖後競争鎖,進而得到CPU的執行。notifyAll()方法用于喚醒所有正在等待相應對象鎖的線程,使它們進入就緒隊列,以便在目前線程釋放鎖後競争鎖,進而得到CPU的執行。

每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列。就緒隊列存儲了已就緒(将要競争鎖)的線程,阻塞隊列存儲了被阻塞的線程。當一個阻塞線程被喚醒後,才會進入就緒隊列,進而等待CPU的排程。反之,當一個線程被wait後,就會進入阻塞隊列,等待被喚醒。

4.9 說一說sleep()和wait()的差別

  1. sleep()是Thread類中的靜态方法,而wait()是Object類中的成員方法;
  2. sleep()可以在任何地方使用,而wait()隻能在同步方法或同步代碼塊中使用;
  3. sleep()不會釋放鎖,而wait()會釋放鎖,并需要通過notify()/notifyAll()重新擷取鎖。

4.10 說一說notify()、notifyAll()的差別

  • notify()

    用于喚醒一個正在等待相應對象鎖的線程,使其進入就緒隊列,以便在目前線程釋放鎖後競争鎖,進而得到CPU的執行。

  • notifyAll()

    用于喚醒所有正在等待相應對象鎖的線程,使它們進入就緒隊列,以便在目前線程釋放鎖後競争鎖,進而得到CPU的執行。