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

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

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

一、體會線程安全問題

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

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

讀者可以自行實現(xià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,實際輸出的結(jié)果是0
        // 原因是主線程 main 運行太快了,當(dāng) t1 和 t2 線程還在計算時,主線程已經(jīng)打印結(jié)果、運行完畢了
        System.out.println(count);
    }
    // 讓主線程等待 t1 和 t2 線程,等到它們兩個都執(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 對象調(diào)用 join 方法
        // 表示讓主線程 main 等待 t1 線程和 t2 線程
        t1.join();
        t2.join();
        // 當(dāng)兩個線程都執(zhí)行完畢后主線程再繼續(xù)執(zhí)行打印操作
        System.out.println(count);
        // 實際輸出的結(jié)果小于100000,仍不符合預(yù)期
    }
}

二、線程安全的概念

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

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

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

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

三、線程安全問題的原因

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

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

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

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

  • 原子性:指 Java 語句,一條 Java 語句可能對應(yīng)不止一條指令,若對應(yīng)一條指令,就是原子的。
  • 可見性:一個線程對主內(nèi)存(共享變量)的修改,可以及時被其他線程看到。
  • 有序性:一個線程觀察其他線程中指令的執(zhí)行順序,由于 JVM 對指令進行了重排序,觀察到的順序一般比較雜亂。因其原理與 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 ++” 這條語句對應(yīng)多條指令:讀取數(shù)據(jù)、計算結(jié)果、存儲數(shù)據(jù)。

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

初始情況:

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

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

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

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

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

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

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

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

此時 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,此時 t2 讀取 count 并更新為 2,然后寫回內(nèi)存中。當(dāng) t2 執(zhí)行完成,內(nèi)存中的 count 就更新成 2 了:

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

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

現(xià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);
}

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

而上述的操作,其實就是 Java 中的 加鎖操作 。

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

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

注意:

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

4.1 synchronized關(guān)鍵字

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

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

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

        // 一些需要保護的邏輯

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

當(dāng)多個線程針對同一個鎖對象競爭的時候,加鎖操作才有意義。

對之前的例子進行加鎖操作:

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)鍵字用來修飾 普通方法 時,相當(dāng)于給 this 加鎖;

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

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

// 寫法二:
// 將 count++ 所包含的三個操作封裝成一個 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)鍵字

我們再來一個例子:

讓 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 時一直循環(huán)
            }
            System.out.println("t1-結(jié)束");
        });
        Thread t2 = new Thread(() -> {
            // 對 flag 進行修改
            Scanner in = new Scanner(System.in);
            System.out.println("請輸入 flag 的值:");
            flag = in.nextInt();
        });
        t1.start();
        t2.start();
        // 運行后發(fā)現(xiàn)即使輸入1,t1 線程并不會結(jié)束
    }
}

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

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

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

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

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

在我們這個程序中:

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

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

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

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

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

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

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

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

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

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

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

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

// 使用 volatile 關(guān)鍵字來修飾被讀取的變量,此時無論讀取速度怎樣,JVM 都不會對該變量進行優(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 時一直循環(huán)
        }
        System.out.println("t1-結(jié)束");
    });
    Thread t2 = new Thread(() -> {
        // 對 flag 進行修改
        Scanner in = new Scanner(System.in);
        System.out.println("請輸入 flag 的值:");
        flag = in.nextInt();
    });
    t1.start();
    t2.start();
}

注意:

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

五、死鎖

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

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

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

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

不妨試試在你的 IDE 上運行一下~

5.1 synchronized的可重入性

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

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

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

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

5.2 死鎖的概念

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

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

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

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

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

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

5.3 如何避免死鎖

要想避免死鎖,我們就要從死鎖的四個條件入手,其中,互斥條件和不可剝奪條件基本上是無法打破的,因為這兩個是 synchronized 鎖的基本特性,因此我們選擇從后兩個條件入手。

1. 持有并等待條件:

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

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

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

這種情況一般都是雙方都持有對方想要的鎖但是都不肯釋放,所以我們就調(diào)整一下順序:讓 t1 和 t2 獲取鎖的順序都是 locker1 和 locker2,當(dāng) t1 已占有 locker1 時 t2 想獲取只能阻塞等待,但這時 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 線程獲取到鎖對象 locker1
            try {
                Thread.sleep(1000);
                // 此時 t2 因 locker1 被 t1 獲取而處于阻塞狀態(tài) BLOCKED
                // t1 休眠結(jié)束后,獲取 locker2
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 嘗試獲取鎖對象 locker2
            synchronized (locker2) {
                System.out.println("t1 線程獲取到兩把鎖");
                // t1 獲取到兩把鎖之后結(jié)束執(zhí)行,此時 locker1 和 locker2 均被釋放
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker1) {
            // t2 線程獲取到鎖對象 locker1
            // 因 t1 先獲取到 locker1 ,t2 此時處于阻塞狀態(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);
            }
            // 嘗試獲取鎖對象 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ā)安全的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

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

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

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

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

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

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

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

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

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

    Java中的BigDecimal精度運算詳解

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

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

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

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

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

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

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

    Spring Boot詳解整合JWT教程

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

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

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

最新評論

梓潼县| 辽阳市| 嘉禾县| 健康| 咸丰县| 江油市| 嵩明县| 凭祥市| 巴中市| 内江市| 垦利县| 福海县| 西林县| 龙口市| 宜兴市| 辽阳市| 天峻县| 乐山市| 吉隆县| 石首市| 车致| 永清县| 舟曲县| 湟源县| 文成县| 葫芦岛市| 进贤县| 西和县| 原阳县| 留坝县| 大田县| 临西县| 马龙县| 尚志市| 隆安县| 琼结县| 田林县| 迭部县| 喀什市| 延边| 林甸县|