什么是線程安全?如何保證線程安全?

目錄

一、引入線程安全?👇

二、?線程安全👇

1、線程安全概念?🔍

2、線程不安全的原因?🔍

搶占式執行(罪魁禍首,萬惡之源)導致了線程之間的調度是“隨機的”

多個線程修改同一個變量?

?修改操作,不是原子的(不可分割的最小單位)?

內存可見性,引起的線程不安全?

指令重排序,引起的線程不安全

三、解決之前的線程不安全問題👇

1、synchronized 關鍵字-監視器鎖monitor lock??🔍

1)synchronized 的特性?

(1) 互斥

(2)刷新內存

(3) 可重入

2)synchronized 使用示例?

1) 直接修飾普通方法:

?2) 修飾靜態方法:

3) 修飾代碼塊: 明確指定鎖哪個對象.

2、volatile 關鍵字?(保證內存可見性)?🔍

?1)引入volatile

2)volatile不保證原子性

💡?總結:


一、引入線程安全?👇

執行以下代碼:

package threading;
class Counter {public int count = 0;void increase() {count++;}
}public class ThreadDemo23 {public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

可以觀察代碼,我們對兩個線程分別累加50000次,結果應該為100000,但是大家看運行結果并非如此,而且可以發現,每次運行的結果都不一樣,這是什么原因呢??

答案就是涉及到了線程安全問題

?

?

本質原因:線程在系統中的調度是無序的/隨機的(搶占式執行的)?


二、?線程安全👇

1、線程安全概念?🔍

? ? 如果多線程環境下代碼運行的結果是符合我們預期的,即在單線程環境應該的結果,則說這個程序是線 程安全的。?

2、線程不安全的原因?🔍

  • 搶占式執行(罪魁禍首,萬惡之源)導致了線程之間的調度是“隨機的”

  • 多個線程修改同一個變量?

一個線程修改同一個變量=>安全

多個線程讀取同一個變量=>安全

多個線程修改不同變量=> 安全?

  • ?修改操作,不是原子的(不可分割的最小單位)?

什么是原子性🐶

我們把一段代碼想象成一個房間,每個線程就是要進入這個房間的人。如果沒有任何機制保證,A進入房間之后,還沒有出來;B 是不是也可以進入房間,打斷 A 在房間里的隱私。這個就是不具備原子性的。

那我們應該如何解決這個問題呢?🐶

是不是只要給房間加一把鎖,A 進去就把門鎖上,其他人是不是就進 不來了。這樣就保證了這段代碼的原子性了。 有時也把這個現象叫做同步互斥,表示操作是互相排斥的。

一條 java 語句不一定是原子的,也不一定只是一條指令 比如剛才我們看到的 n++,其實是由三步操作組成的: 1. 從內存把數據讀到 CPU 2. 進行數據更新 3. 把數據寫回到 CPU

不保證原子性會給多線程帶來什么問題🐶

如果一個線程正在對一個變量操作,中途其他線程插入進來了,如果這個操作被打斷了,結果就可能是錯誤的。 這點也和線程的搶占式調度密切相關. 如果線程不是 "搶占" 的, 就算沒有原子性, 也問題不大.

  • 內存可見性,引起的線程不安全?

? ? ? ? ? 可見性指, 一個線程對共享變量值的修改,能夠及時地被其他線程看到.

  • 指令重排序,引起的線程不安全


三、解決之前的線程不安全問題👇

1、synchronized 關鍵字-監視器鎖monitor lock??🔍

1)對文章初始代碼進行修改:既可以保證 ++ 操作就是原子的,不受影響啦

可以將{ }視為廁所,進表示加鎖,出表示解鎖

void increase() {//鎖有倆個核心操作,加鎖和解鎖//進入該代碼塊就會觸發加鎖,出了代碼塊,就會觸發解鎖synchronized (this) {count++;}}

?注意:此處的this指的就是counter對象

因此,在上述代碼中,兩個線程實在競爭同一個鎖對象,就會產生鎖競爭。

再執行上述代碼:就是100000

疑惑為啥以上操作為什么可以解決線程安全問題呢??🐶

1)synchronized 的特性?

(1) 互斥

