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

Java SE多線程之線程安全、synchronized、volatile與wait/notify全解

 更新時間:2026年05月13日 09:35:31   作者:桃也在Coding  
文章詳細解釋了Java中線程不安全的概念、原因及解決方案,并介紹了`synchronized`、`volatile`的使用方法和特性,以及`wait`/`notify`機制在多線程協(xié)作中的作用

一、線程不安全

1.1 線程不安全的直觀現(xiàn)象

多線程的優(yōu)勢是提升效率,但多個線程同時操作共享數(shù)據(jù)時,極易出現(xiàn)數(shù)據(jù)錯亂的問題,這就是線程不安全。

計數(shù)器案例:

private static int count = 0;
public static void main(String[] args) throws InterruptedException {
    // 線程1:自增5萬次
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) count++;
    });
    // 線程2:自增5萬次
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) count++;
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    // 預(yù)期10萬,實際永遠小于10萬
    System.out.println("count: " + count);
}

運行結(jié)果永遠小于預(yù)期的10萬,這就是典型的線程不安全問題。

1.2 線程不安全的原因

操作系統(tǒng)對線程的調(diào)度是隨機的,這也是線程不安全的罪魁禍?zhǔn)住?/p>

(1)原子性缺失:操作被拆分打斷

原子性指一段操作不可分割,要么全部執(zhí)行,要么全部不執(zhí)行。

count++看似一行代碼,實則對應(yīng)3個CPU的指令:

  1. load:把內(nèi)存中的值加載到CPU寄存器中
  2. add:把寄存器的內(nèi)容+1
  3. save:把寄存器中的內(nèi)容保存回內(nèi)存中

執(zhí)行這三個指令時不一定能一次執(zhí)行完,很有可能1和2執(zhí)行完,調(diào)度走;過了很久,再調(diào)度回來執(zhí)行3。

如果兩個線程對于同一個count進行操作,極大概率t1還沒來得及保存新結(jié)果(1),t2就已經(jīng)加載并修改,保存了數(shù)據(jù)(0 -> 1),此時再調(diào)度t1,t1接下來該保存新數(shù)據(jù)(1),t1的修改覆蓋了t2的修改,對于這兩次自增,count的結(jié)果是1。這樣多次覆蓋,就會導(dǎo)致count最終的結(jié)果小于100000

(2)可見性缺失:數(shù)據(jù)更新互相看不見

Java內(nèi)存模型(JMM)規(guī)定線程有獨立工作內(nèi)存,共享數(shù)據(jù)存主內(nèi)存。

  • 線程修改共享變量時,先改工作內(nèi)存副本,再同步到主內(nèi)存。
  • 線程A修改了變量,但未及時同步到主內(nèi)存,線程B讀取的還是舊值,導(dǎo)致邏輯錯誤。

(3)有序性缺失:指令被亂序優(yōu)化

為提升效率,編譯器和CPU會對指令重排序(不影響單線程結(jié)果),但多線程下會打亂邏輯:

  • 典型場景:雙重檢查鎖單例模式中,instance = new Singleton()可能被重排序,導(dǎo)致線程獲取未初始化的對象,引發(fā)空指針。

二、synchronized

synchronized是Java內(nèi)置的互斥鎖,能同時保證原子性、可見性、有序性,是解決線程安全最常用的關(guān)鍵字。

  • 進入 synchronized 修飾的代碼塊, 相當(dāng)于加鎖
  • 退出 synchronized 修飾的代碼塊, 相當(dāng)于解鎖

加鎖操作不是把線程鎖死在CPU上,不讓這個線程被調(diào)度走,而是禁止其他線程重新加這個鎖,避免其他線程的操作,在當(dāng)前線程的執(zhí)行過程中插隊。

2.1 三大特性

(1)互斥性(原子性)

synchronized用的鎖是存在Java對象里的,可以粗略的理解為每個對象在內(nèi)存中存儲時,都有一塊內(nèi)存表示當(dāng)前鎖定的狀態(tài)(類似于廁所的有人/無人)

  • 如果是無人狀態(tài),就可以使用,使用時設(shè)置為“有人”狀態(tài)
  • 如果是有人狀態(tài),其他人無法使用,只能等待。

理解阻塞等待
針對每一把鎖,操作系統(tǒng)內(nèi)部都維護了一個等待隊列,當(dāng)這個鎖被某個線程占有時,其他線程嘗試進行加鎖,就加不上了,就會阻塞等待,一直到之前的線程解鎖后,由操作系統(tǒng)喚醒一個新線程,再來獲取這個鎖。

  • 上個一個線程解鎖后,下一個線程不是立即就能獲取,而是靠操作系統(tǒng)來“喚醒”,這也是操作系統(tǒng)線程調(diào)度的一部分工作
  • 假設(shè)A B C三個線程,線程A先獲得鎖,然后B嘗試獲取,C再嘗試獲取。此時B和C都在阻塞隊列中排隊等待,但是當(dāng)A釋放鎖之后,雖然B比C先來,B不一定立即獲得鎖,而是重新與C競爭。
  • 同一時刻,只有一個線程能獲取同一把鎖,執(zhí)行臨界區(qū)代碼。
  • 其他線程嘗試獲取鎖時,會進入阻塞等待狀態(tài),直到鎖被釋放。

