最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java多線程開發(fā)之并發(fā)安全詳解

 更新時(shí)間:2026年01月02日 11:18:46   作者:木井巳  
這篇文章主要介紹了Java多線程開發(fā)之并發(fā)安全,并發(fā)安全是指在多個(gè)線程同時(shí)訪問共享資源時(shí),通過鎖、volatile、原子類或線程安全集合等機(jī)制,防止數(shù)據(jù)競(jìng)爭(zhēng)和不一致,確保程序執(zhí)行的正確性和穩(wěn)定性,需要的朋友可以參考下

一、體會(huì)線程安全問題

當(dāng)我們編寫一個(gè)多線程程序,要求兩個(gè)線程對(duì)同一個(gè)變量(共享變量)進(jìn)行修改,得到的結(jié)果是否與預(yù)期一致?

創(chuàng)建兩個(gè)線程,分別對(duì)共享變量(count)進(jìn)行自增5萬次操作,最后輸出的結(jié)果理論上應(yīng)為10萬,但是實(shí)際上輸出的結(jié)果是一個(gè)小于10萬且不確定的數(shù)。

讀者可以自行實(shí)現(xiàn)一下該多線程程序,運(yùn)行后看看結(jié)果是否符合預(yù)期。

public class Demo14_threadSafety {
    private static int count = 0;
    public static void main1(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t1-結(jié)束");
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t2-結(jié)束");
        });
        t1.start();
        t2.start();
        // 理論上輸出的結(jié)果應(yīng)是100000,實(shí)際輸出的結(jié)果是0
        // 原因是主線程 main 運(yùn)行太快了,當(dāng) t1 和 t2 線程還在計(jì)算時(shí),主線程已經(jīng)打印結(jié)果、運(yùn)行完畢了
        System.out.println(count);
    }
    // 讓主線程等待 t1 和 t2 線程,等到它們兩個(gè)都執(zhí)行完成再打印,故使用 join 方法
    public static void main2(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t1-結(jié)束");
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t2-結(jié)束");
        });
        t1.start();
        t2.start();
        // 在主線程中,通過 t1 和 t2 對(duì)象調(diào)用 join 方法
        // 表示讓主線程 main 等待 t1 線程和 t2 線程
        t1.join();
        t2.join();
        // 當(dāng)兩個(gè)線程都執(zhí)行完畢后主線程再繼續(xù)執(zhí)行打印操作
        System.out.println(count);
        // 實(shí)際輸出的結(jié)果小于100000,仍不符合預(yù)期
    }
}

二、線程安全的概念

通過上面的一個(gè)例子,想必讀者已經(jīng)體會(huì)到線程安全問題了吧?那究竟什么是線程安全問題呢?其原因是什么?如何解決線程安全問題呢?

不要急,且聽小編慢慢道來~

如果在多線程環(huán)境下運(yùn)行的程序其結(jié)果符合預(yù)期或與在單線程環(huán)境下運(yùn)行的結(jié)果一致,就說這個(gè)程序是線程安全的,否則是線程不安全的。

上面的例子在單線程環(huán)境下運(yùn)行——比如來兩個(gè)循環(huán)對(duì)共享變量進(jìn)行自增操作,那么結(jié)果是符合預(yù)期的;但是在多線程環(huán)境下運(yùn)行就不符合預(yù)期。因此該程序是線程不安全的,也可以說該程序存在線程安全問題。

三、線程安全問題的原因

究竟是哪里出問題導(dǎo)致程序出現(xiàn)線程安全問題呢?

究其根本,罪魁禍?zhǔn)资?span> 操作系統(tǒng)的線程調(diào)度有隨機(jī)性/搶占式執(zhí)行 

由于操作系統(tǒng)的線程調(diào)度是有隨機(jī)性的,這就會(huì)存在這種情況:某一個(gè)線程還沒執(zhí)行完呢,就調(diào)度到其他線程去執(zhí)行了,從而導(dǎo)致數(shù)據(jù)不正確。

