" />

欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java多線程之線程安全問題詳解

 更新時(shí)間:2022年03月02日 16:56:35   作者:小小茶花女  
這篇文章主要為大家詳細(xì)介紹了Java多線程之線程安全問題,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助

面試題:

  • 什么是線程安全和線程不安全?
  • 自增運(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)文章

最新評論