解決線程安全的幾個方法

線程安全:線程安全問題的發現與解決-CSDN博客

Java中所使用的并發機制依賴于JVM的實現和CPU的指令。 所以了解并掌握深入Java并發編程基礎的前提知識是熟悉JVM的實現了解CPU的指令。

1.volatile簡介

在多線程并發編程中,有兩個重要的關鍵字:synchronized和volatile,譯為

volatile是輕量級的synchronized,它在多線程開發中確保了共享內存變量的"可見性"。

什么叫做可見性?簡要的概述其實很簡單:

當一個線程修改一個共享變量的話,另一個線程能知道并讀到這個修改后的值。

如果volatile變量修飾符使用得當的話,會比synchronized的使用和執行成本更低,因為它不會引起

線程的上下文切換和調度。

1.1volatile的定義與使用

volatile的定義:Java語言允許線程共享變量,為了確保共享變量能被準確和一致的更新,線程應該確保通過排他鎖單獨獲得這個變量。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。

上面我們提到了,可見性,volatile,synchronized還有排他鎖這幾個新鮮的概念,我們來逐個討論一下,等到了最后volatile關鍵字也就理解的差不多了。

1.內存可見性

談到可見性,一般都是內存可見性,內存可見性的問題是由于編譯器優化導致的,

正如我們開頭展示的筆記那樣,一個Java文件想要被cpu所執行,需要經歷重重編譯,轉化等等

而在程序員這個圈子里,水平各個參差不齊,總的來說還是菜鳥更多,大佬更少,怎么樣在這種情況讓菜鳥也能寫出來優秀的代碼呢?大佬們就在編譯器上動了手腳,加入了優化機制這樣一來,即使初學者寫出的代碼不夠高效,編譯器也能在背后“兜底”,生成更高性能的執行代碼,從而實現“寫出來的代碼比人本身更聰明”的目標。編譯器編譯的時候自動分析代碼的邏輯,在保持代碼邏輯不變的前提下,自動修改代碼的內容,從而讓代碼變得更高效。

在這個案例中,我們希望通過線程2來控制線程1的循環條件,從而控制線程1的結束。

從線程1的循環中,我們可以看到,每次循環的條件,再假設線程2不能控制的前提下,都是為真的,編譯器就發現了

1. 這里的isRunning每次讀到的都是相同的值,僅僅1s足夠讓循環執行上萬次,重復無效的代碼了

2. 編譯器查看循環條件和循環體并沒有發現需要修改的地方

對于編譯器而言,它無法靜態分析出這個修改何時發生、是否會發生,甚至是否發生在同一個內存空間(因為線程間的可見性并不總是成立)

并且,這段代碼的執行,依靠了,讀內存操作,比較和跳轉操作

通過讀內存操作從內存中讀取isRunning的值到CPU寄存器,通過比較寄存器存放的值和true是否相同,如果相同就繼續執行否則使用跳轉語句到指定的位置。

通過優化后變為

  • 從內存讀取變量值:通過“load”操作,將共享變量 isRunning 的值從主內存讀取到 CPU 寄存器 或說是線程工作內存中。

  • 寄存器中進行比較:循環條件判斷時,CPU 不會每次都訪問主內存,而是直接比較寄存器中或者說是工作內存中的值是否為 true

  • 分支跳轉指令執行控制流

    • 如果等于 true,程序繼續執行循環體;

    • 如果等于 false,程序跳轉到循環之后的位置,退出循環

也就是說,編譯器此處做了個大膽的決定,把訪問內存這步操作在第一次訪問后給優化掉了,后續的循環只需要從CPU寄存器或者緩存中讀取值即可!

此時如果t2線程即使修改了isRunning的值,t1線程也無法感知到了,t1已經被優化了并沒有從內存中讀取而是從(寄存器/緩存)工作內存中讀取了!

對于多線程中的內存可見性問題,其中一個關鍵原因就是編譯器為了優化性能而對代碼進行了重排和緩存

比如,在一個循環中重復讀取某個變量的值,編譯器會認為:

“這個變量的值在循環體內沒有被修改,而且看上去始終相同,那我就沒必要每次都從內存中去讀了,直接緩存到寄存器里用就行。

小問題:如果我們此時將While循環中的空代碼塊加入Thread.sleep(1)后發現?