當(dāng)然了,一個(gè)巴掌拍不響,還有以下三個(gè)導(dǎo)致線程不安全的原因:

  • 原子性:指 Java 語句,一條 Java 語句可能對(duì)應(yīng)不止一條指令,若對(duì)應(yīng)一條指令,就是原子的。
  • 可見性:一個(gè)線程對(duì)主內(nèi)存(共享變量)的修改,可以及時(shí)被其他線程看到。
  • 有序性:一個(gè)線程觀察其他線程中指令的執(zhí)行順序,由于 JVM 對(duì)指令進(jìn)行了重排序,觀察到的順序一般比較雜亂。因其原理與 CPU 及編譯器的底層原理有關(guān),暫不討論。

之前的例子就是由于原子性沒有得到保障而出現(xiàn)線程安全問題:

public static void main2(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            count++;
        }
        System.out.println("t1-結(jié)束");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            count++;
        }
        System.out.println("t2-結(jié)束");
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

1. “count ++” 這條語句對(duì)應(yīng)多條指令:讀取數(shù)據(jù)、計(jì)算結(jié)果、存儲(chǔ)數(shù)據(jù)。

2. t1 線程和 t2 線程分別執(zhí)行一次“count++”語句,期望的結(jié)果是“count = 2”,其過程如下:

初始情況:

當(dāng)線程執(zhí)行“count ++”時(shí),總共分三個(gè)步驟:load、update、save,由于線程調(diào)度的隨機(jī)性/搶占式執(zhí)行,可能會(huì)出現(xiàn)以下情況(可能出現(xiàn)的情況有很多種,這里只是其中一種):

這時(shí)候 t1 正在執(zhí)行“count ++”這條語句,執(zhí)行了“load 和 update”指令后,t1 的工作內(nèi)存(寄存器)存著更新后的值,但是還未被寫回內(nèi)存中:

接著調(diào)度到 t2 線程并開始執(zhí)行“count ++”語句,并且語句中包含的三條指令都執(zhí)行。此時(shí)由于 t1 更新后的 count 的值還未寫回內(nèi)存,因此 t2 執(zhí)行 load 操作所獲取到的 count 仍是 0。接著 t2 執(zhí)行 update 和 save 指令:

當(dāng) t2 執(zhí)行完成,內(nèi)存的 count 已被修改為 1 。此時(shí)調(diào)度回 t1 線程并繼續(xù)執(zhí)行 save 指令,但是 t1 線程寄存器中 count 的值也是 1 ,此時(shí)寫回內(nèi)存更新后 count 的值依然是 1 。

結(jié)果 count = 1,與預(yù)期的 count = 2 不符,因此存在線程安全問題,其原因是操作系統(tǒng)的隨機(jī)線程調(diào)度和 count 語句存在非原子性。

四、解決線程安全問題的方法

從上面的例子我們知道,當(dāng)一條語句的指令被拆開來執(zhí)行的話是存在線程安全問題的,但是,當(dāng)我們將“count ++”這條語句的三個(gè)指令都放在一起執(zhí)行怎么樣?

當(dāng)線程調(diào)度的情況如下:

此時(shí) t1 線程開始執(zhí)行“count ++”語句的 load、update 和 save 指令。內(nèi)存中的  count 為 0,t1 讀取到內(nèi)存中的 count 之后更新至 1 并寫回內(nèi)存中。當(dāng) t1 執(zhí)行完成后內(nèi)存的 count 由 0 更新至 1:

接著調(diào)度至 t2 線程,開始執(zhí)行“count ++”語句的 load、update 和 save 指令。經(jīng)過更新后內(nèi)存中的 count 為 1,此時(shí) t2 讀取 count 并更新為 2,然后寫回內(nèi)存中。當(dāng) t2 執(zhí)行完成,內(nèi)存中的 count 就更新成 2 了:

可以發(fā)現(xiàn),結(jié)果與預(yù)期相符!說明這個(gè)方法可行。

