第一章、并發編程的挑戰?
并發和并行:指多線程或多進程?
線程的本質:操作系統能夠進行運算調度的最小單位,是進程(Process)中的實際工作單元
進程的本質:操作系統進行資源分配和調度的基本單位,是程序的一次動態執行過程。
多線程定義:在同一個進程內創建多個線程,共享進程的內存和資源(如堆、全局變量),但每個線程有獨立的棧和程序計數器。
多進程定義:操作系統同時運行多個獨立的進程,每個進程有獨立的內存空間和系統資源。
操作系統:操作系統(Operating System, OS)是計算機系統的核心管理者和硬件與軟件的橋梁
并發和并行的區別是什么??
【本文專注于多線程】
并行:不同的線程或進程間互不干擾?
并發:不同的線程之間有資源的競爭
線程的基本代碼:?
Thread t1---->創建第一個線程
Thread t2----->創建第二個線程
運行時是兩個任務交替運行;去掉t1.join()/t2.join(),運行是3個任務交替運行
線程的幾種狀態:?
操作系統的指令執行不是立刻的,而是一個任務調度的過程;指令的最終執行靠的是CPU里的運算核心。
在任務管理器中,句柄指變量,核心指CPU中間的運算核心【ALU】,每個核心同一時刻只能執行一個任務。【因為電壓信號同時傳輸會合并為一個信號】;內核為14指CPU同時只能執行14個指令,每個基礎指令是線程級別,可以認為是一個任務--->CPU同時最多執行14個任務;快速切換任務執行;
1、CPU內部的信號指令1010,都是電壓信號,高電壓代表1,低電壓代表0,電壓信號不能同時傳遞,必須是排隊傳輸。
2、計算機內部的各個硬件之間的指令傳輸都是高低電壓傳輸硬件之間的指令傳輸,由于電壓傳輸,每個指令必須得排隊。?
?就緒隊列:每個任務都進入就緒態,操作系統隨時可以選中去執行,由就緒態選中變成運行狀態,當剩的時間片不多,執行完時,從就緒隊列清除,變成死亡狀態。
時間片:?決定了任務在就緒隊列中能連續占用CPU的時長
在電腦中任何任務的提交不能繞過操作系統。
隊列為什么不是先進先出?
?先進先出是公平隊列,整體性能損耗較大;
操作系統里是非公平隊列,判定時是按順序判斷該任務是否符合條件,只有符合條件的才會被選中;所以態既叫隊列,又叫非公平隊列;
為什么微信,QQ等一直在運行狀態??
里面有一個死循環,當用戶點擊關閉觸發條件判定,?循環判定不滿足,程序結束。
在微信、QQ等應用保持后臺運行的場景中,“死循環”通常指的是主線程的事件循環(Event Loop)。
用戶是不能讓程序直接進入運行態的,用戶只能交給操作系統讓它進入就緒態。
?
代碼內存圖:
?方法的執行都是拷貝入棧;方法本身也是一個棧結構;Java的引用類型變量底層是C語言的指針;new的兩個對象在底層是C語言創建的空間,是在堆區域創建了兩個線程對象;t1對象屬于線程類;Thread里有很多屬性和方法,上述代碼只是對run方法進行重寫;
run和start方法是非靜態方法,在每個對象里都有一份;
main方法被static修飾,只有一份;
方法的調用都是拷貝入棧;
t1的start方法拷貝入棧后,t1的start方法會把它的整個線程信息交給操作系統的就緒隊列,提交完后出棧------t2的start拷貝入棧,t2的start把t2的信息提交給操作系統,t2在就緒隊列,t2再出棧-----主方法本身也在就緒隊列里----操作系統隨時選中執行,選中后再創建新線程【比如針對t1創建t1線程棧,針對t2創建t2線程棧】---t1被操作系統選中后會執行里面的run方法,run方法拷貝進入t1線程;
類.方法-----方法是靜態的
通過主線程可以創建子線程,創建完之后就相互獨立;
先進入就緒態被操作系統選中的概率大,但是順序不確定;
上下文切換【面試點】
CPU每次執行完任務會把任務執行到哪了記下來,下次再執行時會去讀從哪接著開始執行,時間損耗是毫秒級。
定義:
上下文切換是操作系統在多任務環境中,將CPU從一個線程(或進程)切換到另一個線程(或進程)時,保存當前任務狀態并恢復新任務狀態的過程。它是并發編程中影響性能的關鍵因素之一。
時間片一般是幾毫秒——幾十毫秒,不同操作系統不一樣。
?多線程一定會涉及到上下文切換,頻繁的上下文切換一定會額外消耗CPU的時間。
什么情況下用多線程?【面試題】
多線程在CPU浪費嚴重的情況下使用。【執行時間和等待時間比例特別懸殊的情況相愛,采用多線程】(一定是io密集,就是大量的高頻訪問網落訪問硬盤)
IO密集型(輸入輸出):出現了大量的CPU等待時間。
io:訪問網落,訪問硬盤
指令密集型:大量給CPU任務---單線程合適
join()
?是 Java 多線程編程中的一個核心方法,用于讓當前線程等待另一個線程執行完畢
t1、t2都執行完時,for循環才會執行。
t1、t2的執行互不等待;只有t1執行完時,t2才會發出join指令
?在棧里,只有棧頂的元素處于運行狀態,不在棧頂處于停止運行狀態。
在這個代碼里,對比單線程執行兩個for循環和兩個線程分別執行for循環哪個更快。【thread.jon和輸出時間戳互換】
串行:單線程---并發:多線程
只有一個核心,單線程最快
用什么工具來監控內存?----Lmbench3可以測量上下文切換的時長,指令:vmstat---可以測量上下文切換的次數【面試點】
如何減少上下文切換?【面試點】----無鎖并發編程、CAS算法、使用最少線程和使用協程。
?
grep指令:文本搜索工具;在文件或輸入流中匹配指定模式(正則表達式)
awk指令:文本處理與報表生成;逐行處理文本,按字段(默認以空格/Tab分隔)提取、計算或格式化輸出。
sort指令:?文本排序工具;對文件或輸入流按行排序(默認按字典序)。
dump指令:快照,查看這一瞬間內存中的信息【面試點】
指令結合案例:
sudo:Linux中使用管理員指令。
在后面的目錄下查詢id為31177的線程,把查看的信息保存為后面路徑下的dump17文件。
?用grep指令在dump17文件中篩選行里有java.lang.Thread.State的信息,然后統計完排序。
?死鎖
?非靜態方法必須依托對象的存在,在每個對象里都有一份。
synchronized(A)是對A加鎖;加鎖只能對引用類型加鎖,鎖住A后其他線程不允許讀A?;
當進入睡眠狀態時,即使時間片沒執行完也必須讓出CPU,并且不在就緒隊列。【面試點】
?鎖住A后什么時候會釋放鎖?---把{ }里的全部執行完才會釋放
一個線程就算進入睡眠狀態,也不會釋放鎖。【面試點】
sleep方法:讓出CPU,但不釋放鎖
wait方法:讓出CPU,并且釋放鎖 【面試點】
?加鎖其實是給當前線程加上標記。
t1鎖住A后沉睡,t2被操作系統選中鎖住B后訪問A失敗,t2讓出CPU進入阻塞隊列,但鎖仍未釋放;2秒之后t1進入就緒隊列,想要鎖B失敗,t1也進入阻塞隊列------->死鎖;
但不會消耗CPU,CPU仍可以執行其他任務,只是這兩個任務永遠執行不完了
競爭失敗要進入阻塞隊列;進入阻塞隊列操作系統不會選中;
?恰巧的情況下,t2先執行,就不會出現死鎖了。
?如何避免死鎖?【面試點】
?
?
第二章、JAVA并發機制的底層實現原理
synchronize:重量級鎖;讀寫都鎖住---->讀寫都安全
volatile:輕量級鎖(功能不全的鎖);只能鎖住寫-->讀安全,寫不安全【面試點】
?volatile在多處理器(CPU里的多核心)開發中保證了共享變量的“可見性”----->當一個線程修改另一個共享變量時,另外一個線程能讀到這個修改的值。
CPU要從內存中讀取數據進行操作;內存中存儲的是變量以及函數在它的運行狀態下的數據;
CPU通過總線(本質上是導線)向內存中傳輸指令(1010指令就是電壓信號);
單根導線上同一時刻只能過一個電壓信號;總線的寬度一般是36-41位(并排三十多根到四十多根的導線);
一個指令通常先傳地址,再傳數據;
一次指令差不多是一個最基本的指令,也就是總線的寬度;不同的指令是不同同時傳輸的,不然電壓信號會相互干擾;
指令在總線必須排隊傳輸--->存在指令隊列
內存和CPU的數據是雙向的;?但CPU發送的指令比內存多,所以指令隊列在CPU;
內存同一時刻只能被一個指令所指揮;
?假設現在有三個線程,分別對a,b,c for循環一萬次,a通過總線讀完+1再通過總線往回更新;
當a進行讀寫操作時,總線一直被占用,其他兩個核心沒辦法操作;--->此時多核沒意義
現在,CPU和內部的存儲一直交互釋放了總線,總線就可以把其他線程的數據讀過來操作了--->支持了多核CPU的發展
a加到1萬返回內存,在總線上一來一回,對總線占用的不多;
所以CPU的存儲越多,存的數據越多,對核心的支持也越多,對總線的壓力降低越明顯,CPU內部存儲和性能關乎大;
長距離網絡傳輸時,一根導線可以同時存在多個電壓信號。
?
?加入中轉站后效率大幅度提高,但轉發也損耗時間,不是越多越好,平衡點性能最好的平衡點是三級緩存。
多級緩存(L1/L2/L3)通過?減少CPU核心的等待時間?和?優化數據局部性,顯著提高核心利用率。
高速緩存的并發性問題
現在有兩個線程操作同一個變量a,線程1拿到時間片,將a加到10?,還沒往回更新時時間片到期,停止運行,線程2拿到時間片,將a加到10,往回更新,任務執行完釋放CPU,線程1接著執行,往回更新---->此時兩個線程一共加了20,但是最終的結果是10---->導致了相互覆蓋問題。
高速緩存優點:增加傳輸效率,提升CPU利用率
高速緩存缺點:會導致數據相互覆蓋
高速緩存往回更新是隨機的,不一定計算完后往回更新;------->最終結果是不確定的
高速緩存往回更新時,高速緩存內的數據會清空,下一次計算需要重新從內存讀取數據。
內存屏障:在內存中有一個變量標記,根據這個標記判斷是否要訪問。
緩存行:高速緩存的基本存儲單元【一個緩存行64字節,沒一個緩存行和外界相連的導線線路進行數據交互;】處理器填寫緩存線時會加載整個緩存線,需要使用多個主內存讀周期【一個周期從內存中讀數據叫做一個主內存周期】
計算機存儲區域:內存、硬盤、CPU內部的高速緩存---數據是一份份存儲的,硬盤和內存中的數據單元是4KB
計算機中最小的物理單元是一字節一個存儲單元,操作系統給硬盤和內存劃分的邏輯存儲是4KB一個存儲區域-----在硬盤上,一個存儲單元是棧區,在內存中一個存儲單元是一個頁,在高速緩存中存儲單元是一個緩存行64字節。
一個緩存行可以存多份不同任務的數據,其中一個任務占著線操作時,其他任務就不能對它操作了--->所以每個任務過來操作時,相當于獨占整個緩存行(不同芯片不一樣,但每個都是獨占的),造成一定性能損耗。
原子操作:不可中斷的一個或一系列操作(一系列操作要么都成功,要么都失敗,其中一個步驟如果失敗,其他步驟都要還原)
在電腦底層只存在C語言的基本類型數據(6種基本數據:short? int? long bool double char?),其他語言在電腦內部都會轉成C語言;
緩存行主要用于存儲六種基本類型的數據,其容量從一字節到八字節不等。由于緩存行可容納大量數據,當多個進程同時進行讀寫操作時,容易造成擁堵現象。具體而言,緩存行中存儲的數據越多,發生排隊的概率就越大;反之,數據量越少,排隊概率則越小。
為優化性能,我們可采取以下策略:首先,盡量在緩存行中存儲體積較大的數據,以減少數據總量,從而降低競爭概率。其次,在存儲數據時,可采用獨占式填充策略,即用自身數據填滿整個緩存行。這種做法的優勢在于,當需要讀取數據時,可以避免競爭,實現快速訪問,從而顯著提升性能。
然而,這種優化策略的適用性取決于數據規模。在數據量較少時,這種方法是可行的;但當數據量較大時,若緩存行中存儲的數據過少,反而會導致整體性能下降。因此,緩存行的設計需要權衡利弊,這也是為什么標準緩存行大小通常設定為64字節。過大的緩存行會增加擁堵風險,反而降低讀取效率,有時甚至不如直接從內存讀取。
對于追求極致性能的場景,可以將整個64字節緩存行用于存儲單一數據。這種獨占式存儲方式能夠確保每次讀取都暢通無阻,達到最佳性能表現。
緩存命中:高層緩存容量有限,僅能存儲少量數據。當核心處理器需要訪問數據時,首先會查詢高速緩存。若所需數據存在于緩存中,則直接返回,這種情況稱為緩存命中;若緩存中不存在,則需訪問內存,這種情況稱為緩存未命中。由于緩存空間有限,部分數據可能存儲在緩存中,而另一部分則不在,因此每次訪問都需要先檢查緩存是否存在所需數據,再決定是否訪問內存。這一過程就是緩存命中的基本原理。
?寫命中:------>針對高速緩存
當數據寫回到內存緩存區域時,系統會首先檢查該緩存地址是否存在于緩存中。以讀取變量a為例:首先讀取a,然后對a進行操作,操作完成后再將結果更新回緩存。在這個過程中,如果緩存中存在a的地址,稱為寫命中;如果不存在,則稱為寫未命中。值得注意的是,由于高速緩存空間有限,之前存儲的數據可能已被清理。如果數據已被清除,在寫回時就無法找到對應的緩存地址,從而導致寫未命中。
寫缺失: -------->針對內存
舉例:a=5往回更新時,可能有其他線程把內存中的a刪除,內存中的a沒了,這種情況就是寫缺失。
?【以上術語只有原子操作、緩存命中是學術性術語,其他幾個面試面不到】
x86處理器是指CPU的內部結構;以下內容以x86處理器為主:
主流CPU處理器:?x86架構、ARM架構【軍隊政府學校】、RISC-V?架構
?new的對象被volatile修飾,轉換成匯編代碼,里面有lock指令;
有volatile變量修飾的共享變量進行寫操作時會多出第二行匯編代碼---->CPU架構
?
當內存中的a被volatile變量修飾時,其中一個a往回更新時,另一個在高速緩存中的a失效?;
但是用volatile修飾后還是會出錯,如圖,當a=10進入隊列時,時間片到期,a=20執行指令,高速緩存中的數據失效,但問題是數據已經進入隊列,所以仍會出現覆蓋問題。
volatile并不能保證準確性。
補充:內存圖角度理解并發編程
進程:正在執行的程序,內存為正在執行的程序開辟內存空間
線程:程序的執行過程,線程的執行依賴方法的不斷入棧出棧
內存會為正在運行的程序開辟內存空間;
方法區:存儲類信息
對象在堆里開辟空間
只有爭搶的資源需要加鎖,不爭搶不用加鎖
synchronized不能對變量進行加鎖
鎖是要加在被爭搶的資源上的
一個完整的操作不能分開加鎖
synchronized修飾靜態方法:鎖定的是類,非靜態方法:鎖方法的調用者,修飾代碼塊:鎖定傳入對象