多線程系列-上下文、死鎖、高頻面試題


前言

上一期的蹲坑系列我們介紹了多線程的基礎知識,是不是和你平時的了解有些出入呢。

《蹲坑也能進大廠》多線程這幾道基礎面試題,80%小伙伴第一題就答錯

今天繼續講解多線程相對基礎的理論知識點,如果你是新手或者對多線程了解不多,千萬不要想着上去就肝實戰課,沒用的,隨便出一個bug你都看不出來啥原因,花GIe強烈建議跟着本系列走完(手動狗頭護體)。

20160116922699_rEdRNG.gif

我:喲呼,狗剩子今天怎么不在家修養,也不陪你女朋友,又來公司寫bug啊

狗剩子:嘿嘿,怎么可能不陪女朋友,我們每天形影不離

我:......

狗剩子:趁着今天沒人,走,坑里見,我要和你坦誠相對。

正文

我:狗剩子,請聽第一題,守護線程和用戶線程有什么區別?

我們應該知道,Java有兩種線程:【守護線程Daemon】與【用戶線程User】,兩者的唯一的區別就是:虛擬機離開時,如果JVM中所有線程都是守護線程時,JVM就會自動退出;但是如果還有一個或以上的非守護線程則不會退出。

我:昨天問過你notify,那你知道Java中notify 和 notifyAll有什么區別?或者說我們怎么選擇使用哪一種?

昨天的事居然還記得,你這記性還可以呀,我都忘記了。

是這樣的,notify不能指定喚醒某一個具體的線程(這是網上說的,俺就跟着說,至於為啥后面告訴你),可能會導致信號丟失這樣的問題,只有在一個線程等待的時候才是它的主場,而notifyAll會喚醒所有等待線程,並允許他們爭奪鎖,雖然效率不高,但是可以保證至少有一個線程繼續執行。如果想要使用notify,必須確保滿足以下兩種情況。

  • 一次通知僅需要喚醒最多一條線程。
  • 所有等待喚醒的線程,自身處理邏輯相同。舉個栗子大家就會明白,比如使用Runnable接口實例創建的不同線程,或者同一個Thread子類new出來的多個實例。

我:不要得意,這都是開胃菜,再問你一個,wait為什么只能在代碼塊中使用?

啥?(心里搗鼓)wait只能在代碼塊中使用嗎,我咋不知道。那是....可能wait有潔癖,喜歡一個人自嗨吧。

我:你踏馬....

哦...我想起來了,我們可以反過來想,如果wait不要求在同步塊中,那可能會發生以下的錯誤。

先看一處用wait、notify實現的線程安全隊列的代碼:

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();
 
    //
    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }
 
    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}
  1. 消費者A調用 take() , 此時buffer.isEmpty()為true;
  2. 消費者A進入while,在調用wait() 方法之前, 生產者B調用了一個完整的 give()( 即buffer.add(data)和notify());
  3. 之后消費者A調用了 wait() ,但是錯過了生產者B調用的notify()
  4. 如果之后沒有別的生產者調用give()方法,消費者A所在線程則會一直等待。

我:這波解釋我都忍不住給你點個贊,你知道Java中鎖是什么嗎?

鎖的概念小伙伴們也可以,在學習完花Gie的volatile之后再看。

這個東西說起來很抽象, 你可以就把它想象成現實中的鎖, 至於他的防盜鎖、金鎖、還是指紋鎖並不重要,哪怕它就是一根草繩,一個自行車、甚至一坨那啥,都可以當做鎖。 是什么外在形象並不重要,重要的是它代表的含義: 誰持有它, 誰就有獨立訪問臨界資源的權利。

我:你有了解過什么是線程死鎖嗎?

死鎖就是說兩個或兩個以上線程在執行過程中,由於競爭資源,它們都在等待某個資源被釋放,因此線程被無限期地阻塞,此時的系統處於死鎖狀態。

如圖,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。

image.png

我:那你知道形成死鎖的四個必要條件嗎?

咱家有啥不知道的,這就給你列舉出來。

  1. 互斥條件:線程(進程)對分配到的資源具有排他性,也就是說該資源任意一個時刻只能有一個進程占用。
  2. 請求與保持條件:一個線程(進程)因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:線程(進程)已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源。
  4. 循環等待條件:當發生死鎖時,所等待的線程(進程)必定形成一個環路,死循環造成永久堵塞。

