Java多線程的原子性,可見性,有序性你都了解嗎
問題:
1.什么是原子性、可見性、有序性?
1. 原子性問題
原子性、可見性、有序性是并發(fā)編程所面臨的三大問題。
所謂原子操作,就是“不可中斷的一個或一系列操作”,是指不會被線程調(diào)度機(jī)制打斷的操作。這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會有任何線程的切換。
例如對于 i++ 而言,實(shí)際會產(chǎn)生如下的 JVM 字節(jié)碼指令:
getstatic i // 獲取靜態(tài)變量i的值(內(nèi)存取值) iconst_1 // 準(zhǔn)備常量1 iadd // 自增 (寄存器增加1) putstatic i // 將修改后的值存入靜態(tài)變量i(存值到內(nèi)存)
如果是單線程以上 8 行代碼是順序執(zhí)行(不會交錯)沒有問題:
但多線程下這 8 行代碼可能交錯運(yùn)行:
出現(xiàn)負(fù)數(shù)的情況:
出現(xiàn)正數(shù)的情況:
一個自增運(yùn)算符是一個復(fù)合操作,“內(nèi)存取值”“寄存器增加1”和“存值到內(nèi)存”這三個JVM指令本身是不可再分的,它們都具備原子性,是線程安全的,也叫原子操作。但是,兩個或者兩個以上的原子操作合在一起進(jìn)行操作就不再具備原子性了。比如先讀后寫,就有可能在讀之后,其實(shí)這個變量被修改了,出現(xiàn)讀和寫數(shù)據(jù)不一致的情況。
因為這4個操作之間是可以發(fā)生線程切換的,或者說是可以被其他線程中斷的。所以,++操作不是原子操作,在并行場景會發(fā)生原子性問題。
2. 可見性問題
一個線程對共享變量的修改,另一個線程能夠立刻可見,我們稱該共享變量具備內(nèi)存可見性。
談到內(nèi)存可見性,要先引出Java內(nèi)存模型的概念。JMM規(guī)定,將所有的變量都存放在公共主存中,當(dāng)線程使用變量時會把主存中的變量復(fù)制到自己的工作內(nèi)存(私有內(nèi)存)中,線程對變量的讀寫操作,是自己工作內(nèi)存中的變量副本。
如果兩個線程同時操作一個共享變量,就可能發(fā)生可見性問題:
(1) 主存中有變量sum,初始值sum=0;
(2) 線程A計劃將sum加1,先將sum=0復(fù)制到自己的私有內(nèi)存中,然后更新sum的值,線程A操作完成之后其私有內(nèi)存中sum=1,然而線程A將更新后的sum值回刷到主存的時間是不固定的;
(3) 在線程A沒有回刷sum到主存前,剛好線程B同樣從主存中讀取sum,此時值為0,和線程A進(jìn)行同樣的操作,最后期盼的sum=2目標(biāo)沒有達(dá)成,最終sum=1;
線程A和線程B并發(fā)操作sum發(fā)生內(nèi)存可見性問題:
要想解決多線程的內(nèi)存可見性問題,所有線程都必須將共享變量刷新到主存,一種簡單的方案是:使用Java提供的關(guān)鍵字volatile修飾共享變量。
為什么Java局部變量、方法參數(shù)不存在內(nèi)存可見性問題?
在Java中,所有的局部變量、方法定義參數(shù)都不會在線程之間共享,所以也就不會有內(nèi)存可見性問題。所有的Object實(shí)例、Class實(shí)例和數(shù)組元素都存儲在JVM堆內(nèi)存中,堆內(nèi)存在線程之間共享,所以存在可見性問題。
3. 有序性問題
所謂程序的有序性,是指程序按照代碼的先后順序執(zhí)行。如果程序執(zhí)行的順序與代碼的先后順序不同,并導(dǎo)致了錯誤的結(jié)果,即發(fā)生了有序性問題。
@Slf4j public class Test3 { private static volatile int x=0,y=0; private static int a=0,b=0; public static void main(String[] args) throws InterruptedException { for(int i=0;;i++){ a=0; b=0; x=0; y=0; Thread t1 = new Thread(() -> { a = 1; x = b; }); Thread t2 = new Thread(() -> { b = 1; y = a; }); t1.start(); t2.start(); t1.join(); t2.join(); // 假如t1線程先執(zhí)行,t2線程后執(zhí)行,則結(jié)果為a=1,x=0,b=1,y=1 (0,1) // 假如t2線程先執(zhí)行,t1線程后執(zhí)行,則結(jié)果為b=1,y=0,a=1,x=1 (1,0) // 假如t1線程和t2線程的指令是同時或交替執(zhí)行的,則結(jié)果為a=1,b=1,x=1,y=1 (1,1) // 但是不可能出現(xiàn)(0,0) if(x==0 && y==0){ log.debug("x:{}, y:{}",x,y); } } } }
由于并發(fā)執(zhí)行的無序性,賦值之后x、y的值可能為(1,0)、(0,1)或(1,1)。為什么呢?因為線程t1可能在線程t2開始之前就執(zhí)行完了,也可能線程t2在線程t1開始之前就執(zhí)行完了,甚至有可能二者的指令是同時或交替執(zhí)行的。
然而,執(zhí)行以上代碼時,出乎意料的事情發(fā)生了:這段代碼的執(zhí)行結(jié)果也可能是(0,0),部分結(jié)果如下:
19:37:32.113 [main] DEBUG com.example.test.Test3 - x:0, y:0
19:37:33.041 [main] DEBUG com.example.test.Test3 - x:0, y:0
19:37:34.501 [main] DEBUG com.example.test.Test3 - x:0, y:0
19:37:41.825 [main] DEBUG com.example.test.Test3 - x:0, y:0
于以上程序來說,(0,0)結(jié)果是錯誤的,意味著已經(jīng)發(fā)生了并發(fā)的有序性問題。為什么會出現(xiàn)(0,0)結(jié)果呢?可能在程序的執(zhí)行過程中發(fā)生了指令重排序。對于線程t1來說,可能a=1和x=b這兩個語句的賦值操作順序被顛倒了,對于線程t2來說,可能b=1和y=a這兩個語句的賦值操作順序被顛倒了,從而出現(xiàn)了(x,y)值為(0,0)的錯誤結(jié)果。
什么是指令重排序?
一般來說,CPU為了提高程序運(yùn)行效率,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個語句的執(zhí)行順序同代碼中的先后順序一致,但是它會保證程序最終的執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
重排序也是單核時代非常優(yōu)秀的優(yōu)化手段,有足夠多的措施保證其在單核下的正確性。在多核時代,如果工作線程之間不共享數(shù)據(jù)或僅共享不可變數(shù)據(jù),重排序也是性能優(yōu)化的利器。然而,如果工作線程之間共享了可變數(shù)據(jù),由于兩種重排序的結(jié)果都不是固定的,因此會導(dǎo)致工作線程似乎表現(xiàn)出了隨機(jī)行為。指令重排序不會影響單個線程的執(zhí)行,但是會影響多個線程并發(fā)執(zhí)行的正確性。
事實(shí)上,輸出了亂序的結(jié)果,并不代表一定發(fā)生了指令重排序,內(nèi)存可見性問題也會導(dǎo)致這樣的輸出。但是,指令重排序也是導(dǎo)致亂序的原因之一。
總之,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個沒有得到保證,就有可能會導(dǎo)致程序運(yùn)行不正確。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
如何去除Java中List集合中的重復(fù)數(shù)據(jù)
這篇文章主要介紹了Java中List集合去除重復(fù)數(shù)據(jù)的方法,對大家的工作或?qū)W習(xí)有一定價值,有需求的朋友可以參考下2020-05-05Java面向?qū)ο蠡A(chǔ)知識之?dāng)?shù)組和鏈表
這篇文章主要介紹了Java面向?qū)ο蟮闹當(dāng)?shù)組和鏈表,文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)java基礎(chǔ)的小伙伴們有很好的幫助,需要的朋友可以參考下2021-11-11SpringBoot實(shí)現(xiàn)定時任務(wù)動態(tài)管理示例
這篇文章主要為大家介紹了SpringBoot實(shí)現(xiàn)定時任務(wù)動態(tài)管理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Spring報錯:Error creating bean with name的問
這篇文章主要介紹了Spring報錯:Error creating bean with name的問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08spring框架下@value注解屬性static無法獲取值問題
這篇文章主要介紹了spring框架下@value注解屬性static無法獲取值問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11