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

Java 線程安全與 volatile與單例模式問(wèn)題及解決方案

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

什么是線程安全

在進(jìn)行多線程編程的時(shí)候,當(dāng)我們編寫(xiě)出來(lái)的多線程的代碼運(yùn)行結(jié)果不符合我們的預(yù)期的時(shí)候,這時(shí)候就是 bug,這種 bug 是由于多線程的問(wèn)題而產(chǎn)生出來(lái)的 bug 我們稱(chēng)之為 線程安全問(wèn)題

當(dāng)我們編寫(xiě)出來(lái)的多線程代碼運(yùn)行之后的結(jié)果符合我們的預(yù)期結(jié)果的時(shí)候,說(shuō)明代碼沒(méi)有問(wèn)題,這時(shí)候就是 線程安全

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

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

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

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

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

多個(gè)線程對(duì)同一個(gè)變量進(jìn)行修改

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

解決方法也很簡(jiǎn)單,就是加鎖,通過(guò) synchronized 進(jìn)行加鎖。

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);
    }
}

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

這個(gè)問(wèn)題其實(shí)和第二個(gè)問(wèn)題是一樣的,為什么修改同一個(gè)變量可能會(huì)發(fā)生線程安全問(wèn)題,因?yàn)槲覀兊男薷闹噶畈⒉皇窃有缘?,也就說(shuō),這個(gè)操作并不是 CPU 執(zhí)行一次指令就可以完成 count++ 的,count ++ 實(shí)質(zhì)是由三條指令實(shí)現(xiàn)的,首先 load count 這個(gè)數(shù)值,然后進(jìn)行 count +1 操作,最后將結(jié)果保存到內(nèi)存里。

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

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

這個(gè)問(wèn)題是由于 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("請(qǐng)輸入falg 的數(shù)值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在這里插入圖片描述

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

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

原因:JVM / 編譯器 其實(shí)是帶有優(yōu)化功能的,因?yàn)椴煌某绦騿T寫(xiě)出來(lái)的代碼不同,運(yùn)行效率也是不同,為了提高代碼的運(yùn)行效率,JVM / 編譯器 在不改變我們代碼的邏輯的情況下,會(huì)對(duì)我們寫(xiě)的代碼進(jìn)行優(yōu)化。雖然說(shuō)對(duì)我們代碼邏輯不會(huì)做出改變,但是在多線程編程下可能會(huì)發(fā)生誤判。

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

等到了 t2 線程開(kāi)始運(yùn)行的時(shí)候,我們進(jìn)行修改 flag 數(shù)值,內(nèi)存中 flag 即使被修改了,但是 t1 線程還是不知道flag 被修改了,因?yàn)榇藭r(shí)它是從寄存器讀取 flag 數(shù)值。

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

    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("請(qǐng)輸入falg 的數(shù)值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }

在這里插入圖片描述

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

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

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

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

在這里插入圖片描述

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

注意這個(gè)關(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("請(qǐng)輸入falg 的數(shù)值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在這里插入圖片描述

指令重排序問(wèn)題

這個(gè)問(wèn)題在下面的單例模式中的懶漢模式會(huì)提到~~

單例模式

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

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

餓漢模式

餓漢模式 是程序已啟動(dòng),隨著類(lèi)的加載,對(duì)象也隨之創(chuàng)建出來(lái)了,所以稱(chēng)之為 餓漢模式,說(shuō)明創(chuàng)建的很快。

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

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

為什么說(shuō)我們不能進(jìn)行多次創(chuàng)建呢?
因?yàn)檫@個(gè)類(lèi)的構(gòu)造方法被我們用private 修飾了,在外面是不能進(jìn)行實(shí)例化的,這也是單例模式的點(diǎn)睛之筆。

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

懶漢模式

懶漢模式 顧名思義就是 懶,等我們真正需要這個(gè)對(duì)象的時(shí)候,才會(huì)進(jìn)行實(shí)例化對(duì)象的操作。我們來(lái)看一下代碼:

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

當(dāng)我們真正需要用到這個(gè)對(duì)象的時(shí)候,才進(jìn)行實(shí)例化,這就是懶漢模式。

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

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

但是每次進(jìn)行判斷的時(shí)候都需要進(jìn)行加鎖,這就導(dǎo)致效率低下,所以我們?cè)谕饷嬖偌右粚?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() {}
}

即使代碼被我們修改成這樣,還是會(huì)存在一個(gè)問(wèn)題,指令重排序的問(wèn)題。

我們?cè)趯?shí)例化一個(gè)對(duì)象有三條指令需要做:第一申請(qǐng)內(nèi)存空間,第二初始化對(duì)象,第三將內(nèi)存空間的首地址賦值給引用。

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

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

