前言
上一期的蹲坑系列我們介紹了多線程的基礎知識,是不是和你平時的了解有些出入呢。
《蹲坑也能進大廠》多線程這幾道基礎面試題,80%小伙伴第一題就答錯
今天繼續講解多線程相對基礎的理論知識點,如果你是新手或者對多線程了解不多,千萬不要想着上去就肝實戰課,沒用的,隨便出一個bug你都看不出來啥原因,花GIe強烈建議跟着本系列走完(手動狗頭護體)。
我:喲呼,狗剩子今天怎么不在家修養,也不陪你女朋友,又來公司寫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();
}
}
- 消費者A調用
take()
, 此時buffer.isEmpty()
為true; - 消費者A進入while,在調用
wait()
方法之前, 生產者B調用了一個完整的give()
( 即buffer.add(data)和notify()); - 之后消費者A調用了
wait()
,但是錯過了生產者B調用的notify()
; - 如果之后沒有別的生產者調用give()方法,消費者A所在線程則會一直等待。
我:這波解釋我都忍不住給你點個贊,你知道Java中鎖是什么嗎?
鎖的概念小伙伴們也可以,在學習完花Gie的volatile之后再看。
鎖
這個東西說起來很抽象, 你可以就把它想象成現實中的鎖, 至於他的防盜鎖、金鎖、還是指紋鎖並不重要,哪怕它就是一根草繩,一個自行車、甚至一坨那啥,都可以當做鎖。 鎖
是什么外在形象並不重要,重要的是它代表的含義: 誰持有它, 誰就有獨立訪問臨界資源的權利。
我:你有了解過什么是線程死鎖嗎?
死鎖
就是說兩個或兩個以上線程在執行過程中,由於競爭資源,它們都在等待某個資源被釋放,因此線程被無限期地阻塞,此時的系統處於死鎖狀態。
如圖,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。
我:那你知道形成死鎖的四個必要條件嗎?
咱家有啥不知道的,這就給你列舉出來。
- 互斥條件:線程(進程)對分配到的資源具有排他性,也就是說該資源任意一個時刻只能有一個進程占用。
- 請求與保持條件:一個線程(進程)因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:線程(進程)已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源。
- 循環等待條件:當發生死鎖時,所等待的線程(進程)必定形成一個環路,死循環造成永久堵塞。
我:既然你知道形成死鎖的條件,那你肯定知道如何避免咯?
正所謂對症下葯,想要避免死鎖
,那就需要破壞者四個必要條件的任意一個即可。
- 破壞互斥條件:此路不通,因為我們用鎖本來就是想讓他們互斥的。
- 破壞請求與保持條件:一次性申請所有的資源。
- 破壞不剝奪條件:占有資源線程可以嘗試申請其它資源,如果申請不到,可以主動釋放它占有的資源。
- 破壞循環等待條件:按照某一順序申請資源,釋放資源時則反序釋放。
我:這波回答的不錯,昨晚是不是偷偷准備了。狗剩子請繼續聽題,上下文切換曉得嗎?
麻煩尊重俺一下,以后請叫狗爺。
說到上下文切換
,那我們得先知道什么是上下文
,直白說上下文
就是某個時間點CPU寄存器和程序計數器的內容。
拓展:每個線程都有一個程序計數器(記錄要執行的下一條指令),一組寄存器(保存當前線程的工作變量),堆棧(記錄執行歷史,其中每一幀保存了一個已經調用但未返回的過程)。
寄存器:寄存器就是CPU內部內存,負責存儲已經、正在和將要執行的任務,數量較少但是速度很快,與之相對應的是CPU外部相對較慢的RAM主內存。
程序計數器:程序計數器是一個專用的寄存器,用於表明指令序列CPU當前執行的位置,存儲的內容是正在執行指令的位置或下一次將要執行指令的位置。
大致了解了上下文
,那上下文切換
也就簡單了,它是指當前任務執行完,CPU時間片切換到下一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時可以繼續執行下去,任務從保存到再加載執行就是一次上下文切換。
如果還不是十分清楚,可以分下面三個步驟理解。
- 掛起一個進程,將這個進程在CPU中的狀態(上下文)存儲在內存中;
- 在內存中檢索下一個進程的上下文,並且將該進程在CPU的寄存器中回復;
- 跳轉到程序計數器所指向的位置,也就是該進程被中斷時,代碼當時執行到的位置,從而恢復該進程。
我:講的好詳細,突然好心動,那上下文切換會帶來什么問題呢?
看完上面介紹我們應該有一個感覺,那就是如果高並發情況下,頻繁切換上下文會導致系統串行執行,運行速率大大降低。
- 直接消耗:包括CPU寄存器需要保存和加載, 系統調度器的代碼需要執行。
- 間接消耗:CPU為了加快執行速度,會把常用的數據緩存起來,但是當上下文切換后(即CPU執行不同線程的不同代碼),那原本所緩存的內容很大程度沒有利用價值了,因此CPU就會重新進行緩存,這也導致線程被調度運行后,一開始啟動速度會比較慢。
拓展:線程調度器為了避免頻繁切換上下文帶來的開銷,會給每個被調度到的線程設置一個最小執行時間,從而減少上下文切換的次數,從而提高性能,但是缺點也顯而易見,就是會降低響應速度。
我:那你跟我講一下volatile是啥唄?
不了不了,今天快累死咱家了,老衲需要休息休息,明天我們再戰。
總結
多線程知識點非常龐大,涉及到很多方面,特別是剛剛接觸多線程的小伙伴,對於鎖
、上下文
這種概念理解起來非常困難,想要真正全部掌握需要深究每一個問題所涉及到的知識面,比如怎么用wait/notify實現生產者消費者模式
、線程的調度過程
、Java代碼如何一步步轉化被CPU執行
,還有非常重要的,就是上面這些知識點的原理是什么,Thread如何啟動、中斷線程的
,線程間進行通信的原理是什么
。
這些花GieGie后面都會逐步帶大家掌握,有些大的知識點會拿出來進行單篇的講解,希望大家持續關注,假日不打樣,繼續肝。
點關注,防走丟
以上就是本期全部內容,如有紕漏之處,請留言指教,非常感謝。我是花GieGie ,有問題大家隨時留言討論 ,我們下期見🦮。
原創不易,你怎忍心白嫖,如果你覺得這篇文章對你有點用的話,感謝老鐵為本文點個贊、評論或轉發一下,因為這將是我輸出更多優質文章的動力,感謝!