可以將操作順序改成先讓 t1 線程完成“count ++”操作,再讓 t2 線程完成該操作——即串行執(zhí)行。

現(xiàn)在我們對(duì)之前的例子進(jìn)行優(yōu)化:

// 可以試著讓 t1 線程先執(zhí)行完后,再讓 t2 線程執(zhí)行,改成串行執(zhí)行
public static void main3(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            count++;
        }
        System.out.println("t1-結(jié)束");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            count++;
        }
        System.out.println("t2-結(jié)束");
    });
    t1.start();
    t1.join();
    t2.start();
    t2.join();
    System.out.println(count);
}

剛剛是讓一個(gè)線程一次性執(zhí)行“count ++”這條語句的三個(gè)指令,也就是說,我們是通過這樣操作將原本是非原子的三條指令打包成了一個(gè)原子指令(即執(zhí)行過程中不可被打斷——調(diào)度走)。這樣就有效的解決了線程安全問題。

而上述的操作,其實(shí)就是 Java 中的 加鎖操作

當(dāng)一個(gè)線程執(zhí)行一個(gè)非原子的語句時(shí),通過加鎖操作可以防止在執(zhí)行過程中被調(diào)度走或被其他線程打斷,若其他線程想要執(zhí)行該語句,則要進(jìn)入 阻塞等待 的狀態(tài),當(dāng)線程執(zhí)行完畢并將鎖釋放,操作系統(tǒng)這時(shí)喚醒等待中的線程,才可以執(zhí)行該語句。

就相當(dāng)于上廁所:當(dāng)廁所內(nèi)沒有人時(shí)(沒有線程加鎖),就可以使用;當(dāng)廁所內(nèi)有人時(shí)(已經(jīng)有線程加鎖了),那么就必須等里面的人出來后才能使用。

注意:

  1. 前一個(gè)線程解鎖之后,并不是后一個(gè)線程立刻獲取到鎖。而是需要靠操作系統(tǒng)喚醒阻塞等待中的線程的。
  2. 若 t1、t2 和 t3 三個(gè)線程競(jìng)爭(zhēng)同一個(gè)鎖,當(dāng) t1 線程獲取到鎖,t2 線程再嘗試獲取鎖,接著 t3 線程嘗試獲取鎖,此時(shí) t2 和 t3 線程都因獲取鎖失敗而處于阻塞等待狀態(tài)。當(dāng) t1 線程釋放鎖之后,t2 線程并不會(huì)因?yàn)橄冗M(jìn)入阻塞狀態(tài)在被喚醒后比 t3 先拿到鎖,而是和 t3 進(jìn)行公平競(jìng)爭(zhēng)。(不遵循先來后到原則)

4.1 synchronized關(guān)鍵字

在處理由原子性導(dǎo)致的線程安全問題時(shí),通常采用加鎖操作。

加鎖 / 解鎖這些操作本身是在操作系統(tǒng)所提供的 API 中的,很多編程語言對(duì)其進(jìn)行了封裝,Java 中使用 synchronized 關(guān)鍵字來進(jìn)行加鎖 / 解鎖操作,其底層是使用操作系統(tǒng)的 mutex lock 來實(shí)現(xiàn)的。Java 中的任何一個(gè)對(duì)象都可以用作“鎖”。

synchronized (鎖對(duì)象){             ——> 進(jìn)入代碼塊,相當(dāng)于加鎖操作

        // 一些需要保護(hù)的邏輯

}                                                       ——> 出了代碼塊,相當(dāng)于解鎖操作

當(dāng)多個(gè)線程針對(duì)同一個(gè)鎖對(duì)象競(jìng)爭(zhēng)的時(shí)候,加鎖操作才有意義。

對(duì)之前的例子進(jìn)行加鎖操作:

public class Demo15_synchronized {
    private static int count = 0;
    public static void main1(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
            System.out.println("t1-結(jié)束");
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
            System.out.println("t2-結(jié)束");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

synchronized 關(guān)鍵字用來修飾 普通方法 時(shí),相當(dāng)于給 this 加鎖;

synchronized 關(guān)鍵字用來修飾 靜態(tài)方法 時(shí),相當(dāng)于給 類對(duì)象 加鎖。

于是可以使用另一種寫法:

// 寫法二:
// 將 count++ 所包含的三個(gè)操作封裝成一個(gè) add 方法
// 使用 synchronized 修飾 add 方法
class Counter {
    private int count = 0;
    synchronized public void add () {
        // synchronized 修飾普通方法相當(dāng)于給 this 加鎖
        count++;
    }
    // 相當(dāng)于:
    // public void add () {
    //     synchronized (this) {
    //         count++;
    //     }
    // }
    public int get () {
        return count;
    }
}
public static void main(String[] args) throws InterruptedException {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.add();
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.get());
}

這樣一來,就成功解決了多線程程序中由原子性導(dǎo)致的線程安全問題。

4.2 volatile關(guān)鍵字

我們?cè)賮硪粋€(gè)例子:

讓 t1 線程讀取共享變量的值,然后讓 t2 線程修改共享變量的值,我們可以寫出如下程序:

public class Demo17_volatile {
    private static int flag = 0;
    public static void main1(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                // 當(dāng) flag 為 0 時(shí)一直循環(huán)
            }
            System.out.println("t1-結(jié)束");
        });
        Thread t2 = new Thread(() -> {
            // 對(duì) flag 進(jìn)行修改
            Scanner in = new Scanner(System.in);
            System.out.println("請(qǐng)輸入 flag 的值:");
            flag = in.nextInt();
        });
        t1.start();
        t2.start();
        // 運(yùn)行后發(fā)現(xiàn)即使輸入1,t1 線程并不會(huì)結(jié)束
    }
}

我們期望該程序運(yùn)行后,輸入非零的數(shù)如 1,t1 線程能夠結(jié)束,實(shí)際并非如此,很顯然,這也是出現(xiàn)了線程安全問題。

這次出現(xiàn)問題的原因并非原子性,而是 可見性 。我們說過,可見性是一個(gè)線程對(duì)主內(nèi)存(共享變量)的修改能夠被其他線程及時(shí)看到,若不能被其他線程及時(shí)看到,就會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)誤,從而導(dǎo)致線程安全問題。

如果我們使用鎖來處理的話,好像并不能解決。

我們先來認(rèn)識(shí)一個(gè)東西:編譯器優(yōu)化。

由于不能保證寫代碼的人每一次寫代碼都不會(huì)出錯(cuò),所以開發(fā) JDK 的先人們就讓編譯器 / JVM 能夠根據(jù)代碼的原有邏輯進(jìn)行優(yōu)化。