(2)可見性

  • 線程進入synchronized代碼塊時,清空工作內(nèi)存,從主內(nèi)存加載最新數(shù)據(jù)。
  • 線程退出代碼塊時,強制將修改后的數(shù)據(jù)刷新回主內(nèi)存,其他線程能立即看到最新值。

(3)可重入性

理解“把自己鎖死”
一個線程沒有釋放鎖,又嘗試重新加鎖
例如第一次加鎖,成功上鎖;第二次加同一把鎖,鎖已經(jīng)被占用,就會阻塞等待。
按照之前的鎖的設(shè)定,第二次加鎖,阻塞等待,直到第一次的鎖釋放;而釋放第一個鎖也是由該線程完成,這樣就陷入了死循環(huán),把自己鎖死了。
這樣的鎖稱為“不可重入鎖”

Java的synchronized引入了可重入的概念,同一線程可重復(fù)獲取同一把鎖,不會自己鎖死自己。

底層通過線程持有者+計數(shù)器實現(xiàn):加鎖時計數(shù)器+1,解鎖時計數(shù)器-1,計數(shù)器為0時真正釋放鎖。

2.2 三種使用方式

(1)修飾代碼塊(鎖自定義對象)

// 鎖任意對象
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            synchronized (lock) { // 加鎖
                count++;
            } // 解鎖
        }
    });
}

對于鎖來說任意對象都可以,一般情況下,我們會專門定義一個Object類給鎖使用。

(2)修飾實例方法(鎖當(dāng)前對象)

// 鎖當(dāng)前實例對象
public synchronized void increment() {
    count++;
}

(3)修飾靜態(tài)方法(鎖類對象,全局唯一)

// 鎖當(dāng)前類的Class對象,所有實例共享一把鎖
public synchronized static void increment() {
    count++;
}

2.3 修復(fù)計數(shù)器案例

count++synchronized鎖,保證原子性:

private static int count = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            synchronized (lock) { count++; }
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            synchronized (lock) { count++; }
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("count: " + count); // 輸出10萬
}

2.4 死鎖

死鎖是指兩個或多個線程,各自拿著對方需要的鎖,又互相等待對方釋放鎖,誰都不放手、誰都走不了,程序永久阻塞卡死。

構(gòu)成死鎖的必要條件

  1. 鎖是互斥的。一個線程拿到鎖后,另一個線程想要拿鎖必須阻塞等待
  2. 鎖不可剝奪。線程1拿到鎖,線程2也想獲取這個鎖,必須阻塞等待,而不能直接搶占
  3. 請求和保持。一個線程拿到鎖A,不釋放鎖1的前提下,獲取鎖B
  4. 循環(huán)等待。 多個線程的等待過程構(gòu)成了循環(huán),例如A等B釋放,B也在等A釋放

如何避免死鎖

前兩個條件是鎖的基本特性,想要避免死鎖的出現(xiàn)就要破壞掉3或4.

  1. 統(tǒng)一鎖的獲取順序
    所有線程都按固定順序拿鎖,例如約定從序號小的鎖開始獲取,如果鎖已經(jīng)被獲取,就要阻塞等待
  2. 放棄請求與保持
    要么一次性把需要的鎖全部拿到,一把都不拿不到就不執(zhí)行。
  3. 設(shè)置超時
    嘗試拿鎖等待一段時間,拿不到就放棄,不永久阻塞。
  4. 減少嵌套加鎖

2.5 Java標(biāo)準(zhǔn)庫中的線程安全類

Java標(biāo)準(zhǔn)庫中很多是線程不安全的,這些類可能會涉及多線程修改共享數(shù)據(jù),又沒有加鎖措施。

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

線程安全類:

StringBuffer是線程安全的,正是因為其中的方法都有synchronized。

但是由于synchronized的限制,代碼中可能出現(xiàn)鎖的競爭導(dǎo)致阻塞,會使代碼的效率大打折扣

String:雖然沒有加鎖,但是不涉及修改,仍然是線程安全的

三、volatile

3.1 內(nèi)存可見性問題

以下面代碼為例:t2改變flag的值來影響t1的執(zhí)行,當(dāng)輸入1時,理論上t1內(nèi)while條件不成立,應(yīng)該跳出循環(huán),線程結(jié)束。然而實際上輸入1后t1線程還在繼續(xù)