線程1居然神奇的受到了線程2輸入的非零數字的影響結束了循環。

難道說Thread.sleep()也能解決內存可見性的問題嗎?

我們知道,內存可見性的問題本質上是編譯器優化所帶來的,但是引入sleep后這個代碼中的 ,從內存讀取的操作并沒有被編譯器優化掉

代碼的指令大致有

1.從內存中讀取數據load

2.cmp通過比較來判斷循環條件是否為真

3.sleep方法(背后是很多多的指令)

哪怕是sleep(0)在這里也不會被優化掉

我們在循環體中做各種復雜的操作,都會引起上述的優化失效!

綜上內存可見性的問題,我們已經了解的差不多了,可以談一下volatile關鍵字了,

如果我們在代碼的isRunning變量加上了volatile關鍵字,就可以解決上述的問題!

?

有沒有覺得很神奇,僅僅只是加了個關鍵字就解決了,我們談論那么長時間的內存可見性問題?

總結成一句話來說:

volatile 保證可見性,靠的是底層 JIT 編譯器在寫操作中生成帶 lock 前綴的匯編指令,這個指令通過緩存一致性協議,確保變量修改對所有 CPU 可見。

2.synchronized簡介

請注意volatile關鍵字只能解決內存可見性的問題,對于,多個進程訪問修改同一個變量,而造成的線程安全問題是無能為力的只能依靠synchronized

2.1synchronized的定義和使用

在多線程并發編程中,synchronized真是一位遠古大能級別的角色,很多人會稱呼他為重量級鎖,

但是隨著JavaSE的各種優化,有些情況下,他就不是那么重了。

synchronized實現同步的基礎:Java中每一個對象都可以作為鎖。具體表現為以下三種形式:

1.對于普通同步方法,鎖是當前的實例對象

2.對于靜態同步方法,鎖是當前類的Class對象

3.對于同步方法塊,鎖是synchronized括號中配置的對象

當一個線程試圖訪問同步代碼塊時,他首先必須要先得到鎖,退出或者拋出異常時必須釋放鎖。

synchronized(obj){...}中的obj就是在同步代碼塊中用來加鎖的那種對象,JVM會對這個obj對象的監視器(monitor)進行加鎖和解鎖,從而實現線程之間的互斥。

注意:此處加鎖并不是禁止線程調度,而是防止其他線程插隊。

該鎖塊中一共有大概

count++ == >(count = count + 1)

load(從內存讀取變量 count 的當前值)

add(對值進行+1的操作)

save(將新值寫回內存)

三個指令操作,執行上述這些操作指令的時候,是隨時會被其他線程插隊從cpu上調度走的,如果加了鎖就保證了操作的原子性,

因為此時如果其他線程嘗試加鎖操作,就會產生阻塞,從而避免執行上述指令時被插隊的問題。

(使用lock和unlock來代替synchronized的{ 和} )

?synchronized的要點

1.進入 { 就是加鎖,離開 } 就是解鎖?

2.加鎖操作是為了防止其他線程在本線程執行中插隊,而不影響本線程調度

3.鎖對象,兩個或者多個線程針對同一個對象加鎖才會有鎖競爭,鎖才會生效

對于下面的代碼是否存在線程安全問題?

對于兩個線程一個加鎖,一個沒有加鎖是會產生線程安全的問題的,

因為在一把鎖生效時,原子操作仍然會被打斷,另一個線程并沒有因為鎖而受到限制

對于下面兩種加鎖的方式,就涉及到鎖的粒度

t1線程:

對整個循環操做加鎖,鎖的粒度大,鎖內部代碼邏輯復雜

t2線程

每一次循環操作都會加鎖,加100次鎖,鎖的粒度小,鎖內部代碼邏輯少

由于synchronized的設計

在synchronized(){

}代碼塊中,

Java 中的 synchronized 關鍵字由 JVM 保證:無論同步代碼塊中是正常執行、return 提前返回,還是拋出異常(throw)提前終止,都會自動執行解鎖(unlock)操作。

這一點是很多高級語言設計lock和unlock操作的不足之處

1.可重入鎖

對于下面的代碼,是否可以正常運行呢?

假設說不存在可重入鎖的概念,我們來分析