在我們這個(gè)程序中:

  • while {...load...cmp...} 先將數(shù)據(jù)從內(nèi)存中 load 到寄存器中,然后在寄存器進(jìn)行 “條件比較” 指令 cmp 。在短時(shí)間內(nèi)用戶還沒來得及輸入呢,但是這個(gè)循環(huán)語句能夠執(zhí)行成萬上億次且內(nèi)存中的 flag 的值一直都是 0。
  • 那么這時(shí)候編譯器 / JVM 就察覺到:既然一直讀取 flag 都是同一個(gè)值,那干脆我直接讀取寄存器算了(讀取寄存器開銷更?。?。
  • 這樣一來,當(dāng)用戶輸入 1 的時(shí)候(存入內(nèi)存),t1 線程就無法讀取通過 t2 線程更新之后的值了,也就不會(huì)結(jié)束。

在上面的程序中,t2 線程修改后的值無法被 t1 線程看到,這就是可見性導(dǎo)致的線程安全問題,由于編譯器我們沒辦法更改,所以只好另尋他法。

如果我們能讓循環(huán)執(zhí)行的慢一點(diǎn),是不是就能解決問題了?

嘗試優(yōu)化該程序:

public static void main2(String[] args) {
    Thread t1 = new Thread(() -> {
        while (flag1 == 0) {
            // 當(dāng) flag1 為 0 時(shí)一直循環(huán)
            try {
                Thread.sleep(1);
                // 放慢讀取速度
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("t1-結(jié)束");
    });
    Thread t2 = new Thread(() -> {
        // 對(duì) flag1 進(jìn)行修改
        Scanner in = new Scanner(System.in);
        System.out.println("請(qǐng)輸入 flag1 的值:");
        flag1 = in.nextInt();
    });
    t1.start();
    t2.start();
}

在運(yùn)行優(yōu)化版的程序后,發(fā)現(xiàn)問題被解決了,其原因是:使用了 sleep(1)—— 表示每一次讀取的時(shí)候休眠 1毫秒,這對(duì)于讀取操作的速度來說已經(jīng)很慢了。因此不會(huì)觸發(fā)編譯器優(yōu)化,也就不會(huì)出現(xiàn)可見性導(dǎo)致的線程安全問題了。

但是,在實(shí)際開發(fā)環(huán)境中頻繁使用 sleep 的話會(huì)導(dǎo)致程序效率下降,這樣的話用戶體驗(yàn)就會(huì)變差。

Java 中使用 volatile 關(guān)鍵字來處理 可見性 導(dǎo)致的線程安全問題。

使用 volatile 關(guān)鍵字來修飾共享變量,這樣一來這個(gè)變量無論如何都不會(huì)被編譯器優(yōu)化。

具體流程是:當(dāng) t1 線程讀取被修飾的變量時(shí),會(huì)強(qiáng)制讀取主內(nèi)存中變量最新的值;當(dāng) t2 線程修改被修飾的變量時(shí),在工作內(nèi)存(寄存器)中更新變量的值之后,立即將改變的值刷新至主內(nèi)存。

加上 volatile 關(guān)鍵字強(qiáng)制讀取內(nèi)存,雖然速度慢了,但是保證數(shù)據(jù)不出錯(cuò)。

使用 volatile 關(guān)鍵字優(yōu)化后的程序:

// 使用 volatile 關(guān)鍵字來修飾被讀取的變量,此時(shí)無論讀取速度怎樣,JVM 都不會(huì)對(duì)該變量進(jìn)行優(yōu)化
private volatile static int flag = 0;
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        while (flag == 0) {
            // 當(dāng) flag 為 0 時(shí)一直循環(huán)
        }
        System.out.println("t1-結(jié)束");
    });
    Thread t2 = new Thread(() -> {
        // 對(duì) flag 進(jìn)行修改
        Scanner in = new Scanner(System.in);
        System.out.println("請(qǐng)輸入 flag 的值:");
        flag = in.nextInt();
    });
    t1.start();
    t2.start();
}

注意:

  • volatile 關(guān)鍵字只能保證可見性,不能保證原子性,因此不能完全解決線程安全問題。
  • 當(dāng)多線程中存在 ++、--、+=、-=、*= 和 /= 或者類似的操作符時(shí)就不能夠使用 volatile 關(guān)鍵字來處理,需要使用 synchronized 關(guān)鍵字。

五、死鎖

那么話又說回來,如果對(duì)鎖使用不當(dāng),就很容易出現(xiàn)死鎖。

如果一個(gè)線程已經(jīng)獲取了鎖,再次獲取這個(gè)鎖,能成功嗎?會(huì)出現(xiàn)死鎖嗎?

Object locker = new Object();
synchronized (locker) {
    synchronized (locker) {
        // 一些需要保護(hù)的邏輯
    }
}

按照前面的邏輯,第一次獲取鎖對(duì)象后,該鎖對(duì)象已被占有,下一次獲取該鎖對(duì)象時(shí)應(yīng)該會(huì)觸發(fā)阻塞等待。而要想解除阻塞等待就得往下繼續(xù)執(zhí)行,但是要想往下執(zhí)行就得將鎖解開。這樣不就構(gòu)成死鎖了嘛?

不妨試試在你的 IDE 上運(yùn)行一下~

5.1 synchronized的可重入性