public class demo18 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag==0){
            }
            System.out.println("t1 線程結(jié)束");
        });
        Thread t2 = new Thread(()->{
            //修改flag
            Scanner scan = new Scanner(System.in);
            System.out.println("輸入flag的值:");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }
}

很明顯,這也是由于線程安全導(dǎo)致的bug。一個線程在讀取,一個線程在修改,修改的值沒有被另一個線程讀取到,這就是“內(nèi)存可見性問題”。

這涉及到編譯器優(yōu)化

我們寫的代碼,都會通過javac.java文件編譯成.class字節(jié)碼文件,由jvm執(zhí)行。編譯器可以保持代碼邏輯不變的情況下,對代碼進行優(yōu)化,提升效率。

而在多線程場景,編譯器很有可能判斷錯誤,導(dǎo)致優(yōu)化前后的邏輯不完全相同。

分析編譯器如何誤判

t1中是空循環(huán),對于CPU主要就是兩個操作:load (加載flag的值),cmp(條件跳轉(zhuǎn))

  • 對于cmp:cpu的寄存器操作,速度快很多
  • 對于load:需要到內(nèi)存中訪問,時間可能是cmp的幾千倍。

每輪循環(huán)執(zhí)行速度非???,短時間內(nèi)就可以執(zhí)行很多次,每次讀取flag的值都是不變的。經(jīng)過多次循環(huán),JVM認為這個讀取操作可以被優(yōu)化(正是因為load在循環(huán)中時間消耗是cmp的幾千倍),因此把讀內(nèi)存操作改成了讀寄存器操作。

而用戶輸入值可能要經(jīng)過好幾秒,與上述的操作時間完全不是一個量級。等到用戶真的輸入flag的值,t1已經(jīng)感知不到了(編譯器優(yōu)化使得t1的讀操作不是真正的讀內(nèi)存)

如果在循環(huán)中加入一些語句

while(flag==0){
	sleep(1);
   }

加入sleep后,使循環(huán)的速度大大大大幅度下降,此時load時間占比對于整個循環(huán)小了很多,JVM認為這個優(yōu)化沒有必要,每次都是讀內(nèi)存操作。因此t2的修改可以被t1感知到,結(jié)果正確。

3.2 volatile的作用

volatile輕量級并發(fā)關(guān)鍵字不保證原子性,僅保證可見性和禁止指令重排序,適合解決“一個線程寫、多個線程讀”的場景。

(1)保證可見性:數(shù)據(jù)更新立即同步

  • volatile變量:修改后立即刷新到主內(nèi)存。
  • volatile變量:強制從主內(nèi)存讀取最新值,不讀工作內(nèi)存緩存。

解決線程感知不到變量更新的問題:

public class demo18 {
    private volatile static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag==0){
            }
            System.out.println("t1 線程結(jié)束");
        });
        Thread t2 = new Thread(()->{
            //修改flag
            Scanner scan = new Scanner(System.in);
            System.out.println("輸入flag的值:");
            flag = scan.nextInt();//輸入后t1就可以感知到,跳出循環(huán),線程結(jié)束 
        });
        t1.start();
        t2.start();
    }
}

(2)禁止指令重排序:避免邏輯錯亂

  • volatile變量前后會加內(nèi)存屏障,禁止編譯器和CPU對其前后指令重排序。
  • 應(yīng)用:雙重檢查鎖(DCL)單例模式,防止指令重排序?qū)е碌目罩羔槨?/li>

volatile適合無復(fù)合操作(如count++)、僅需可見性/有序性的場景;復(fù)合操作必須用synchronizedAtomicInteger。

四、wait 等待 / notify 通知

多線程不僅要“互斥”,還要“協(xié)作”——比如生產(chǎn)者生產(chǎn)完數(shù)據(jù),通知消費者消費;消費者無數(shù)據(jù)時等待。wait()、notify()notifyAll()是實現(xiàn)線程等待-喚醒的核心方法,定義在Object類中。

4.1 方法詳解

(1)wait():讓線程等待并釋放鎖

  • 作用:當(dāng)前線程進入阻塞等待狀態(tài),釋放持有的鎖,允許其他線程獲取鎖執(zhí)行任務(wù)。
  • 重載:wait()(無限等待)、wait(long timeout)(超時等待,毫秒)。
  • 必須在synchronized代碼塊/方法中調(diào)用,否則拋IllegalMonitorStateException。

(2)notify():隨機喚醒一個等待線程

  • 作用:隨機喚醒一個在當(dāng)前對象鎖上等待的線程
  • 喚醒后不立即釋放鎖,需當(dāng)前線程退出synchronized代碼塊后,被喚醒線程才能競爭鎖。

(3)notifyAll():喚醒所有等待線程

  • 作用:喚醒所有在當(dāng)前對象鎖上等待的線程。
  • 所有線程被喚醒后競爭同一把鎖,同一時刻只有一個線程能執(zhí)行。

