java多线程编程_《java多线程编程实战指南》读书笔记 -- 基本概念
展開
并發:多個線程操作相同資源,保證線程安全,合理使用資源
高并發:服務能同時處理多個請求,提高程序性能
測試上下文切換工具
- Lmbench3 測量上下文切換時長
- vmstat 測量上下文切換次數
減少上下文切換
- 無鎖并發編程:將數據ID按hash算法取模分段,不同線程處理不同段數據。
- CAS算法
- 使用最少線程
- 協程:在單線程中實現多任務調度并維持任務間切換
避免死鎖
- 避免一個線程同時獲取多個鎖
- 避免一個線程在鎖內同時占用多個資源
- 嘗試使用定時鎖,使用lock.tryLock(timeout)來代替使用內部鎖機制
- 對數據庫鎖,加鎖和解鎖必須在一個數據庫連接里
創建線程成本
java平臺中,線程就是一個對象,創建需要分配內存。
與普通對象不同,jvm會為每個線程分配調用棧所需的內存空間,調用棧用于跟蹤java方法間的調用關系以及java代碼對Native Code(多為 C代碼)的調用。
java中的每個線程可能還有一個內核線程(具體與jvm實現有關)與之對應。
線程狀態
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jBnNfHDw-1576115827051)(evernotecid://626F545F-28E7-432D-B442-A76BAC946322/appyinxiangcom/14768996/ENResource/p91)]
獲取線程轉儲(Thread dump)的方法
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4EOcSMZc-1576115827052)(evernotecid://626F545F-28E7-432D-B442-A76BAC946322/appyinxiangcom/14768996/ENResource/p92)]
eg:
mac中,使用jstack獲取線程轉儲//進入jdk安裝地址 ? /usr/libexec/java_home -V ? cd /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home //獲取當前所有引用PID /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home ? jps 47697 Launcher 47698 OrtApp 46002 46535 KotlinCompileDaemon 47818 Jps //獲取線程轉儲 /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home ? jstack -l 47698 2019-09-23 09:24:48 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode): "lettuce-kqueueEventLoop-9-1" #223 daemon prio=5 os_prio=31 tid=0x00007fe6cd166800 nid=0x16313 runnable [0x0000700018ed0000] java.lang.Thread.State: RUNNABLE at io.netty.channel.kqueue.Native.keventWait(Native Method) at io.netty.channel.kqueue.Native.keventWait(Native.java:94) at io.netty.channel.kqueue.KQueueEventLoop.kqueueWait(KQueueEventLoop.java:149) at io.netty.channel.kqueue.KQueueEventLoop.kqueueWait(KQueueEventLoop.java:140) at io.netty.channel.kqueue.KQueueEventLoop.run(KQueueEventLoop.java:216) at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None ... 12345678910111213141516171819202122232425262728293031323334
另外,在jdk安裝路徑下執行:
//打開jvisualvm /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home ? jvisualvm 12
可打開圖形化工具,其中可以獲取線程的轉儲信息。
可用同樣方式打開JMC。
競態
競態(Race Condition):線程間相互干擾,由臟讀和更新丟失導致最終產生結果與預期結果不一致的現象。
計算的正確性依賴于相對時間順序(Relative Time)或線程的交錯(Interleaving)。
分析競態的方法 - 二維表分析法
競態產生條件:
- 讀-改-寫(read-modify-write): 原因主要為讀臟數據-覆蓋其他線程對共享變量的更新
- 檢查而后行動(check-then-act): 原因主要為讀取變量值,根據讀取值決定下一步操作。讀取值為臟數據,導致下一步操作出錯。
原子性
原子性:
- 對其他線程,執行線程的狀態只有未開始和已完成兩種,其他線程無法在執行線程訪問(讀、寫)共享變量時,獲取到其中間狀態;
- 訪問同一組共享變量的原子操作是不能交錯的。
Lock 軟件鎖;
CAS 硬件鎖。
java基本數據類型中,long、double的寫操作都不具有原子性。通過添加volatile關鍵字可以使其具有原子性。
可見性
可見性(Visibility): 程序中變量被分配到寄存器(Register)中處理,一個處理器的寄存器無法讀取另一個處理器的寄存器。運行在不同處理器的線程共享變量分配到寄存器進行存儲,就會出現可見性問題。
緩存一致性協議(Cache Coherence Protocol): 用于讀取其他處理器高速緩存中數據,并更新到該處理器高速緩存中。
緩存同步:一個處理器從自身處理器緩存以外的其他存儲部件中讀取數據并將其更新到該處理器的高速緩存的過程。
高速緩存、主內存內容是可同步的。
沖刷處理器緩存(寫):為保障可見性,必須使一個處理器對共享變量做的更新最終被寫入該處理器的高速緩存或主存中的過程。
刷新處理器緩存(讀):若共享變量在處理器讀取之前進行了更新,該處理器必須從進行更新操作的處理器的高速緩存或主存中將更新的內容進行緩存同步。
保障可見性,可以使用volatile:
- 提示JIT編譯器,被修飾的變量可能被多個線程共享,阻止其做出可能導致程序運行不正常的優化;
- 讀取被修飾的變量會使相應處理器進行刷新處理器緩存處理,寫被修飾變量會使相應處理器進行沖刷處理器緩存操作。
相對新值:一個線程更新共享變量后,其他線程能讀到更新值,這個值稱為變量的相對新值。
最新值:一個線程更新共享變量后,其他線程不能讀到更新值,這個值稱為變量的最新值。
可見性保障僅意味著線程能夠讀到共享變量的相對新值,并不能保證該線程能夠讀到最新值。
保障原子性,上述操作process2最終讀取到的a值可能是0/1/2
保障可見性,上述操作process2最終讀取到的a值為2
java規范保證,一個線程終止,其對共享變量的更新,另一個調用期join方法的線程是可見的
有序性
重排序(Reordering):一個處理器上執行多個操作,另一個處理器角度來看可能與目標代碼所指定的順序不一致。
幾種內存操作順序操作:
- 源代碼順序:未經過編譯和解釋的源碼中指定的內存訪問操作順序
- 程序順序:經過編譯執行(機器碼)或解釋執行(字節碼Byte Code)中指定的內存訪問操作順序
- 執行順序: 內存訪問操作在給定處理器上實際執行順序
- 感知順序:給定處理器鎖感知到的該處理器及其他處理器的內存訪問操作發生的順序
java平臺包含兩種編譯器:
- 靜態編譯器javac:將源代碼(.java)編譯成字節碼(.class二進制文件), 代碼編譯階段介入
- 動態編譯器JIT:將字節碼動態編譯為jaav虛擬機宿主機的本地代碼(機器碼), java程序運行過程中介入
javac幾乎不會執行指令重排序;JIT可能執行指令重排序。查看JIT編譯器動態生成的匯編代碼
下載hsdis反編譯工具,將hsdis-amd64.dylib文件放到/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/jre/lib/server下,和libjvm.dylib同級
文件放置好后,使用命令
java -server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:PrintAssemblyOptions=intel [JAVA 文件路徑]
即可看到指定代碼的匯編代碼內容.
-XX:LogFile=[xxx/xxx.log]可將反編譯內容輸出到指定文件中
現代處理器:“順序讀取” – “亂序執行” – “順序提交” 也會導致重排序;
ROB(重排序緩沖器)
內存重排序
Load: 從指定RAM地址(通過高速緩存加載)加載數據到寄存器
Store: 將數據存儲到指定地址表示的RAM存儲單元
貌似串行語義(As-if-serial Semantics)
僅保證重排序不影響單線程程序的正確性。
為保證貌似串行語義,存在數據依賴關系的語句不會被重排序。若兩個操作(指令)訪問同一個變量(地址)且其中一個操作(指令)為寫操作,那么兩個操作之間就存在數據依賴關系。
控制依賴關系: 允許被重排序,若一條語句的執行結果會決定另外一條語句能夠被執行,這兩條語句存在控制依賴關系。如if語句中的條件表達式和對應的語句體。
從底層角度,禁止(邏輯上)重排序是通過調用處理器提供的相應指令(內存屏障)來實現的。java會替我們與這類指令打交道,我們只需要使用語言本身提供的機制即可。
上下文切換
術語
線程上下文切換: 一個線程被暫停,另一個線程被選中開始或繼續運行的過程
時間片: 一個線程可以連續占用處理器運行的時間長度
切入: 一個線程被操作系統選中占用處理器開始或繼續運行
切出: 一個線程被剝奪處理器使用權而暫停運行
上下文: 切入和切出時刻相應線程所執行的任務的進行程度(如計算中間結果、執行到哪條指令等),一般包含通用寄存器的內容和程序計數器的內容。
暫停: 線程由RUNNABLE狀態轉換為非RUNNABLE狀態。
喚醒: 線程由非RUNNABLE狀態轉換為RUNNABLE狀態。
被喚醒的線程并非立即占用處理器運行,當被喚醒的線程被操作系統選中占用處理器繼續運行時,操作系統才會恢復其上下文。
自發性上下文切換:
由自身因素導致的切出,如下方法會導致自發性上下文切換
I/O操作或等待其他線程持有的鎖
非自發性上下文切換:
由于線程調度器的原因被迫切出
- 被切出線程時間片用完
- 一個優先級更高的線程需要被運行
- java虛擬機垃圾回收動作
開銷及測量
直接開銷:
- 操作系統保存和恢復上下文所需開銷
- 線程調度器進行線程調度的開銷
間接開銷:
- 處理器高速緩存重新加載的開銷,切出線程被另一個處理器切入,繼續運行,若新處理器從未運行過該線程,需要重新從主存貨通過緩存一致性協議將線程運行過程中所需變量加載到高速緩存中
- 可能導致整個一級緩存中的內容被沖刷(Flush),內容被寫入下一級高速緩存或主存中
測量:
確定一個多線程程序在某個時間段或某種場景下運行時發生的上下文切換(主要是自發性上下文切換)的次數。
- Linux平臺下,使用其內核提供的perf命令來監視java程序運行過程中的上下文切換次數和頻率。
eg:perf stat -e cpu-clock, task-clock, cs, cache-references, cache-misses java [Main Class]
其中參數e的值中,cs表示被監視程序的上下文切換的數量。 - windows平臺下,perform命令
線程的活性故障
由于資源稀缺性或程序自身的問題和缺陷導致線程一直處于非RUNNABLE狀態,或狀態處于RUNNABLE狀態但是其要執行的任務一直無法進展的現象就被稱為線程活性故障
- 死鎖(Deadlock): 兩個線程互相等待對方釋放資源
- 鎖死(Lockout): 執行所需獲取的資源一直未被釋放
- 活鎖(Livelock): 線程處于RUNNABLE狀態,但要執行的任務沒有進展
- 饑餓(Starvation): 因無法獲得其所需資源而使得任務執行無法進展
資源爭用與調度
排他性資源:一次只能夠被一個線程占用的資源,如處理器、數據庫連接、文件等。
資源爭用:一個線程占用一個排他性資源進行讀寫操作而未釋放其對資源所有權的時候,其他線程試圖訪問該資源的現象。
高/低爭用:同時試圖訪問同一個已經被其他線程占用的資源的線程數量多/少。
高并發:處于運行狀態(RUNNABLE的子狀態RUNNING)的線程數量多,程序運行理想的狀態為高并發、低爭用。
資源調度:多個線程申請同一個排他性資源的情況下,決定哪個申請者占用該資源的過程。常見特性是它是否能保證公平性。
公平性:資源的申請者是否按照其申請資源的順序而被授予資源的獨占權。
排隊:常見資源調度策略,調度器持有一個等待隊列,未獲取獨占權的線程進入隊列暫停,待資源釋放被喚醒,從隊列中移除,若再次申請資源失敗,再次進入隊列中暫停;由此可見,資源調度可能導致上下文切換。
公平調度策略:資源未被其他任何線程占用,等待隊列為空的情況,資源的申請者才被允許搶占相應資源的獨占權。搶占失敗的申請者進入等待隊列。此策略中資源申請者總是按照先來后到的順序獲得資源的獨占權。
非公平調度策略:允許插隊。一個線程釋放其資源獨占權的時候,等待隊列中的一個線程會被喚醒再次申請相應的資源,而在這個過程中另外一個申請該線程的活躍線程可以與這個被喚醒的線程共同參與相應資源的搶占。可能導致饑餓現象。吞吐量更高,但申請者獲取相應資源的獨占權所需的時間偏差可能較大。
非公平調度策略公平調度策略適用于多數線程占用資源較短或資源平均申請時間間隔相對較短的場景適用于多數線程占用資源較長或資源平均申請時間間隔相對較長的場景吞吐率較大吞吐率較小可能導致饑餓狀態不會導致饑餓狀態
等待隊列中的線程從被喚醒到繼續運行可能需要一段時間,此間新來線程若占用該資源時間不長,完全有可能在被喚醒線程繼續執行之前將資源釋放,此時可能減少了上下文切換次數。若新來線程占用資源時間太長,被喚醒資源需要重新被暫停進入等待隊列中,增加了上下文切換次數。
總結
以上是生活随笔為你收集整理的java多线程编程_《java多线程编程实战指南》读书笔记 -- 基本概念的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 红旗-64防空导弹
- 下一篇: 电子工程可以报考二建_非工程类专业也能报