如果你在 IDE 上運(yùn)行了剛剛的程序,可以發(fā)現(xiàn)程序是沒問題的。

按照我們這之前的邏輯,當(dāng)鎖對(duì)象第一次被獲取到,其他的線程再想獲取鎖對(duì)象時(shí)就只能阻塞等待。但是,第二次獲取鎖對(duì)象的是同一個(gè)線程呀~ 因此并不會(huì)觸發(fā)阻塞等待。

  • Java 中的 synchronized 關(guān)鍵字是具有可重入性的,即對(duì)于同一個(gè)線程可以重復(fù)獲取同一個(gè)鎖對(duì)象。
  • 可重入鎖內(nèi)部包含了“線程持有者”和“計(jì)數(shù)器”兩個(gè)信息,若線程加鎖時(shí)發(fā)現(xiàn)鎖已被占有且恰好是自己,此時(shí)仍可以獲取到鎖,讓計(jì)數(shù)器自增即可;當(dāng)計(jì)數(shù)器為 0 時(shí),將鎖釋放,其他線程可以才獲取到鎖。

這樣就可以避免死鎖的出現(xiàn)——開發(fā) JDK 的前人們真的為我們考慮了太多??

5.2 死鎖的概念

  • 當(dāng)有兩個(gè)線程為了保護(hù)兩個(gè)不同的共享資源而使用兩個(gè)不同的鎖且這兩個(gè)鎖使用不當(dāng)時(shí),就會(huì)造成兩個(gè)線程都在等待對(duì)方解鎖,在沒有外界干擾的情況下他們會(huì)一直相互等待,此時(shí)就是發(fā)生了死鎖。
  • 死鎖需要同時(shí)具備以下四個(gè)條件:
  • 互斥條件:要求同一個(gè)資源不能被多個(gè)線程同時(shí)占有。
  • 不可剝奪條件:當(dāng)資源已被某個(gè)線程占有,其他線程只能等到該線程使用完并釋放后才能獲取,不可以強(qiáng)行打斷并獲取。
  • 持有并等待條件:有三個(gè)線程兩個(gè)資源,當(dāng)線程 1 已經(jīng)占有資源 A(線程 2 嘗試獲取資源 A 但觸發(fā)阻塞等待)且嘗試獲取資源 B 時(shí)(此時(shí)資源 B 已被線程 3 占有),線程 1 就會(huì)觸發(fā)阻塞等待且不釋放手中持有的資源 A 。
  • 循環(huán)等待/環(huán)路等待條件:有兩個(gè)線程兩個(gè)資源,線程 1 已占有資源 A 并想要獲取資源 B ,但是資源 B 已被線程 2 占有,并且線程 2 在占有資源 B 的同時(shí)想要獲取資源 A 。

比如,有兩個(gè)線程 t1 和 t2 以及兩個(gè)鎖 locker1 和 locker2,t1 先獲取 locker1 并嘗試獲取 locker2;t2 先獲取 locker2 并嘗試獲取 locker1:

// 兩個(gè)線程兩把鎖,每個(gè)線程獲取到一把鎖之后嘗試獲取對(duì)方的鎖
public static void main2(String[] args) throws InterruptedException {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            // t1 線程獲取到鎖對(duì)象 locker1
            try {
                Thread.sleep(1000);
                // 確保 t2 線程拿到 locker2
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 嘗試獲取鎖對(duì)象 locker2
            synchronized (locker2) {
                System.out.println("t1 線程獲取到兩把鎖");
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker2) {
            // t2 線程獲取到鎖對(duì)象 locker2
            try {
                Thread.sleep(1000);
                // 確保 t1 線程拿到 locker1
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 嘗試獲取鎖對(duì)象 locker1
            synchronized (locker1) {
                System.out.println("t2 線程獲取到兩把鎖");
            }
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
}

運(yùn)行該程序會(huì)發(fā)現(xiàn)什么也沒輸出,程序卻還在運(yùn)行。

這就是發(fā)生了死鎖。

我們借助第三方工具來觀察線程的狀態(tài):

5.3 如何避免死鎖

要想避免死鎖,我們就要從死鎖的四個(gè)條件入手,其中,互斥條件和不可剝奪條件基本上是無法打破的,因?yàn)檫@兩個(gè)是 synchronized 鎖的基本特性,因此我們選擇從后兩個(gè)條件入手。

1. 持有并等待條件:

通常是由于代碼中的嵌套加鎖導(dǎo)致的,因此選擇將嵌套的加鎖代碼改成串行的加鎖代碼(先將已占有的資源釋放掉,然后去獲取另一個(gè)資源):

// 避免死鎖的寫法:打破持有并等待條件——將嵌套加鎖改成串行加鎖
public static void main(String[] args) throws InterruptedException {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            // t1 線程獲取到鎖對(duì)象 locker1
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        // t1 線程釋放已占有的鎖對(duì)象 locker1
        // 嘗試獲取鎖對(duì)象 locker2
        synchronized (locker2) {
            System.out.println("t1 線程獲取到兩把鎖");
            // t1 獲取到兩把鎖之后結(jié)束執(zhí)行,此時(shí) locker1 和 locker2 均被釋放
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker2) {
            // t2 線程獲取到鎖對(duì)象 locker2
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        // t2 線程釋放已占有的鎖對(duì)象 locker2
        // 嘗試獲取鎖對(duì)象 locker1
        synchronized (locker1) {
            System.out.println("t2 線程獲取到兩把鎖");
            // t2 獲取到兩把鎖后結(jié)束執(zhí)行
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
}

2. 循環(huán)等待/環(huán)路等待條件:

這種情況一般都是雙方都持有對(duì)方想要的鎖但是都不肯釋放,所以我們就調(diào)整一下順序:讓 t1 和 t2 獲取鎖的順序都是 locker1 和 locker2,當(dāng) t1 已占有 locker1 時(shí) t2 想獲取只能阻塞等待,但這時(shí) t1 可以獲取 locker2,等待 t1 兩把鎖都獲取到并釋放之后,t2 被喚醒并且獲取兩把鎖。

// 防止死鎖的寫法:打破循環(huán)等待條件——調(diào)整加鎖順序
public static void main(String[] args) throws InterruptedException {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            // t1 線程獲取到鎖對(duì)象 locker1
            try {
                Thread.sleep(1000);
                // 此時(shí) t2 因 locker1 被 t1 獲取而處于阻塞狀態(tài) BLOCKED
                // t1 休眠結(jié)束后,獲取 locker2
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 嘗試獲取鎖對(duì)象 locker2
            synchronized (locker2) {
                System.out.println("t1 線程獲取到兩把鎖");
                // t1 獲取到兩把鎖之后結(jié)束執(zhí)行,此時(shí) locker1 和 locker2 均被釋放
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker1) {
            // t2 線程獲取到鎖對(duì)象 locker1
            // 因 t1 先獲取到 locker1 ,t2 此時(shí)處于阻塞狀態(tài) BLOCKED
            // 當(dāng) t1 結(jié)束執(zhí)行后,t2 恢復(fù)執(zhí)行并獲取 locker1
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 休眠結(jié)束后繼續(xù)獲取 locker2
                throw new RuntimeException(e);
            }
            // 嘗試獲取鎖對(duì)象 locker2
            synchronized (locker2) {
                System.out.println("t2 線程獲取到兩把鎖");
                // t2 獲取到兩把鎖后結(jié)束執(zhí)行
            }
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
}

以上就是Java多線程開發(fā)之并發(fā)安全詳解的詳細(xì)內(nèi)容,更多關(guān)于Java多線程之并發(fā)安全的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java實(shí)現(xiàn)批量下載(打包成zip)的實(shí)現(xiàn)

    Java實(shí)現(xiàn)批量下載(打包成zip)的實(shí)現(xiàn)

    這篇文章主要介紹了Java實(shí)現(xiàn)批量下載(打包成zip)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11
  • java音樂播放器實(shí)現(xiàn)代碼

    java音樂播放器實(shí)現(xiàn)代碼

    這篇文章主要介紹了java音樂播放器的實(shí)現(xiàn)代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • Java根據(jù)前端傳回的圖片生成pdf并且加密碼和水印

    Java根據(jù)前端傳回的圖片生成pdf并且加密碼和水印

    這篇文章主要為大家詳細(xì)介紹了java如何根據(jù)前端傳回的png圖片數(shù)組,后端加水印加密碼生成pdf并返回給前端,感興趣的小伙伴可以參考一下
    2025-01-01
  • java模擬TCP通信實(shí)現(xiàn)客戶端上傳文件到服務(wù)器端

    java模擬TCP通信實(shí)現(xiàn)客戶端上傳文件到服務(wù)器端

    這篇文章主要為大家詳細(xì)介紹了java模擬TCP通信實(shí)現(xiàn)客戶端上傳文件到服務(wù)器端,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-10-10
  • Java中的BigDecimal精度運(yùn)算詳解

    Java中的BigDecimal精度運(yùn)算詳解

    這篇文章主要介紹了Java中的BigDecimal精度運(yùn)算詳解,Java在java.math包中提供的API類BigDecimal,用來對(duì)超過16位有效位的數(shù)進(jìn)行精確的運(yùn)算,雙精度浮點(diǎn)型變量double可以處理16位有效數(shù),但在實(shí)際應(yīng)用中,可能需要對(duì)更大或者更小的數(shù)進(jìn)行運(yùn)算和處理,需要的朋友可以參考下
    2023-10-10
  • java實(shí)現(xiàn)簡(jiǎn)單貪吃蛇小游戲

    java實(shí)現(xiàn)簡(jiǎn)單貪吃蛇小游戲

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單貪吃蛇小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-05-05
  • Java多線程Semaphore工具的使用詳解

    Java多線程Semaphore工具的使用詳解

    Semaphore 是一種用于控制線程并發(fā)訪問數(shù)的同步工具。它通過維護(hù)一定數(shù)量的許可證來限制對(duì)共享資源的訪問,許可證的數(shù)量就是可以同時(shí)訪問共享資源的線程數(shù)目,需要的朋友可以參考下
    2023-05-05
  • 分析jackjson的安全漏洞CVE-2019-14379

    分析jackjson的安全漏洞CVE-2019-14379

    這篇文章主要介紹了jackjson的使用及CVE-2019-14379漏洞分析,ackson知識(shí)點(diǎn)序列化和反序列化,setName和getName調(diào)用順序,通過實(shí)例代碼講解的很詳細(xì),需要的朋友可以參考下
    2021-06-06
  • Spring Boot詳解整合JWT教程

    Spring Boot詳解整合JWT教程

    JWT是目前比較流行的跨域認(rèn)證解決方案,本文主要介紹了SpringBoot整合JWT的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-07-07
  • Java面向?qū)ο笾^承、構(gòu)造方法、重寫、重載

    Java面向?qū)ο笾^承、構(gòu)造方法、重寫、重載

    本章具體介紹了什么是構(gòu)造方法、繼承、重寫、重載以及創(chuàng)建方法,整篇文章用老司機(jī)和人類來舉例,圖解穿插代碼案例,需要的朋友可以參考下
    2023-03-03

最新評(píng)論

六枝特区| 古田县| 榆中县| 子长县| 南丰县| 高安市| 遂平县| 河北区| 水城县| 宣武区| 胶南市| 永泰县| 高安市| 永修县| 贞丰县| 泽库县| 昭通市| 武川县| 梁河县| 莒南县| 沙洋县| 古交市| 剑川县| 凤冈县| 石阡县| 绥芬河市| 巴林右旗| 沐川县| 大洼县| 巴林右旗| 竹溪县| 玉屏| 清丰县| 五莲县| 壤塘县| 通辽市| 文登市| 东海县| 肃北| 扎兰屯市| 牡丹江市|