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

Java 線程安全與 volatile與單例模式問題及解決方案

 更新時間:2025年06月30日 11:55:41   投稿:mrr  
文章主要講解線程安全問題的五個成因(調(diào)度隨機、變量修改、非原子操作、內(nèi)存可見性、指令重排序)及解決方案,強調(diào)使用volatile關(guān)鍵字可同時解決內(nèi)存可見性和指令重排問題,并結(jié)合單例模式中的餓漢與懶漢實現(xiàn)方式分析線程安全的處理,感興趣的朋友一起看看吧

什么是線程安全

在進行多線程編程的時候,當我們編寫出來的多線程的代碼運行結(jié)果不符合我們的預(yù)期的時候,這時候就是 bug,這種 bug 是由于多線程的問題而產(chǎn)生出來的 bug 我們稱之為 線程安全問題

當我們編寫出來的多線程代碼運行之后的結(jié)果符合我們的預(yù)期結(jié)果的時候,說明代碼沒有問題,這時候就是 線程安全

線程安全問題的產(chǎn)生與解決方案

線程安全問題的產(chǎn)生主要有 五個原因

線程的調(diào)度是隨機的

這個原因是由操作系統(tǒng)產(chǎn)生的,CPU 是多核心的,在進行線程的調(diào)度的時候并不是等到線程徹底執(zhí)行完才輪到下一個線程執(zhí)行,CPU 使用的是搶占式執(zhí)行,也就是說,這個線程可能執(zhí)行到一半,就立馬被剝奪了 CPU 資源,開始執(zhí)行下一個線程,然后執(zhí)行完一半,又將上一個線程調(diào)度回來,這是由隨機性的,程序員無法通過代碼應(yīng)用層得知。

這個問題是無法改變的,這也就是為什么會產(chǎn)生線程安全問題的最根本的原因。

多個線程對同一個變量進行修改

在之前的文章中就已經(jīng)設(shè)計過這種情況的討論,如果修改的外部類的成員變量,是會發(fā)生線程安全問題的,如果修改的是局部變量,那就會觸發(fā) “變量捕獲的語法”,這時候是不建議進行修改的。

解決方法也很簡單,就是加鎖,通過 synchronized 進行加鎖。

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

線程的修改操作不是原子性的

這個問題其實和第二個問題是一樣的,為什么修改同一個變量可能會發(fā)生線程安全問題,因為我們的修改指令并不是原子性的,也就說,這個操作并不是 CPU 執(zhí)行一次指令就可以完成 count++ 的,count ++ 實質(zhì)是由三條指令實現(xiàn)的,首先 load count 這個數(shù)值,然后進行 count +1 操作,最后將結(jié)果保存到內(nèi)存里。

為了使修改操作是原子性的,所以我們使用加鎖的方式來實現(xiàn),也就是上面的代碼。

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

這個問題是由于 JVM 優(yōu)化而導(dǎo)致的,