? ? ? synchronized 會起到互斥效果, 某個線程執行到某個對象的 synchronized 中時, 其他線程如果也執行到 同一個對象 synchronized 就會阻塞等待.

進入 synchronized 修飾的代碼塊, 相當于 加鎖

退出 synchronized 修飾的代碼塊, 相當于 解鎖?

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

注意: 上一個線程解鎖之后, 下一個線程并不是立即就能獲取到鎖. 而是要靠操作系統來 "喚醒". 這 也就是操作系統線程調度的一部分工作.

假設有 A B C 三個線程, 線程 A 先獲取到鎖, 然后 B 嘗試獲取鎖, 然后 C 再嘗試獲取鎖, 此時 B 和 C 都在阻塞隊列中排隊等待. 但是當 A 釋放鎖之后, 雖然 B 比 C 先來的, 但是 B 不一定就能 獲取到鎖, 而是和 C 重新競爭, 并不遵守先來后到的規則.?

(2)刷新內存

synchronized 的工作過程:?

  1. 獲得互斥鎖
  2. 從主內存拷貝變量的最新副本到工作的內存
  3. 執行代碼
  4. 將更改后的共享變量的值刷新到主內存
  5. 釋放互斥鎖 所以 synchronized 也能保證內存可見性.?
(3) 可重入

synchronized 同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題;?

理解 "把自己鎖死" :一個線程沒有釋放鎖, 然后又嘗試再次加鎖.?

按照之前對于鎖的設定, 第二次加鎖的時候, 就會阻塞等待. 直到第一次的鎖被釋放, 才能獲取到第 二個鎖. 但是釋放第一個鎖也是由該線程來完成, 結果這個線程已經躺平了, 啥都不想干了, 也就無 法進行解鎖操作. 這時候就會死鎖

當然,Java 中的 synchronized 是 可重入鎖, 因此沒有上面的問題

?示例:

static class Counter {public int count = 0;synchronized void increase() {count++;}synchronized void increase2() {increase();}
}

increase 和 increase2 兩個方法都加了 synchronized,

此處的 synchronized 都是針對 this 當前對象加鎖的.

在調用 increase2 的時候, 先加了一次鎖, 執行到 increase 的時候, 又加了一次鎖. (上個鎖還沒釋 放, 相當于連續加兩次鎖)

這個代碼是完全沒問題的. 因為 synchronized 是可重入鎖.

注意:在可重入鎖的內部, 包含了 "線程持有者" 和 "計數器" 兩個信息.

如果某個線程加鎖的時候, 發現鎖已經被人占用, 但是恰好占用的正是自己, 那么仍然可以繼續獲取 到鎖, 并讓計數器自增.

解鎖的時候計數器遞減為 0 的時候, 才真正釋放鎖. (才能被別的線程獲取到)?

2)synchronized 使用示例?

?synchronized 本質上要修改指定對象的 "對象頭". 從使用角度來看, synchronized 也勢必要搭配一個具 體的對象來使用.

1) 直接修飾普通方法:

鎖的 SynchronizedDemo 對象🐶

public class SynchronizedDemo {public synchronized void methond() {}
}
?2) 修飾靜態方法:

鎖的 SynchronizedDemo 類的對象🐶

public class SynchronizedDemo {public synchronized static void method() {}
}
3) 修飾代碼塊: 明確指定鎖哪個對象.

鎖當前對象🐶

public class SynchronizedDemo {public void method() {synchronized (this) {}}
}

鎖類對象🐶

類對象是啥:Counter.class

.java源代碼文件,javac =>.class(二進制字節碼文件),JVM就可以執行.class文件了,類對象就可以表示這個.class文件的內容~~(描述了類的方方面面的詳細信息,比如誒的名字,類的屬性,類的方法,)

public class SynchronizedDemo {public void method() {synchronized (SynchronizedDemo.class) {}}
}

2、volatile 關鍵字?(保證內存可見性)🔍

?1)引入volatile

所謂內存可見性,就是多線程環境下,編譯器對于代碼優化,產生了誤判,從而引起了bug,進一步導致了代碼的bug