當線程2進入第一層鎖,此時已經加鎖成功,如果此時再對同一個對象加第二次鎖就會產生死鎖,因為第一次加鎖的解鎖操作需要等到第二次加鎖并解決成功,而第二次的加鎖操作又得等第一次解鎖,就死鎖了。

但是Java中存在可重入鎖的概念,十分簡單:

Java 中的 synchronized可重入鎖(Reentrant Lock),其工作機制:

  • 每個鎖記錄:

    • 當前持有鎖的線程ID

    • 當前線程對這把鎖的重入次數(計數器)

于是:

  1. 線程 T1 首次獲得鎖 obj,線程ID 被記錄,重入次數為 1。

  2. T1 再次進入 synchronized(obj),JVM 檢查:鎖的持有者仍是 T1,本線程重入,于是允許繼續進入,同時 重入計數 +1

  3. 等兩個 synchronized 代碼塊都執行完后,T1 每退出一層,重入計數 -1,直到為 0 時,才真正釋放鎖。

避免了“自己鎖死自己”的問題,確保線程可以多次、安全地進入同一把鎖控制的臨界區。

那么鎖到底存在哪里呢?鎖里面會存儲什么信息呢?

這些就涉及到深入的理解了

synchronized的實現原理與應用&Java對象的內存布局_java synchronized原理java對象內存布局-CSDN博客

3.wait和notify簡介

3.1wait的定義和使用

首先需要清楚的是,wait和notify并不是Thread包括任何線程相關類的方法,而是Object基類的方法

在多線程的世界中,線程的調度是隨機的,雖然join方法可以簡單的控制線程的結束時間,

Thread.join() 是一個同步等待方法,可以讓主線程等待子線程執行完畢之后再繼續執行。

在main線程中調用t1,join()和t2.join(),main線程會等待t1和t2執行完畢,main才會執行完畢,而且t1和t2的執行完畢順序也不確定

學習過操作系統課程的一定見過一個很經典的操作,叫PV操作,里面的代碼都是手寫的,需要我們來分析,等到線程1完成了什么什么條件或者任務就會喚醒線程2的操作等等,但是PV操作和我們的wait和notify操作有著本質的區別

1.PV操作是操作系統底層的操作叫原語,基于“信號量(Semaphore)”,通過計數控制資源訪問

2.wait和notify方法是Java語言層面,基于“對象監視器(Monitor)”,通過條件變量進行線程協作

我們學到這里可以把PV操作暫時先忘掉了,雖然二者的很多用法相同,但是為了避免混淆還是不提及

程序中存在t1線程,t2線程

要求t1先執行某個邏輯A 然后t2再執行某個邏輯B

就比如我們生活的例子,只有A球員把球傳給B球員,B球員才能完成扣籃的操作

雖然wait方法任何對象都可以直接調用,如果我們直接調用的話會拋出以下異常:

1.在使用前wait也和sleep方法一樣需要拋出InterruptedException異常

2.運行后發現拋出了java.lang.IllegalMonitorStateException異常

翻譯一下就是非法的監視器狀態異常也就是說

你現在沒有處于這個對象的監視器鎖內部狀態,卻調用了必須在其中調用的方法。

JVM內部實現synchronized時,使用了形如monitor屬性作為變量/方法名,也被稱為“監視器鎖

wait() 必須在 synchronized(obj) 中使用,否則 JVM 會拋出 IllegalMonitorStateException,因為你沒有持有該對象的 monitor 鎖。

就像你去面試一樣,你都沒有準備去面試呢,就在想以后薪資會給你開多少。

使用wait的時候如果沒有被notify就會一直阻塞

在synchronized代碼塊中一共有三個動作:

1.釋放掉當前鎖

2.等待其他線程通知,此時處于阻塞狀態

3.當通知到達后,從阻塞狀態到就緒狀態,并重新嘗試獲取到鎖

假如wait一直占著鎖,別的線程會一直等待鎖,造成死鎖

wait如果是無參版本的話,屬于是死等,而wait也存在有參數的版本,同sleep一樣,等待一定的時間就不會等待了,

同時notify也有一個notifyAll的版本會喚醒所有線程,而notify只是隨機喚醒,上述例子中只有兩個線程,所以一個wait一個喚醒,如果多個線程就不一定的。

sleep和wait的區別

sleep() 是讓線程“暫停”一會兒,wait() 是讓線程“等待”別人通知它繼續。