public class Test {
    private static int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 1) {
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("請輸入falg 的數(shù)值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在這里插入圖片描述

即使我們修改了 flag 數(shù)值,但是程序依舊沒有反應(yīng),說明在 t1 線程中讀取到的 flag 依舊還是 1

一個線程涉及到了讀操作,一個線程涉及到了修改操作,這可能會觸發(fā)線程安全問題,也就是內(nèi)存可見性問題,讀操作沒有讀到修改過的數(shù)值。

原因:JVM / 編譯器 其實是帶有優(yōu)化功能的,因為不同的程序員寫出來的代碼不同,運行效率也是不同,為了提高代碼的運行效率,JVM / 編譯器 在不改變我們代碼的邏輯的情況下,會對我們寫的代碼進行優(yōu)化。雖然說對我們代碼邏輯不會做出改變,但是在多線程編程下可能會發(fā)生誤判。

例如上面的代碼,t1 線程進行讀 flag 操作,也就是寄存器會從內(nèi)存中讀取 flag ,但是這是一個 while 循環(huán),在一秒鐘之內(nèi)就會讀取很多次,雖然 t2 線程會對 flag 進行修改,但是 t2 線程在啟動之前 flag 這個數(shù)值就被 t1 線程讀取了 幾千萬次,所以編譯器 / JVM 會認為 flag 是一個不會被修改的數(shù)值,即把這個讀內(nèi)存操作優(yōu)化為 讀寄存器操作,也就是把 flag 這個數(shù)值拷貝一份到寄存器里,這樣 CPU 就直接從寄存器讀 flag 數(shù)值而不用到 內(nèi)存中讀取了。

等到了 t2 線程開始運行的時候,我們進行修改 flag 數(shù)值,內(nèi)存中 flag 即使被修改了,但是 t1 線程還是不知道flag 被修改了,因為此時它是從寄存器讀取 flag 數(shù)值。

拓展一下,如果我們在 t1 線程 加上 sleep 的話,這個內(nèi)存可見性問題就消失了。

    private static int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 1) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("請輸入falg 的數(shù)值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }

在這里插入圖片描述

即使是 sleep 1 ms 內(nèi)存可見性問題也沒有發(fā)生,這是為什么?

因為讀內(nèi)存操作可能就是幾 ns 的事情,優(yōu)化為 讀寄存器操作可以再快個幾 ns,但是代碼存在 sleep 1 ms ,這個 1ms 的存在,編譯器/ JVM 即使優(yōu)化這個讀操作也不能讓代碼的效率有一個質(zhì)的飛躍,所以干脆就不提升了。所以內(nèi)存可見性問題也就不存在了。

JVM / 編譯器的優(yōu)化是一個很復(fù)雜的事情,具體的細節(jié)大家可以參考深入理解Java虛擬機 這本書,在后續(xù)文章中也會提到 JVM 的部分內(nèi)容。

如何解決這個內(nèi)存可見性問題???
使用 volatile 關(guān)鍵字

在這里插入圖片描述

這個關(guān)鍵字的英文翻譯的易變的,說明這個變量我是會進行修改的,你不能進行讀操作的優(yōu)化。

注意這個關(guān)鍵字只能修飾變量,不能修飾方法!??!

修改后的代碼:

import java.util.Scanner;
public class Test {
    private static volatile int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 1) {
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("請輸入falg 的數(shù)值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在這里插入圖片描述

指令重排序問題

這個問題在下面的單例模式中的懶漢模式會提到~~

單例模式

單例模式是一種設(shè)計模式,也就是一個規(guī)范。

單例模式,顧名思義就是只允許一個對象的創(chuàng)建,也就是一個類只能創(chuàng)建實例化一個對象,不能進行多次實例化。這種設(shè)計模式的應(yīng)用場景還是很多的,例如:我們在進行服務(wù)器開發(fā)的時候,我們需要一個對象來存放數(shù)據(jù),這時候我們就會先寫出類,然后再去創(chuàng)建對象,但是如果這個對象包含的數(shù)據(jù)很大,假如有100G,那么創(chuàng)建多次之后,也就是有幾百G 的數(shù)據(jù)需要放在服務(wù)器上,并且這么多重復(fù)的數(shù)據(jù)也就只有一份是有用的,不僅僅是浪費了服務(wù)器的內(nèi)存資源,還可能會導(dǎo)致服務(wù)器的崩潰,在這種情況下,我們通常使用單例模式來進行約束,只允許一個對象的創(chuàng)建。

餓漢模式

餓漢模式 是程序已啟動,隨著類的加載,對象也隨之創(chuàng)建出來了,所以稱之為 餓漢模式,說明創(chuàng)建的很快。

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public Singleton getInstance() {
        return instance;
    }
}

從上面的代碼,我們就可以看到是要類一加載,對象instance 也就創(chuàng)建出來了private static Singleton instance = new Singleton();

為什么說我們不能進行多次創(chuàng)建呢?
因為這個類的構(gòu)造方法被我們用private 修飾了,在外面是不能進行實例化的,這也是單例模式的點睛之筆。

我們來討論一下,這個餓漢模式 的代碼會不會出現(xiàn)線程安全問題?
答案是不會的,線程只是從getInstance() 進行讀操作,獲取 instance 這個對象,并沒有涉及到修改操作,自然沒有線程安全問題的存在。

懶漢模式

懶漢模式 顧名思義就是 懶,等我們真正需要這個對象的時候,才會進行實例化對象的操作。我們來看一下代碼:

class SingletonLazy {
    private static SingletonLazy instance;
    public SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy() {}
}

當我們真正需要用到這個對象的時候,才進行實例化,這就是懶漢模式。

但是在多線程編程下,是可能會出現(xiàn)線程安全問題,由于代碼涉及到寫操作,也就是 實例化對象的操作,假設(shè)有兩個線程同時進行對象的實例化,就會發(fā)生線程安全問題,所以要加上鎖 synchronized .

class SingletonLazy {
    private static SingletonLazy instance;
    public SingletonLazy getInstance() {
        synchronized (this) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}

但是每次進行判斷的時候都需要進行加鎖,這就導(dǎo)致效率低下,所以我們在外面再加一層 if 判斷,減少加鎖的次數(shù)。

class SingletonLazy {
    private static SingletonLazy instance;
    public SingletonLazy getInstance() {
        if(instance == null) {
            synchronized (this) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}

即使代碼被我們修改成這樣,還是會存在一個問題,指令重排序的問題

我們在實例化一個對象有三條指令需要做:第一申請內(nèi)存空間,第二初始化對象,第三將內(nèi)存空間的首地址賦值給引用。

在編譯器/JVM 下可能會進行優(yōu)化,將上面的三條指令優(yōu)化為先執(zhí)行1,再執(zhí)行3 ,最后執(zhí)行 2.

這可能會導(dǎo)致一個線程還沒初始化對象,另一個線程就直接拿到這個對象進行使用了,但是這些使用操作,在后面的初始化完之后又被覆蓋掉了。這就是第五個引起線程安全問題的原因 —— 指令重排序。

在這里插入圖片描述

如何解決這個問題???
使用 volatile 關(guān)鍵字

沒錯 volatile 關(guān)鍵字不僅僅能解決內(nèi)存可見性問題,還能解決指令重排序問題。

private static volatile SingletonLazy instance;

懶漢模式最終代碼

class SingletonLazy {
    private static volatile SingletonLazy instance;
    public SingletonLazy getInstance() {
        if(instance == null) {
            synchronized (this) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}

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

相關(guān)文章

  • Springboot結(jié)合Mybatis-Plus實現(xiàn)業(yè)務(wù)撤銷回滾功能

    Springboot結(jié)合Mybatis-Plus實現(xiàn)業(yè)務(wù)撤銷回滾功能

    本文介紹了如何在Springboot結(jié)合Mybatis-Plus實現(xiàn)業(yè)務(wù)撤銷回滾功能,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧
    2024-12-12
  • MyBatis結(jié)果映射(ResultMap)的使用

    MyBatis結(jié)果映射(ResultMap)的使用

    在MyBatis中,結(jié)果映射是實現(xiàn)數(shù)據(jù)庫結(jié)果集到Java對象映射的核心,它不僅支持簡單的字段映射,還能處理字段名不一致、嵌套對象和集合映射等復(fù)雜場景,通過ResultMap,開發(fā)者可以靈活定義映射關(guān)系,以適應(yīng)各種需求,感興趣的可以了解一下
    2024-09-09
  • 一文看懂 Spring Aware 接口功能

    一文看懂 Spring Aware 接口功能

    Aware接口是一個空接口,可以理解為是一個標記接口,方便在一個統(tǒng)一的方法(AbstractAutowireCapableBeanFactory.invokeAwareMethods)中進行判斷處理賦值,在子接口寫出各自的set方法,這篇文章主要介紹了一文看懂 Spring Aware 接口功能,需要的朋友可以參考下
    2024-12-12
  • Java 單鏈表數(shù)據(jù)結(jié)構(gòu)的增刪改查教程

    Java 單鏈表數(shù)據(jù)結(jié)構(gòu)的增刪改查教程

    這篇文章主要介紹了Java 單鏈表數(shù)據(jù)結(jié)構(gòu)的增刪改查教程,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-10-10
  • java性能優(yōu)化四種常見垃圾收集器匯總

    java性能優(yōu)化四種常見垃圾收集器匯總

    這篇文章主要介紹了java性能優(yōu)化四種常見垃圾收集器匯總,每種垃圾收集器都有其不同的算法實現(xiàn)和步驟,下面我們簡單描述下我們常見的四種垃圾收集器的算法過程,感興趣的同學(xué)們最好先看下以下的兩篇文章去增加理解
    2022-07-07
  • java如何讀取Excel簡單模板

    java如何讀取Excel簡單模板

    這篇文章主要為大家詳細介紹了java如何讀取Excel簡單模板,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-10-10
  • Java通過What、Why、How了解弱引用

    Java通過What、Why、How了解弱引用

    這篇文章主要介紹了Java通過What、Why、How了解弱引用,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友可以參考下
    2020-03-03
  • SpringBoot 如何整合 ES 實現(xiàn) CRUD 操作

    SpringBoot 如何整合 ES 實現(xiàn) CRUD 操作

    這篇文章主要介紹了SpringBoot 如何整合 ES 實現(xiàn) CRUD 操作,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下
    2020-10-10
  • Mybatis多個字段模糊匹配同一個值的案例

    Mybatis多個字段模糊匹配同一個值的案例

    這篇文章主要介紹了Mybatis多個字段模糊匹配同一個值的案例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-09-09
  • Java @Transactional與synchronized使用的問題

    Java @Transactional與synchronized使用的問題

    這篇文章主要介紹了Java @Transactional與synchronized使用的問題,了解內(nèi)部原理是為了幫助我們做擴展,同時也是驗證了一個人的學(xué)習能力,如果你想讓自己的職業(yè)道路更上一層樓,這些底層的東西你是必須要會的
    2023-01-01

最新評論