因此我們可以加上 volatile(讓編譯器對這個場景暫停優化) , 強制讀寫內存. 速度是慢了, 但是數據變的更準確了.每次都是從內存中重新讀取數據

觀察以下代碼:

package threading;import java.util.Scanner;public class ThreadDemo24 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("循環結束,t1結束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("請輸入一個整數");//t2通過控制臺輸入一個整數,yidanyonghushurule非0的值,此時t1的循環就會立即結束,從而t1線程就會退出flag = scanner.nextInt();});t1.start();t2.start();}
}

?我們預期的效果應該是輸入一個非零的數,線程t1就會停止,但實際上仍然在執行,處在RUNNABLE狀態

?為什么會出現以上問題呢?內存可見性的鍋!!!

讓我們分析一下代碼:

直接訪問工作內存(實際是 CPU 的寄存器或者 CPU 的緩存), 速度 非常快, 但是可能出現數據不一致的情況.

?

volatile public static int flag = 0;

加上volatile關鍵字,此時編譯器就可以保證每次都是重新從內存讀取flag變量的值,

此時t2修改flag,t1就可以立即感知到了,t1就可以正確退了

2)volatile不保證原子性

這個是最初的演示線程安全的代碼.

給 increase 方法去掉 synchronized

給 count 加上 volatile 關鍵字.

static class Counter {volatile public int count = 0;void increase() {count++;}
}
public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);
}

此時可以看到, 最終 count 的值仍然無法保證是 100000?

💡?總結:
  • volatile不保證原子性
  • volatile也能禁止指令重排序
  • volatile 適用一個線程讀,一個線程寫的情況
  • synchronized則是多個線程寫?
  • volatile 和 synchronized 有著本質的區別. synchronized 能夠保證原子性(也能保證內存可見性), volatile 保證的是內存可見 性
static class Counter {public int flag = 0;
}
public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (true) {synchronized (counter) {if (counter.flag != 0) {break;}}// do nothing}System.out.println("循環結束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("輸入一個整數:");counter.flag = scanner.nextInt();});t1.start();t2.start();
}

上面代碼:

去掉 flag 的 volatile

給 t1 的循環內部加上 synchronized, 并借助 counter 對象加鎖.?

運行結果是可以正常結束的

因此 synchronized是可以保證內存可見性的

?

?補充: 指令重排序,也是編譯器優化的策略,調整了代碼的執行順序,讓程序更高效,前提也是保證整體邏輯不變

?💡?總結:(面試題)

線程不安全的原因:

【根本原因】==操作系統上的線程是“搶占式執行”的,線程調度是隨機的,==這是線程不安全的一個主要原因。隨機調度會導致在多線程環境下,程序的執行順序不確定,程序員必須確保無論哪種執行順序,代碼都能正常運行。
【代碼結構】共享資源:多個線程同時訪問并修改共享的數據或資源。當多個線程同時訪問和修改共享資源時容易引發競態條件和數據不一致的問題。
①一個線程修改一個變量是安全的
②多個線程修改一個變量是不安全的
③多個線程修改不同變量是安全的
【直接原因】多線程操作不是“原子的”。多線程操作中的原子性指的是一個操作是不可中斷的,要么全部執行完成,要么都不執行,不能被其他線程干擾。這對于并發編程非常重要,因為如果一個操作在執行過程中被中斷,可能導致數據不一致或者其他意外情況發生。(在上述多線程操作中,count++操作不是“原子的”,而是由多個CPU指令組成的,一個線程執行這些指令時,可能會在執行過程中被搶占,從而給其他線程“可乘之機”。要保證原子性操作,每個CPU指令都應該是“原子的”,即要么完全執行,要么完全不執行。)
內存可見性問題:在多線程環境下調用不可重入的函數(即不支持多線程調用的函數),可能導致數據混亂或程序崩潰。
指令重排序問題:在多線程環境下,由于編譯器或處理器對指令進行重排序優化,可能導致預期之外的程序行為。

?

線程安全就是多線程訪問時,采用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致或者數據污染。
線程不安全就是不提供數據訪問保護,有可能出現多個線程先后更改數據造成所得到的數據是臟數據。