對比點wait()sleep()
1. 設計目的主要用于線程間通信與協作,通常與 notify() / notifyAll() 搭配使用。主要用于讓線程休眠一段時間,是一種簡單的阻塞延遲機制。
2. 是否釋放鎖釋放鎖wait() 會釋放當前對象的監視器鎖(monitor)。不釋放鎖。線程進入休眠時仍然持有鎖。
3. 是否需要在同步塊中調用必須synchronized 塊中使用,否則拋出 IllegalMonitorStateException不需要,可以在任何地方調用。
4. 是否可以被喚醒可以被 notify() / notifyAll() 喚醒,也可以被 interrupt() 打斷。只能被 interrupt() 打斷,無法被 notify() 喚醒。
5. 是否拋異常需要處理 InterruptedException也需要處理 InterruptedException
6. 喚醒后行為通常被喚醒后繼續參與協作,如再次 wait() 或繼續執行臨界區。被打斷或睡眠時間到后繼續執行,不涉及線程間協作

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

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

相關文章

大模型應用班-第2課 DeepSeek使用與提示詞工程課程重點 學習ollama 安裝 用deepseek-r1:1.5b 分析PDF 內容

DeepSeek使用與提示詞工程課程重點Homework:ollama 安裝 用deepseek-r1:1.5b 分析PDF 內容python 代碼建構:1.小模型 1.5b 可以在 筆記本上快速執行2.分析結果還不錯3. 重點是提示詞 prompt 的寫法一、DeepSeek模型創新與特點1. DeepSeek-V3模型特點采用…

在FreeBSD系統下使用llama-cpp運行飛槳開源大模型Ernie4.5 0.3B(失敗)

先上結論,截止到目前2025.7.25日,還不能用。也就是Ernie4.5模型無法在llama.cpp 和Ollama上進行推理,原因主要就llama是不支持Ernie4.5異構MoE架構。 不局限于FreeBSD系統,Windows也測試失敗,理論上Ubuntu下也是不行。…

OpenCV圖像梯度、邊緣檢測、輪廓繪制、凸包檢測大合集

一、圖像梯度 在圖像處理中,「梯度(Gradient)」是一個非常基礎但又極其重要的概念。它是圖像邊緣檢測、特征提取、紋理分析等眾多任務的核心。梯度的本質是在空間上描述像素灰度值變化的快慢和方向。 但我們如何在圖像中計算梯度?…

GitHub 趨勢日報 (2025年07月25日)

📊 由 TrendForge 系統生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日報中的項目描述已自動翻譯為中文 📈 今日獲星趨勢圖 今日獲星趨勢圖1814Resume-Matcher985neko714Qwen3-Coder622OpenBB542BillionMail486hrms219hyper…

編程語言Java——核心技術篇(五)IO流:數據洪流中的航道設計

🌟 你好,我是 勵志成為糕手 ! 🌌 在代碼的宇宙中,我是那個追逐優雅與性能的星際旅人。 ? 每一行代碼都是我種下的星光,在邏輯的土壤里生長成璀璨的銀河; 🛠? 每一個算法都是我繪制…

基于FPGA的16QAM軟解調+卷積編碼Viterbi譯碼通信系統,包含幀同步,信道,誤碼統計,可設置SNR

目錄 1.引言 2.算法仿真效果 3.算法涉及理論知識概要 3.1 16QAM調制軟解調原理 3.2 幀同步 3.3 卷積編碼,維特比譯碼 4.Verilog程序接口 5.參考文獻 6.完整算法代碼文件獲得 1.引言 基于FPGA的16QAM軟解調卷積編碼Viterbi譯碼通信系統開發,包含幀同步,高斯…

Python數據分析基礎(二)

一、Numpy 常用函數分類概覽函數類別常用函數基本數學函數np.sum(x)、np.sqrt(x)、np.exp(x)、np.log(x)、np.sin(x)、np.abs(x)、np.power(a, b)、np.round(x, n) 等統計函數np.mean(x)、np.median(x)、np.std(x)、np.var(x)、np.min(x)、np.max(x)、np.percentile(x, q) 等比…

Colab中如何臨時使用udocker(以MinIO為例)

本文主要是想記錄一下自己在Colab中用udocker啟動一個MinIO的容器的過程。 1. 命令行配置環境 由于目前沒有用到GPU,所以我選擇的是CPU的環境。(內存12G)然后就可以在命令行里安裝udocker了,并配置minio的環境 # 由于minio需要做兩個端口映射&#xff0c…