注意:

  1. 調(diào)用wait(),notify()以及這兩個方法所在的synchronized代碼塊內(nèi)必須是同一對象才能生效
  2. 要確保先wait 再notify,才會有作用。如果先notify再wait,不會對notify所在的線程有影響,但是沒啥用

4.2 wait()與sleep()的區(qū)別

特性wait()sleep()
所屬類Object類Thread類
鎖行為釋放鎖不釋放鎖
喚醒方式需notify()/notifyAll()喚醒超時自動喚醒
使用場景線程協(xié)作(等待-喚醒)線程休眠(暫停執(zhí)行)

如果sleep()在synchronized代碼塊內(nèi),就會出現(xiàn)“抱著鎖睡”的情況,休眠期間其他線程也不能拿到這把鎖。

到此這篇關(guān)于Java SE多線程之線程安全、synchronized、volatile與wait/notify全解的文章就介紹到這了,更多相關(guān)Java SE 多線程volatile與wait/notify內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java-流的使用完結(jié)與異常處理機制(詳解)

    java-流的使用完結(jié)與異常處理機制(詳解)

    下面小編就為大家?guī)硪黄猨ava-流的使用完結(jié)與異常處理機制(詳解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-09-09
  • Spring Boot命令行運行器的實現(xiàn)方法

    Spring Boot命令行運行器的實現(xiàn)方法

    這篇文章主要介紹了Spring Boot命令行運行器的實現(xiàn)方法,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-10-10
  • java如何解析/讀取xml文件

    java如何解析/讀取xml文件

    這篇文章主要為大家詳細介紹了java如何解析/讀取xml文件的方法,感興趣的小伙伴們可以參考一下
    2016-03-03
  • Java 中的新 HTTP 客戶端深入解析

    Java 中的新 HTTP 客戶端深入解析

    Java11引入了全新的標(biāo)準(zhǔn)化HTTP客戶端API,支持HTTP/2、異步請求和WebSocket,旨在替代舊的HttpURLConnection,提供更高效、更現(xiàn)代的網(wǎng)絡(luò)編程體驗,本文給大家介紹Java 中的新 HTTP 客戶端,感興趣的朋友跟隨小編一起看看吧
    2025-11-11
  • 解決lombok的@Data注解無法打印繼承的父類信息問題

    解決lombok的@Data注解無法打印繼承的父類信息問題

    在Java編程中,使用@Data注解可能導(dǎo)致子類繼承父類屬性后,打印只顯示子類信息不顯示父類信息,問題源于@Data注解作用域僅限于當(dāng)前類,解決方法包括使用@ToString(callSuper=true)注解或重寫toString方法
    2024-11-11
  • Java設(shè)計模式之中介者模式的實現(xiàn)方式

    Java設(shè)計模式之中介者模式的實現(xiàn)方式

    Java中介者模式是一種行為型設(shè)計模式,它通過一個中介者對象來協(xié)調(diào)多個對象之間的交互,降低對象之間的耦合度,提高系統(tǒng)的可維護性和可擴展性。本文將介紹該設(shè)計模式的原理、使用場景和實現(xiàn)方法
    2023-04-04
  • PageHelper在springboot+mybatis框架中的使用步驟及原理解析

    PageHelper在springboot+mybatis框架中的使用步驟及原理解析

    這篇文章主要介紹了PageHelper在springboot+mybatis框架中的使用步驟及原理解析,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-03-03
  • springBoot接入阿里云oss的實現(xiàn)步驟

    springBoot接入阿里云oss的實現(xiàn)步驟

    這篇文章主要介紹了springBoot接入阿里云oss的實現(xiàn)步驟,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-01-01
  • SpringBoot3.3.X整合Mybatis-Plus的實現(xiàn)示例

    SpringBoot3.3.X整合Mybatis-Plus的實現(xiàn)示例

    本文介紹了在Spring Boot 3.3.2中整合MyBatis-Plus 3.5.7,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2025-03-03
  • JAVA并發(fā)圖解

    JAVA并發(fā)圖解

    這篇文章主要介紹了JAVA的并發(fā),文中圖解非常細致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2021-09-09

最新評論

青海省| 红桥区| 四会市| 山阳县| 黄陵县| 册亨县| 三河市| 舟山市| 申扎县| 巩义市| 宁夏| 永仁县| 康乐县| 梅州市| 芒康县| 和硕县| 南漳县| 田阳县| 红桥区| 曲松县| 雅江县| 林口县| 平乡县| 鹤岗市| 庄河市| 手游| 平昌县| 莱州市| 专栏| 尉氏县| 搜索| 乐安县| 和田市| 嘉义县| 西畴县| 隆回县| 江西省| 晴隆县| 彭水| 永登县| 三都|