? ? ? ? ? ? ? ? ? ? ? ??

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/13992.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/13992.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/13992.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ESP8266實現獲取天氣情況

利用太極創客提供的ESP8266 心知天氣庫獲取天氣情況并顯示 心知天氣庫地址&#xff1a; ESP8266-心知天氣: 本庫主要功能為使用ESP8266物聯網開發板通過心知天氣 API 獲取天氣等信息。 clone到本地: git clone https://gitee.com/taijichuangke/ESP8266-Seniverse.git 安裝該…

跟著Kimi學習結構化提示詞:19套內置提示詞都在這里了!

大家好&#xff0c;我是木易&#xff0c;一個持續關注AI領域的互聯網技術產品經理&#xff0c;國內Top2本科&#xff0c;美國Top10 CS研究生&#xff0c;MBA。我堅信AI是普通人變強的“外掛”&#xff0c;所以創建了“AI信息Gap”這個公眾號&#xff0c;專注于分享AI全維度知識…

C++ Primer Plus第十六章復習題

1、考慮下面的 類聲明 class RQ1 { private:char * st; public:RQ1(){st new char [1]; strcpy(st,"");}RQ1(const RQ1 & rq){st new char [strlen(rq.st)1]; strcpy(st,rq.st);}~RQ1(){delete [] st};RQ & OPERATOR (cosnt RQ &rq); }; 將它轉換為使…

Java技術深度解析:高級面試問題與精粹答案(一)

Java 面試問題及答案 問題1&#xff1a;請解釋什么是Java虛擬機&#xff08;JVM&#xff09;以及它的作用是什么&#xff1f; 答案1&#xff1a; Java虛擬機&#xff08;JVM&#xff09;是一個可以執行Java字節碼的虛擬計算機。它是一個抽象的計算機&#xff0c;能夠通過軟件…

【筆記】樹(Tree)

一、樹的基本概念 1、樹的簡介 之前我們都是在談論一對一的線性數據結構&#xff0c;可現實中也有很多一對多的情況需要處理&#xff0c;所以我們就需要一種能實現一對多的數據結構--“樹”。 2、樹的定義 樹&#xff08;Tree&#xff09;是一種非線性的數據結構&#xff0…

作物水文模型AquaCrop---用于評估作物對水的需求、灌溉計劃和管理策略

AquaCrop是由世界糧食及農業組織&#xff08;FAO&#xff09;開發的一個先進模型&#xff0c;旨在研究和優化農作物的水分生產效率。這個模型在全球范圍內被廣泛應用于農業水管理&#xff0c;特別是在制定農作物灌溉計劃和應對水資源限制方面顯示出其強大的實用性。AquaCrop 不…

如何在海豚調度器自動監測報表是否跑出數據

在數據倉庫報表開發時,有的報表依賴的表多,雖然在海豚調度任務上是跑成功,但實際上沒有跑出數據來。開發人員負責的任務和表越來越多,每天去手動檢查費時費力,不去理睬默認是成功的,等到業務或產品發現問題時,又給人一種不專業不負責的感覺。 比較好的方式是用代碼進行自…

Python知識點復習

文章目錄 Input & OutputVariables & Data typesPython字符串重復&#xff08;字符串乘法&#xff09;字符串和數字連接在一起print時&#xff0c;要強制類型轉換int為str用input()得到的用戶輸入&#xff0c;是str類型&#xff0c;如果要以int形式計算的話&#xff0c…

SkyWalking 介紹及部署

1、SkyWalking簡介2、SkyWalking的搭建 2.1 部署Elasticsearch2.2 部署SkyWalking-Server2.3 部署SkyWalking-UI3、應用接入 3.1 jar包部署方式3.2 dockerfile方式3.3 DockerFile示例4、SkyWalking UI 界面說明 4.1 儀表盤 4.1.1 APM &#xff08;1&#xff09;全局維度&#x…

UBUNTU22.04無法安裝nvidia-driver-550 依賴于 nvidia-dkms-550 (<= 550.54.15-1)

類似的報錯信息&#xff0c;就是卡在了nvidia-dkms-550無法安裝 Loading new nvidia-550.40.07 DKMS files… Building for 6.5.0-15-generic Building for architecture x86_64 Building initial module for 6.5.0-15-generic ERROR: Cannot create report: [Errno 17] File e…

前端canvas項目實戰——在線圖文編輯器(十):小地圖MiniMap(上)

目錄 前言一、 效果展示二、 實現步驟0. 行動前的思考1. 為小地圖更新「背景圖」2. 為小地圖更新「滑動窗口」2.1 獲取新的滑動窗口「寬高」2.2 獲取新的滑動窗口「位置」3. 為小地圖更新「遮罩」后記前言 上一篇博文中,我們引入了「邏輯畫布」的概念,讓整個工具的頁面看起來…

JPA 3萬字面試寶典

目錄 什么是JPA? JPA和Hibernate有什么區別? 什么是ORM(對象關系映射)? 什么是Entity?

【機器學習】在電子商務(淘*拼*京*—>抖)的應用分析

機器學習與大模型&#xff1a;電子商務的新引擎 一、電子商務的變革與挑戰二、機器學習與大模型的崛起三、機器學習與大模型在電子商務中的應用實踐個性化推薦精準營銷智能客服庫存管理與商品定價 四、總結與展望 隨著互聯網的飛速發展&#xff0c;電子商務已經成為我們生活中不…

NDIS小端口驅動(四)

NDIS中斷相關 1. 注冊和取消注冊中斷&#xff1a; 微型端口驅動程序調用 NdisMRegisterInterruptEx 來注冊中斷。 驅動程序分配并初始化 NDIS_MINIPORT_INTERRUPT_CHARACTERISTICS 結構&#xff0c;以指定中斷特征和函數入口點&#xff0c;驅動程序將結構傳遞給 NdisMRegister…

【三劍客和正則表達式】

文章目錄 學習目標一、什么是三劍客1.三劍客grep2.三劍客sed3.三劍客awk4.正則過濾例子15.正則過濾例子2 總結 學習目標 1.學會使用 grep 2.學會使用 sed 3.學會使用 awk 4.學會使用正則表達式一、什么是三劍客 正則三劍客&#xff1a;grep sed awk 1.三劍客grep # 擅長過濾…

【MySQL精通之路】查詢優化器的使用(8)

MySQL通過影響查詢計劃評估方式的系統變量、可切換優化、優化器和索引提示以及優化器成本模型提供優化器控制。 服務器在column_statistics數據字典表中維護有關列值的直方圖統計信息&#xff08;請參閱第10.9.6節“Optimizer統計信息”&#xff09;。與其他數據字典表一樣&am…

#Ethereum 現貨ETF 問題匯總 轉

專題&#xff1a; #Ethereum 現貨ETF 問題匯總&#xff0c;包括了多數小伙伴們的疑問&#xff0c;有任何忽略請留言給我&#xff0c;我會補充。 1. #ETH 現貨ETF何時公布&#xff1f; 一般來說會在北京時間的5月24日凌晨2點至4點之間&#xff0c;不排除稍微延后到凌晨6點的可能…

基于大語言模型的應用

在AI領域&#xff0c;大語言模型已成為備受矚目的焦點&#xff0c;尤其在自然語言處理&#xff08;NLP&#xff09;領域&#xff0c;其應用愈發廣泛。BLM作為一種多任務語言建模方法&#xff0c;旨在構建一個具備多功能的強大模型。在給定文本和查詢條件下&#xff0c;該模型能…

【深度學習】YOLOv8訓練,交通燈目標檢測

文章目錄 一、數據處理二、環境三、訓練 一、數據處理 import traceback import xml.etree.ElementTree as ET import os import shutil import random import cv2 import numpy as np from tqdm import tqdmdef convert_annotation_to_list(xml_filepath, size_width, size_he…

海山數據庫(He3DB)代理ProxySQL使用詳解:(二)功能實測

讀寫分離實測 ProxySQL官方demo演示了三種讀寫分離的方式&#xff1a;使用不同的端口進行讀寫分離、使用正則表達式進行通用的讀寫分離、使用正則和digest進行更智能的讀寫分離。最后一種是針對特定業務進行的優化調整&#xff0c;也可將其歸結為第二種方式&#xff0c;下邊分…