Java多線程之線程安全問題詳解
面試題:
- 什么是線程安全和線程不安全?
- 自增運(yùn)算是不是線程安全的?如何保證多線程下 i++ 結(jié)果正確?
1. 什么是線程安全和線程不安全?
什么是線程安全呢?當(dāng)多個(gè)線程并發(fā)訪問某個(gè)Java對象時(shí),無論系統(tǒng)如何調(diào)度這些線程,也無論這些線程將如何交替操作,這個(gè)對象都能表現(xiàn)出一致的、正確的行為,那么對這個(gè)對象的操作是線程安全的。
如果這個(gè)對象表現(xiàn)出不一致的、錯(cuò)誤的行為,那么對這個(gè)對象的操作不是線程安全的,發(fā)生了線程的安全問題。
2. 自增運(yùn)算為什么不是線程安全的?
線程安全實(shí)驗(yàn):兩個(gè)線程對初始值為 0 的靜態(tài)變量一個(gè)做自增,一個(gè)做自減,各做 5000 次,結(jié)果是 0 嗎?具體的代碼如下
public class ThreadDemo { private static int i = 0; public static void main(String[] args) throws InterruptedException { // 線程1對變量i做5000次自增運(yùn)算 Thread t1 = new Thread(()->{ for(int j=0;j<5000;j++){ i++; } }); Thread t2 = new Thread(()->{ for(int j=0;j<5000;j++){ i--; } }); t1.start(); t2.start(); // 主線程等待t1線程和t2線程執(zhí)行結(jié)束再繼續(xù)執(zhí)行 t1.join(); t2.join(); System.out.println(i);// 581 / -1830 / 0 } }
以上的結(jié)果可能是正數(shù)、負(fù)數(shù)、零。為什么呢?因?yàn)?Java 中對靜態(tài)變量的自增,自減并不是原子操作,要徹底理解,必須從字節(jié)碼來進(jìn)行分析。
例如對于 i++ 而言,實(shí)際會(huì)產(chǎn)生如下的 JVM 字節(jié)碼指令:
getstatic i // 獲取靜態(tài)變量i的值 iconst_1 // 準(zhǔn)備常量1 iadd // 自增 putstatic i // 將修改后的值存入靜態(tài)變量i
而對應(yīng) i-- 也是類似:
getstatic i // 獲取靜態(tài)變量i的值 iconst_1 // 準(zhǔn)備常量1 isub // 自減 putstatic i // 將修改后的值存入靜態(tài)變量
而 Java 的內(nèi)存模型如下,完成靜態(tài)變量的自增,自減需要在主存和工作內(nèi)存中進(jìn)行數(shù)據(jù)交換:
如果是單線程以上 8 行代碼是順序執(zhí)行(不會(huì)交錯(cuò))沒有問題:
但多線程下這 8 行代碼可能交錯(cuò)運(yùn)行:
出現(xiàn)負(fù)數(shù)的情況:
出現(xiàn)正數(shù)的情況:
因此,一個(gè)自增運(yùn)算符是一個(gè)復(fù)合操作,至少包括三個(gè)JVM指令:“內(nèi)存取值”“寄存器增加1”和“存值到內(nèi)存”。這三個(gè)指令在JVM內(nèi)部是獨(dú)立進(jìn)行的,中間完全可能會(huì)出現(xiàn)多個(gè)線程并發(fā)進(jìn)行。“內(nèi)存取值”“寄存器增加1”和“存值到內(nèi)存”這三個(gè)JVM指令本身是不可再分的,它們都具備原子性,是線程安全的,也叫原子操作。但是,兩個(gè)或者兩個(gè)以上的原子操作合在一起進(jìn)行操作就不再具備原子性了。比如先讀后寫,就有可能在讀之后,其實(shí)這個(gè)變量被修改了,出現(xiàn)讀和寫數(shù)據(jù)不一致的情況。
3. 臨界區(qū)資源和競態(tài)條件
在多個(gè)線程操作相同資源(如變量、數(shù)組或者對象)時(shí)就可能出現(xiàn)線程安全問題。一般來說,只在多個(gè)線程對這個(gè)資源進(jìn)行寫操作的時(shí)候才會(huì)出現(xiàn)問題,如果是簡單的讀操作,不改變資源的話,顯然是不會(huì)出現(xiàn)問題的。
臨界區(qū)資源表示一種可以被多個(gè)線程使用的公共資源或共享數(shù)據(jù),但是每一次只能有一個(gè)線程使用它。一旦臨界區(qū)資源被占用,想使用該資源的其他線程則必須等待。在并發(fā)情況下,臨界區(qū)資源是受保護(hù)的對象。
臨界區(qū)代碼段是每個(gè)線程中訪問臨界資源的那段代碼,多個(gè)線程必須互斥地對臨界區(qū)資源進(jìn)行訪問。線程進(jìn)入臨界區(qū)代碼段之前,必須在進(jìn)入?yún)^(qū)申請資源,申請成功之后執(zhí)行臨界區(qū)代碼段,執(zhí)行完成之后釋放資源。臨界區(qū)代碼段的進(jìn)入和退出如圖所示:
競態(tài)條件可能是由于在訪問臨界區(qū)代碼段時(shí)沒有互斥地訪問而導(dǎo)致的特殊情況。如果多個(gè)線程在臨界區(qū)代碼段的并發(fā)執(zhí)行結(jié)果可能因?yàn)榇a的執(zhí)行順序不同而不同,我們就說這時(shí)在臨界區(qū)出現(xiàn)了競態(tài)條件問題。
比如下面代碼中的臨界區(qū)資源和臨界區(qū)代碼段:
public class SafeDemo { // 臨界區(qū)資源 private static int i = 0; // 臨界區(qū)代碼段 public void selfIncrement(){ for(int j=0;j<5000;j++){ i++; } } // 臨界區(qū)代碼段 public void selfDecrement(){ for(int j=0;j<5000;j++){ i--; } } // 這個(gè)不是臨界區(qū)代碼,因?yàn)殡m然使用了共享資源,但是這個(gè)方法并沒有被多個(gè)線程同時(shí)訪問 public int getI(){ return i; } }
public class ThreadDemo { public static void main(String[] args) throws InterruptedException { SafeDemo safeDemo = new SafeDemo(); Thread t1 = new Thread(()->{ safeDemo.selfIncrement(); }); Thread t2 = new Thread(()->{ safeDemo.selfDecrement(); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(safeDemo.getI()); } }
當(dāng)多個(gè)線程訪問臨界區(qū)的selfIncrement()方法時(shí),就會(huì)出現(xiàn)競態(tài)條件的問題。更標(biāo)準(zhǔn)地說,當(dāng)兩個(gè)或多個(gè)線程競爭同一個(gè)資源時(shí),對資源的訪問順序就變得非常關(guān)鍵。為了避免競態(tài)條件的問題,我們必須保證臨界區(qū)代碼段操作具備排他性。這就意味著當(dāng)一個(gè)線程進(jìn)入臨界區(qū)代碼段執(zhí)行時(shí),其他線程不能進(jìn)入臨界區(qū)代碼段執(zhí)行。
總結(jié):
(1) 一個(gè)程序運(yùn)行多個(gè)線程本身是沒有問題的,問題出在多個(gè)線程訪問共享資源,多個(gè)線程讀共享資源其實(shí)也沒有問題,而在多個(gè)線程對共享資源讀寫操作時(shí)發(fā)生指令交錯(cuò),就會(huì)出現(xiàn)問題 ;
(2) 一段代碼塊內(nèi)如果存在對共享資源的多線程讀寫操作,稱這段代碼塊為臨界區(qū)代碼塊;
(3) 多個(gè)線程在臨界區(qū)內(nèi)執(zhí)行,由于代碼的執(zhí)行序列不同而導(dǎo)致結(jié)果無法預(yù)測,稱之為發(fā)生了競態(tài)條件;
在Java中,可以使用synchronized關(guān)鍵字,使用Lock顯式鎖實(shí)例,或者使用原子變量(AtomicVariables)對臨界區(qū)代碼段進(jìn)行排他性保護(hù)。
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
java validation 后臺參數(shù)驗(yàn)證的使用詳解
本篇文章主要介紹了java validation 后臺參數(shù)驗(yàn)證的使用詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10springboot集成swagger3與knife4j的詳細(xì)代碼
這篇文章主要介紹了springboot集成swagger3與knife4j,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08SpringBoot-JPA刪除不成功,只執(zhí)行了查詢語句問題
這篇文章主要介紹了SpringBoot-JPA刪除不成功,只執(zhí)行了查詢語句問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Java 多線程并發(fā)編程_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java 多線程并發(fā)編程的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-05-05Java FileDescriptor總結(jié)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
FileDescriptor 是“文件描述符”??梢员挥脕肀硎鹃_放文件、開放套接字等。接下來通過本文給大家分享Java FileDescriptor總結(jié),感興趣的朋友一起學(xué)習(xí)吧2017-05-05