rt-thread 5.2.1 基于at-start-f437開發過程記錄

基于rt-thread 5.2.1 bsp/at/at32f437-start進行開發,記錄詳細過程,包括中間遇到的各種坑。 at32f437-start原理圖 自己設計的電路板主要換了一塊小封裝的同系列芯片, 目標是移植opENer。 1. 開發環境 env長時間不用,有點忘了。這次新下載…

EMCCD相機與電可調變焦透鏡的同步控制系統設計與實現

EMCCD相機與電可調變焦透鏡的同步控制系統設計與實現 前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家,覺得好請收藏。點擊跳轉到網站。 摘要 本文詳細介紹了基于Python的EMCCD相機&#xff0…

前綴和-560.和為k的子數組-力扣(LeetCode)

一、題目解析1.子數組是數組中元素的連續非空序列2.nums[i]范圍為[-1000,1000],存在負數3.由于2的題目條件,該題不能用雙指針算法,不具備單調性 二、算法原理解法1:暴力解法->枚舉 O(N^2)固定一個值,向后枚舉數組和…

解決企業微信收集表沒有圖片、文件組件,不能收集圖片的問題

問題: 企業微信里面的收集表功能,有一個圖片收集的收集表,但是插入的組件沒有收集圖片的組件? 原因: 大概率是微盤未啟用 解決方法: 1、登陸企業微信管理后臺 企業微信 2、訪問微盤頁面,…

認識單片機

《認識單片機》課程內容 一、課程導入 在我們的日常生活中,有很多看似普通卻充滿智慧的小物件。比如家里的智能電飯煲,它能精準地控制煮飯的時間和溫度,讓米飯煮得香噴噴的;還有樓道里的聲控燈,當有人走過發出聲音時&a…

數據結構(2)順序表算法題

一、移除元素1、題目描述2、算法分析 思路1:查找val值對應的下標pos,執行刪除pos位置數據的操作。該方法時間復雜度為O(n^2),因此不建議使用。思路2:創建新數組(空間大小與原數組一致&#xff0…

汽車電子架構

本文試圖從Analog Devices官網中的汽車解決方案視角帶讀者構建起汽車電子的總體架構圖,為國內熱愛和從事汽車電子行業的伙伴們貢獻一份力量。 一 、汽車電子架構總覽 整個汽車電子包括四個部分:車身電子(Body Electronics)、座艙與…

pycharm 2025 專業版下載安裝教程【附安裝包】

安裝之前,請確保已經關閉所有安全軟件(如殺毒軟件、防火墻等)安裝包 👇鏈接:https://pan.xunlei.com/s/VOU-5_L1KOH5j3zDaaCh-Z28A1# 提取碼:6bjy下載 PyCharm2025專業版 安裝包 并 進行解壓運行 pycharm-2…

在 Java 世界里讓對象“旅行”:序列化與反序列化

Java 生態里關于 JSON 的序列化與反序列化(以下簡稱“序列化”)是一個久經考驗的話題,卻常因框架繁多、配置瑣碎而讓初學者望而卻步。本文將圍繞一段極簡的 JsonUtils 工具類展開,以 FastJSON 與 Jackson 兩大主流實現為例&#x…

High Speed SelectIO Wizard ip使用記錄

本次實驗的目的是通過VU9P開發板的6個TG接口,采用固定連接的方式,即X和X-維度互聯,其框圖如下所示:IP參數配置通過調用High Speed SelectIO Wizard來實現數據通路,High Speed SelectIO Wizard ip有24對數據通道&#x…

Execel文檔批量替換標簽實現方案

問題背景需求:俺現網班級作為維度,批量導出每個班級學員的數據,excel的個數在1k左右,每一張表的人數在90左右。導出總耗時在10小時左右。代碼編寫完成并導出現網數據后,發現導出的標題錯了。解決方案1.通過修改代碼&am…

SpringBoot配置多數據源多數據庫

Springboot支持配置多數據源。默認情況,在yml文件中只會配置一個數據庫。如果涉及到操作多個數據庫的情況,在同實例中(即同一個ip地址下的不同數據庫),可以采用數據庫名點數據庫表的方式,實現跨庫表的操作。…