在這里插入圖片描述

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

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

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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 詳細(xì)講解Java中==與equals的區(qū)別對(duì)比

    詳細(xì)講解Java中==與equals的區(qū)別對(duì)比

    這篇文章主要為大家詳細(xì)介紹了Java中==與equals的區(qū)別對(duì)比,文中有詳細(xì)的代碼示例供大家參考,具有一定的參考價(jià)值,感興趣的同學(xué)可以參考閱讀下
    2023-09-09
  • 基于SpringBoot+Pcap4j實(shí)現(xiàn)網(wǎng)絡(luò)流量抓包與實(shí)時(shí)分析

    基于SpringBoot+Pcap4j實(shí)現(xiàn)網(wǎng)絡(luò)流量抓包與實(shí)時(shí)分析

    在現(xiàn)代企業(yè)網(wǎng)絡(luò)環(huán)境中,網(wǎng)絡(luò)故障排查、性能監(jiān)控、安全審計(jì)等需求日益增長(zhǎng),本文將詳細(xì)介紹如何使用?Spring?Boot?+?Pcap4j?構(gòu)建一個(gè)功能完整的網(wǎng)絡(luò)流量抓包與分析系統(tǒng),感興趣的小伙伴可以了解下
    2025-08-08
  • Springboot整合Activiti操作詳解

    Springboot整合Activiti操作詳解

    這篇文章主要給大家詳細(xì)介紹了Springboot整合Activiti的操作流程,文中流程步驟和代碼示例介紹的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下
    2023-07-07
  • RocketMQ NameServer 核心源碼解析

    RocketMQ NameServer 核心源碼解析

    這篇文章主要為大家介紹了RocketMQ NameServer 核心源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • Java開(kāi)發(fā)崗位面試被問(wèn)到泛型怎么辦

    Java開(kāi)發(fā)崗位面試被問(wèn)到泛型怎么辦

    泛型在java中有很重要的地位,在面向?qū)ο缶幊碳案鞣N設(shè)計(jì)模式中有非常廣泛的應(yīng)用。java泛型知識(shí)點(diǎn)也是Java開(kāi)發(fā)崗位必問(wèn)的一個(gè)話題,今天小編就給大家普及下Java泛型常見(jiàn)面試題,感興趣的朋友一起看看吧
    2021-07-07
  • MyBatis流式查詢(xún)的三種實(shí)現(xiàn)方法

    MyBatis流式查詢(xún)的三種實(shí)現(xiàn)方法

    流式查詢(xún)指的是查詢(xún)成功后不是返回一個(gè)集合而是返回一個(gè)迭代器,應(yīng)用每次從迭代器取一條查詢(xún)結(jié)果,本文介紹了MyBatis流式查詢(xún)的實(shí)現(xiàn),感興趣的可以了解一下
    2021-05-05
  • Java如何求交集、并集、差集

    Java如何求交集、并集、差集

    這篇文章主要介紹了Java如何求交集、并集、差集問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • 解決Error:Java:無(wú)效的源發(fā)行版:14問(wèn)題

    解決Error:Java:無(wú)效的源發(fā)行版:14問(wèn)題

    在項(xiàng)目開(kāi)發(fā)中,版本不一致常見(jiàn)問(wèn)題,首先,應(yīng)檢查本地JDK版本,使用命令java-version,其次,核對(duì)項(xiàng)目及模塊版本,若有不一致,通過(guò)修改pom.xml文件同步版本,重新下載依賴(lài)即可解決問(wèn)題,這種方法簡(jiǎn)單有效,適用于多種開(kāi)發(fā)環(huán)境
    2024-10-10
  • 通過(guò)實(shí)例了解java TransferQueue

    通過(guò)實(shí)例了解java TransferQueue

    這篇文章主要介紹了TransferQueue實(shí)例,下面小編和大家一起來(lái)學(xué)習(xí)一下
    2019-05-05
  • Java Bean的作用域,生命周期和注解

    Java Bean的作用域,生命周期和注解

    這篇文章主要介紹了淺談Spring中Bean的作用域,生命周期和注解,具有一定借鑒價(jià)值,需要的朋友可以參考下,希望能夠給你帶來(lái)幫助
    2021-11-11

最新評(píng)論

邵东县| 安溪县| 滨州市| 垣曲县| 永城市| 东明县| 嘉善县| 沛县| 阿拉善左旗| 仙居县| 西藏| 兴业县| 通许县| 盐源县| 民和| 南乐县| 泸水县| 沛县| 理塘县| 中宁县| 平邑县| 沾益县| 若羌县| 乐亭县| 平安县| 化德县| 临沭县| 朔州市| 娱乐| 洛扎县| 夏河县| 康乐县| 涞水县| 宁蒗| 广南县| 黄大仙区| 武威市| 新平| 吴川市| 綦江县| 垫江县|