我:既然你知道形成死鎖的條件,那你肯定知道如何避免咯?

正所謂對症下葯,想要避免死鎖,那就需要破壞者四個必要條件的任意一個即可。

  1. 破壞互斥條件:此路不通,因為我們用鎖本來就是想讓他們互斥的。
  2. 破壞請求與保持條件:一次性申請所有的資源。
  3. 破壞不剝奪條件:占有資源線程可以嘗試申請其它資源,如果申請不到,可以主動釋放它占有的資源。
  4. 破壞循環等待條件:按照某一順序申請資源,釋放資源時則反序釋放。

我:這波回答的不錯,昨晚是不是偷偷准備了。狗剩子請繼續聽題,上下文切換曉得嗎?

麻煩尊重俺一下,以后請叫狗爺。

說到上下文切換,那我們得先知道什么是上下文,直白說上下文就是某個時間點CPU寄存器和程序計數器的內容。

拓展:每個線程都有一個程序計數器(記錄要執行的下一條指令),一組寄存器(保存當前線程的工作變量),堆棧(記錄執行歷史,其中每一幀保存了一個已經調用但未返回的過程)。

寄存器:寄存器就是CPU內部內存,負責存儲已經、正在和將要執行的任務,數量較少但是速度很快,與之相對應的是CPU外部相對較慢的RAM主內存。

程序計數器:程序計數器是一個專用的寄存器,用於表明指令序列CPU當前執行的位置,存儲的內容是正在執行指令的位置或下一次將要執行指令的位置。

大致了解了上下文,那上下文切換也就簡單了,它是指當前任務執行完,CPU時間片切換到下一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時可以繼續執行下去,任務從保存到再加載執行就是一次上下文切換。

如果還不是十分清楚,可以分下面三個步驟理解。

  1. 掛起一個進程,將這個進程在CPU中的狀態(上下文)存儲在內存中;
  2. 在內存中檢索下一個進程的上下文,並且將該進程在CPU的寄存器中回復;
  3. 跳轉到程序計數器所指向的位置,也就是該進程被中斷時,代碼當時執行到的位置,從而恢復該進程。

我:講的好詳細,突然好心動,那上下文切換會帶來什么問題呢?

看完上面介紹我們應該有一個感覺,那就是如果高並發情況下,頻繁切換上下文會導致系統串行執行,運行速率大大降低。

  • 直接消耗:包括CPU寄存器需要保存和加載, 系統調度器的代碼需要執行。
  • 間接消耗:CPU為了加快執行速度,會把常用的數據緩存起來,但是當上下文切換后(即CPU執行不同線程的不同代碼),那原本所緩存的內容很大程度沒有利用價值了,因此CPU就會重新進行緩存,這也導致線程被調度運行后,一開始啟動速度會比較慢。

拓展:線程調度器為了避免頻繁切換上下文帶來的開銷,會給每個被調度到的線程設置一個最小執行時間,從而減少上下文切換的次數,從而提高性能,但是缺點也顯而易見,就是會降低響應速度。

我:那你跟我講一下volatile是啥唄?

不了不了,今天快累死咱家了,老衲需要休息休息,明天我們再戰。

總結

多線程知識點非常龐大,涉及到很多方面,特別是剛剛接觸多線程的小伙伴,對於上下文這種概念理解起來非常困難,想要真正全部掌握需要深究每一個問題所涉及到的知識面,比如怎么用wait/notify實現生產者消費者模式線程的調度過程Java代碼如何一步步轉化被CPU執行,還有非常重要的,就是上面這些知識點的原理是什么,Thread如何啟動、中斷線程的線程間進行通信的原理是什么

這些花GieGie后面都會逐步帶大家掌握,有些大的知識點會拿出來進行單篇的講解,希望大家持續關注,假日不打樣,繼續肝

點關注,防走丟

以上就是本期全部內容,如有紕漏之處,請留言指教,非常感謝。我是花GieGie ,有問題大家隨時留言討論 ,我們下期見🦮。

原創不易,你怎忍心白嫖,如果你覺得這篇文章對你有點用的話,感謝老鐵為本文點個贊、評論或轉發一下,因為這將是我輸出更多優質文章的動力,感謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM