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