并發(fā)編程的難題和挑戰(zhàn)
在并發(fā)編程的技術領域中交惯,對于我們而言的難題主要有兩個:
- 多線程之間如何進行通信和線程之間如何同步乘盼,通信是指線程之間以何種機制來交換信息。
多線程的線程通信機制
在命令式編程中货葬,線程之間的通信機制有兩種:共享內存和消息傳遞财搁。
- 共享內存的方式,多線程之間共享公共的狀態(tài)(變量)慕购,那么線程之間通過寫/讀內存中的公共狀態(tài)(變量)來隱式進行通信聊疲。在此模式下,同步實現(xiàn)是隱式進行的沪悲,由于消息的發(fā)送必須在消息的接收之前获洲。
- 消息傳遞的方式,多線程之間沒有公共的狀態(tài)(變量)殿如,那么線程之間必須通過明確的傳遞狀態(tài)(變量)來顯式進行通信贡珊。在此模式下,同步實現(xiàn)是顯式進行的涉馁,必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行门岔。
Java中的同步模式是什么?
同步機制是指程序用于控制不同線程之間操作發(fā)生相對順序的機制烤送。
Java生態(tài)中的并發(fā)編程模型采用的是共享內存模型寒随,因此在Java線程之間的通信總是隱式進行, 整個通信過程對開發(fā)者是黑盒的帮坚,如果編寫多線程程序的開發(fā)者不深入理解這種隱式模式下的線程之間通信機制妻往,就會會出現(xiàn)內存可見性和一致性的問題,我們統(tǒng)稱為線程不安全問題试和。
存在內存可見問題
Java應用程序中讯泣, 所有實例域、靜態(tài)域和數(shù)組元素存儲在堆內存中灰署, 堆內存在線程之間共享判帮。會存在這內存可見性問題。
不存在內存可見問題
局部變量(Local variables) 溉箕, 方法定義參數(shù)(java語言規(guī)范稱之為formal method parameters) 和異常處理器參數(shù)(exception handler parameters) 不會在線程之間共享晦墙,它們不會有內存可見性問題,也不受內存模型的影響肴茄。
所以晌畅,我們在開發(fā)多線程場景下的程序的時候主要需要關注的就是內存可見問題變量,包含:實例域寡痰、靜態(tài)域和數(shù)組元素抗楔。
而為了降低并發(fā)編程的難度和門檻棋凳,這些線程之間的數(shù)據(jù)同步和通信控制就交由一個特定的數(shù)據(jù)模型進行控制和管理,我們稱之為Java內存模型(JMM)连躏。
Java內存模型(JMM)
JMM決定在程序運行中剩岳,一個線程對共享變量的寫入何時對另一個線程可見。
JMM定義了線程和主內存之間的抽象關系
線程之間的共享變量存儲在主內存中入热,每個線程都有一個私有的本地內存 拍棕, 本地內存中存儲了該線程以讀/寫共享變量的副本。
本地內存是JMM的一個抽象概念勺良, 并不真實存在绰播。它涵蓋了緩存, 寫緩沖區(qū)尚困, 寄存器以及其他的硬件和編譯器優(yōu)化蠢箩。
Java 內存模型的抽象示意圖如下:
[圖片上傳失敗...(image-4668d3-1679827701944)]
由上圖可見,線程A與線程B之間如要數(shù)據(jù)通信事甜,需要有以下兩個步驟:
- 線程A把本地內存A中更新過的共享變量刷新到主內存中去谬泌。
- 線程B到主內存中去讀取線程A之前已更新過的共享變量。
下面通過示意圖來說明這兩個步驟:
[圖片上傳失敗...(image-c991b9-1679827701944)]
如上圖所示讳侨,本地內存A和B有主內存中共享變量x的副本呵萨。假設初始時,這三個內存中的x值都為0跨跨。
- 線程A在執(zhí)行時潮峦,把更新后的x值,臨時存放在自己的本地內存A中勇婴。
- 線程A和線程B需要通信時忱嘹,線程A首先會把自己本地內存中修改后的x值刷新到主內存中,此時主內存中的x值變了耕渴。
- 線程B到主內存中去讀取線程A更新后的x值拘悦,此時線程B的本地內存的x值也變了。
總結一下就是橱脸,這兩個步驟數(shù)據(jù)角度而言是線程A在向線程B發(fā)送消息础米,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互添诉, 來為程序提供內存可見性保證屁桑。
線程不安全因素之一(指令重排序問題)
基于上述所說的場景之下,JVM為了在執(zhí)行程序時為了提高性能栏赴,編譯器和處理器常常會對指令做重排序蘑斧。在此我們將按照重排序的執(zhí)行時間前后分為重排序分三種類型,如下圖所示。
[圖片上傳失敗...(image-ad7254-1679827701944)]
第一步屬于編譯器重排序:編譯器優(yōu)化的重排序竖瘾,編譯器在不改變單線程程序語義的前提下沟突,可以重新安排語句的執(zhí)行順序。
第二步屬于處理器重排序:指令級并行的重排序捕传,現(xiàn)代處理器采用了指令級并行技術(Instruction-Level Parallelism惠拭, ILP) 來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性庸论, 處理器可以改變語句對應機器指令的執(zhí)行順序求橄。
第三步屬于處理器重排序:內存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū)葡公,這使得加載和存儲操作看上去可能是在亂序執(zhí)行,此處特別是針對與本地內存和共享主存之間的更新操作的一致性和可見性
這些重排序都可能會導致多線程程序出現(xiàn)內存可見性問題条霜。
JMM解決重排序的線程不安全問題
解決編譯器級別重排序
- JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序催什,此處注意:不是所有的編譯器重排序都要禁止。
解決處理器級別重排序
- JMM的處理器重排序規(guī)則會要求java編譯器在生成指令序列時宰睡, 插入特定類型的內存屏障(memory barriers蒲凶, 也可以稱之為memory fence)指令, 通過 內存屏障 指令來禁止特定類型的處理器重排序拆内,此處注意:不是所有的處理器重排序都要禁止)旋圆。
總結一下,針對于JMM屬于語言級的內存模型麸恍, 它確保在不同的編譯器和不同的處理器平臺之上灵巧,通過禁止特定類型的編譯器重排序和處理器重排序,從而實現(xiàn)了內存的可見性以及一致性抹沪。
處理器重排序與內存屏障指令
上面說了其實是通過插入了內存屏障指令刻肄,從而控制住了對應的處理器級別的指令重排。
線程不安全因素之一(寫緩存處理模式)
現(xiàn)代的處理器使用寫緩沖區(qū)來臨時保存向內存寫入的數(shù)據(jù)融欧,寫緩沖區(qū)可以保證指令流水線持續(xù)運行敏弃,它可以避免由于處理器停頓下來等待向內存寫入數(shù)據(jù)而產生的延遲。
通過以批處理的方式刷新寫緩沖區(qū)噪馏,以及合并寫緩沖區(qū)中對同一內存地址的多次寫麦到,可以減少對內存總線的占用。雖然寫緩沖區(qū)有這么多好處欠肾,但每個處理器上的寫緩沖區(qū)瓶颠,僅僅對它所在的處理器可見。
這個特性會對內存操作的執(zhí)行順序產生重要的影響董济,處理器對內存的讀/寫操作的執(zhí)行順序步清,不一定與內存實際發(fā)生的讀/寫操作順序一致。
[圖片上傳失敗...(image-db09cd-1679827701944)]
- 處理器A和處理器B可以同時把共享變量寫入自己的寫緩沖區(qū)(A1,B1)
- 從內存中讀取另一個共享變量(A2廓啊,B2)
- 最后才把自己寫緩存區(qū)中保存的臟數(shù)據(jù)刷新到內存中(A3欢搜,B3)。
從內存操作實際發(fā)生的順序來看谴轮,直到處理器A執(zhí)行A3來刷新自己的寫緩存區(qū)炒瘟,寫操作A1才算真正執(zhí)行了。雖然處理器A執(zhí)行內存操作的順序為:A1->A2第步,但內存操作實際發(fā)生的順序卻是:A2->A1疮装。此時,處理器A的內存操作順序被重排序了(處理器B的情況和處理器A一樣)粘都。
由于現(xiàn)代的處理器都會使用寫緩沖區(qū)廓推,因此現(xiàn)代的處理器都會允許對寫-讀操作重排序。常見的處理器都允許Store-Load重排序翩隧,常見的處理器都不允許對存在數(shù)據(jù)依賴的操作做重排序樊展。
內存屏障指令
為了保證內存可見性, java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序堆生。JMM把內存屏障指令分為下列四類:
內存屏障類型 | 指令示例 | 備注 |
---|---|---|
LoadLoad Barries | Load1\LoadLoad\Load2 | 確保Load1數(shù)據(jù)的裝載专缠,之前于Load2及所有后續(xù)裝載指令的裝載 |
StoreStore Barries | Store1\StoreStore\Store2 | 確保Store1數(shù)據(jù)對其他處理器可見(刷新到內存),之前于Store2及所有后續(xù)存儲指令的存儲淑仆。 |
LoadStore Barriers | Load1\ LoadStore\Store2 | 確保Load1數(shù)據(jù)裝載涝婉, 之前于Store2及所有后續(xù)的存儲指令刷新到內存 |
StoreLoad Barriers | Store1\StoreLoad\Load2 | 確保Storel數(shù)據(jù)對其他處理器變得可見(指刷新到內存),之前于Load2及所有后續(xù)裝載指令的裝載蔗怠。StoreLoad Barriers會使該屏障之前的所有內存訪問指令(存儲和裝載指令)完成之后墩弯,才執(zhí)行該屏障之后的內存訪問指令。 |
**StoreLoad Barriers是一個“全能型”的屏障蟀淮, 它同時具有其他三個屏障的效果∽钭。現(xiàn)代的多處理器大都支持該屏障(其他類型的屏障不一定被所有處理器支持)。執(zhí)行該屏障開銷會很昂貴怠惶,因為當前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內存中(buffer fully flush) **涨缚。
未完善待續(xù)!