Java多線程與高并發專題——原子類和 volatile、synchronized 有什么異同?

原子類和 volatile異同

首先,通過我們對原子類和的了解,原子類和volatile 都能保證多線程環境下的數據可見性。在多線程程序中,每個線程都有自己的工作內存,當多個線程訪問共享變量時,可能會出現一個線程修改了共享變量的值,而其他線程不能及時看到最新值的情況。原子類和volatile關鍵字都能在一定程度上解決這個問題。例如,當一個變量被volatile修飾后,對該變量的寫操作會立即刷新到主內存,讀操作會直接從主內存讀取,保證了其他線程能看到最新的值;原子類同樣可以保證對變量操作的結果能被其他線程及時看到。

下面我們通過一個代碼去看看它們的差異:

/*** 該類用于演示 volatile 關鍵字和 AtomicInteger 類在多線程環境下的不同表現。* 展示了使用 volatile 變量和 AtomicInteger 類進行自增操作的差異。*/
public class VolatileVsAtomic {// 用 volatile 修飾的變量,保證變量的可見性,但不保證操作的原子性private static volatile int volatileCount = 0;// 原子類,提供原子操作,保證操作的原子性private static AtomicInteger atomicCount = new AtomicInteger(0);/*** 主方法,程序的入口點。* 創建多個線程,分別對 volatile 變量和 AtomicInteger 類的實例進行自增操作,并輸出結果。** @param args 命令行參數* @throws InterruptedException 如果線程在等待時被中斷*/public static void main(String[] args) throws InterruptedException {// 定義線程數量int threadCount = 10;// 創建線程數組Thread[] threads = new Thread[threadCount];// 使用 volatile 變量進行自增操作for (int i = 0; i < threadCount; i++) {// 創建線程threads[i] = new Thread(() -> {// 每個線程執行 1000 次自增操作for (int j = 0; j < 1000; j++) {// 此操作不是原子性的,可能會出現數據競爭問題volatileCount++;}});// 啟動線程threads[i].start();}// 等待所有線程執行完畢for (Thread thread : threads) {thread.join();}// 輸出 volatile 變量的最終值System.out.println("Volatile count: " + volatileCount);// 重置計數器volatileCount = 0;atomicCount.set(0);// 使用原子類進行自增操作for (int i = 0; i < threadCount; i++) {// 創建線程threads[i] = new Thread(() -> {// 每個線程執行 1000 次自增操作for (int j = 0; j < 1000; j++) {// 原子性自增操作,保證操作的原子性atomicCount.incrementAndGet();}});// 啟動線程threads[i].start();}// 等待所有線程執行完畢for (Thread thread : threads) {thread.join();}// 輸出 AtomicInteger 類實例的最終值System.out.println("Atomic count: " + atomicCount.get());}
}

?輸出結果如下:

在上述代碼中,volatileCount是一個被volatile修飾的變量,多個線程對其進行自增操作時,由于自增操作不是原子性的,最終結果可能小于預期值;而atomicCount是一個AtomicInteger類型的原子類,多個線程對其進行自增操作時,能保證操作的原子性,最終結果是準確的。

原子類和 volatile 的使用場景

那下面我們就來說一下原子類和 volatile 各自的使用場景。

我們可以看出,volatile 和原子類的使用場景是不一樣的,如果我們有一個可見性問題,那么可以使用 volatile 關鍵字,但如果我們的問題是一個組合操作,需要用同步來解決原子性問題的話,那么可以使用原子變量,而不能使用 volatile 關鍵字。

通常情況下,volatile 可以用來修飾 boolean 類型的標記位,因為對于標記位來講,直接的賦值操作本身就是具備原子性的,再加上 volatile 保證了可見性,那么就是線程安全的了。

而對于會被多個線程同時操作的計數器 Counter 的場景,這種場景的一個典型特點就是,它不僅僅是一個簡單的賦值操作,而是需要先讀取當前的值,然后在此基礎上進行一定的修改,再把它給賦值回去。這樣一來,我們的 volatile 就不足以保證這種情況的線程安全了。我們需要使用原子類來保證線程安全。

原子類和 synchronized異同

原子類和 synchronized 關鍵字都可以用來保證線程安全,下面我們分別用原子類和 synchronized 關鍵字來解決一個經典的線程安全問題,給出具體的代碼對比,然后再分析它們背后的區別。

首先,原始的線程不安全的情況的代碼如下所示:

/*** BaseTest 類實現了 Runnable 接口,用于演示多線程并發修改共享變量的情況。* 該類包含一個靜態變量 value,多個線程會同時對其進行遞增操作。*/
public class BaseTest implements Runnable{// 靜態變量 value,用于存儲線程遞增的結果static int value = 0;/*** main 方法是程序的入口點,創建并啟動兩個線程來執行 BaseTest 實例的 run 方法。* 等待兩個線程執行完畢后,打印最終的 value 值。* * @param args 命令行參數* @throws InterruptedException 如果線程在等待過程中被中斷*/public static void main(String[] args) throws InterruptedException {// 創建 BaseTest 實例Runnable runnable = new BaseTest();// 創建第一個線程并傳入 BaseTest 實例Thread thread1 = new Thread(runnable);// 創建第二個線程并傳入 BaseTest 實例Thread thread2 = new Thread(runnable);// 啟動第一個線程thread1.start();// 啟動第二個線程thread2.start();// 等待第一個線程執行完畢thread1.join();// 等待第二個線程執行完畢thread2.join();// 打印最終的 value 值System.out.println(value);}/*** run 方法是 Runnable 接口的實現,包含一個循環,將 value 變量遞增 10000 次。*/@Overridepublic void run() {// 循環 10000 次,每次將 value 加 1for (int i = 0; i < 10000; i++) {value++;}}
}

在代碼中我們新建了一個 value 變量,并且在兩個線程中對它進行同時的自加操作,每個線程加 10000次,然后我們用 join 來確保它們都執行完畢,最后打印出最終的數值。

因為 value++ 不是一個原子操作,所以上面這段代碼是線程不安全的,所以代碼的運行結果會小于 20000,例如我執行的結果如下:

我們首先給出方法一,也就是用原子類來解決這個問題,代碼如下所示:

/*** AtomicTest 類實現了 Runnable 接口,用于演示使用 AtomicInteger 進行線程安全的計數操作。* 該類創建了兩個線程,每個線程都會對一個靜態的 AtomicInteger 實例進行 10000 次遞增操作。* 最后,主線程等待兩個子線程執行完畢,并輸出最終的計數值。*/
public class AtomicTest implements Runnable {// 靜態的 AtomicInteger 實例,用于線程安全的計數操作static AtomicInteger atomicInteger = new AtomicInteger();/*** 程序的入口點,創建并啟動兩個線程,等待它們執行完畢,然后輸出最終的計數值。** @param args 命令行參數,在本程序中未使用。* @throws InterruptedException 如果在等待線程執行完畢時被中斷。*/public static void main(String[] args) throws InterruptedException {// 創建一個 AtomicTest 實例,作為線程的任務Runnable runnable = new AtomicTest();// 創建第一個線程并傳入任務Thread thread1 = new Thread(runnable);// 創建第二個線程并傳入任務Thread thread2 = new Thread(runnable);// 啟動第一個線程thread1.start();// 啟動第二個線程thread2.start();// 等待第一個線程執行完畢thread1.join();// 等待第二個線程執行完畢thread2.join();// 輸出最終的計數值System.out.println(atomicInteger.get());}/*** 實現 Runnable 接口的 run 方法,該方法會對 atomicInteger 進行 10000 次遞增操作。*/@Overridepublic void run() {// 循環 10000 次,每次對 atomicInteger 進行遞增操作for (int i = 0; i < 10000; i++) {// 原子地遞增 atomicInteger 的值并返回更新后的值atomicInteger.incrementAndGet();}}
}

用原子類之后,我們的計數變量就不再是一個普通的?int?變量了,而是 AtomicInteger 類型的對象,并且自加操作也變成了 incrementAndGet 法。由于原子類可以確保每一次的自加操作都是具備原子性的,所以這段程序是線程安全的,所以以上程序的運行結果會始終等于 20000。

下面我們給出方法二,我們用 synchronized 來解決這個問題,代碼如下所示:

/*** SynTest 類用于演示多線程環境下的同步機制。* 該類實現了 Runnable 接口,多個線程可以共享同一個實例來執行任務。* 通過同步塊確保對靜態變量 value 的安全訪問。*/
public class SynTest  implements Runnable {// 靜態變量,用于記錄所有線程累加的結果static int value = 0;/*** 程序的入口點,創建并啟動兩個線程來執行任務。** @param args 命令行參數* @throws InterruptedException 如果線程在等待時被中斷*/public static void main(String[] args) throws InterruptedException {// 創建 SynTest 類的實例Runnable runnable = new SynTest();// 創建第一個線程并傳入 Runnable 實例Thread thread1 = new Thread(runnable);// 創建第二個線程并傳入 Runnable 實例Thread thread2 = new Thread(runnable);// 啟動第一個線程thread1.start();// 啟動第二個線程thread2.start();// 等待第一個線程執行完畢thread1.join();// 等待第二個線程執行完畢thread2.join();// 輸出最終累加結果System.out.println(value);}/*** 實現 Runnable 接口的 run 方法,定義線程要執行的任務。* 在這個方法中,線程會對靜態變量 value 進行 10000 次累加操作。*/@Overridepublic void run() {// 循環 10000 次for (int i = 0; i < 10000; i++) {// 使用同步塊確保同一時間只有一個線程可以訪問和修改 value 變量synchronized (this) {// 對 value 變量進行累加操作value++;}}}
}

它與最開始的線程不安全的代碼的區別在于,在 run 方法中加了?synchronized 代碼塊,就可以非常輕松地解決這個問題,由于 synchronized 可以保證代碼塊內部的原子性,所以以上程序的運行結果也始終等于 20000,是線程安全的。

原子類和 synchronized 的使用對比

下面我們就對這兩種不同的方案進行分析。

第一點,我們來看一下它們背后原理的不同。

synchronized 保證線程安全的核心是?monitor 鎖,同步方法和同步代碼塊的背后原理會有少許差異,但總體思想是一致的:在執行同步代碼之前,需要首先獲取到 monitor 鎖,執行完畢后,再釋放鎖。而原子類保證線程安全的原理是利用了 CAS 操作。從這一點上看,雖然原子類和?synchronized 都能保證線程安全,但是其實現原理是大有不同的。

第二點不同是使用范圍的不同。

對于原子類而言,它的使用范圍是比較局限的。因為一個原子類僅僅是一個對象,不夠靈活。而synchronized 的使用范圍要廣泛得多。比如說 synchronized 既可以修飾一個方法,又可以修飾一段代碼,相當于可以根據我們的需要,非常靈活地去控制它的應用范圍。

所以僅有少量的場景,例如計數器等場景,我們可以使用原子類。而在其他更多的場景下,如果原子類不適用,那么我們就可以考慮用 synchronized 來解決這個問題。

第三個區別是粒度的區別。

原子變量的粒度是比較小的,它可以把競爭范圍縮小到變量級別。通常情況下,synchronized 鎖的粒度都要大于原子變量的粒度。如果我們只把一行代碼用 synchronized 給保護起來的話,有一點殺雞焉用牛刀的感覺。

第四點是它們性能的區別,同時也是悲觀鎖和樂觀鎖的區別。

因為 synchronized 是一種典型的悲觀鎖,而原子類恰恰相反,它利用的是樂觀鎖。所以,我們在比較synchronized 和 AtomicInteger 的時候,其實也就相當于比較了悲觀鎖和樂觀鎖的區別。

從性能上來考慮的話,悲觀鎖的操作相對來講是比較重量級的。因為 synchronized 在競爭激烈的情況下,會讓拿不到鎖的線程阻塞,而原子類是永遠不會讓線程阻塞的。不過,雖然 synchronized 會讓線程阻塞,但是這并不代表它的性能就比原子類差。

因為悲觀鎖的開銷是固定的,也是一勞永逸的。隨著時間的增加,這種開銷并不會線性增長。而樂觀鎖雖然在短期內的開銷不大,但是隨著時間的增加,它的開銷也是逐步上漲的。

所以從性能的角度考慮,它們沒有一個孰優孰劣的關系,而是要區分具體的使用場景。在競爭非常激烈的情況下,推薦使用 synchronized;而在競爭不激烈的情況下,使用原子類會得到更好的效果。

值得注意的是,synchronized 的性能隨著 JDK 的升級,也得到了不斷的優化。synchronized 會從無鎖升級到偏向鎖,再升級到輕量級鎖,最后才會升級到讓線程阻塞的重量級鎖。因此synchronized 在競爭不激烈的情況下,性能也是不錯的。

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

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

相關文章

207、【圖論】孤島的總面積

題目 思路 相比于 206、【圖論】島嶼數量&#xff0c;就是在這個代碼的基礎上。先遍歷邊界&#xff0c;將邊界連接的島嶼變為0&#xff0c;然后再計算一遍當前為1的島嶼面積。 代碼實現 import collectionsn, m list(map(int, input().split())) graph []for _ in range(n…

大模型最新面試題系列:微調篇之微調基礎知識

一、全參數微調&#xff08;Full-Finetune&#xff09; vs 參數高效微調&#xff08;PEFT&#xff09;對比 1. 顯存使用差異 全參數微調&#xff1a;需存儲所有參數的梯度&#xff08;如GPT-3 175B模型全量微調需約2.3TB顯存&#xff09;PEFT&#xff1a;以LoRA為例&#xff…

【GPT入門】第21課 langchain核心組件

【GPT入門】第21課 langchain核心組件 1. langchain 核心組件2.文檔加載器 Document loader3.文檔處理器3.1 langchain_text_splitters3.3 FAISS向量數據庫和向量檢索主要作用應用場景4. 對話歷史管理1. langchain 核心組件 模型 I/O 封裝 LLMs:大語言模型 Chat Models:一般…

應急響應靶機練習-Linux2

1.背景 前景需要&#xff1a;看監控的時候發現webshell告警&#xff0c;領導讓你上機檢查你可以救救安服仔嗎&#xff01;&#xff01; 挑戰內容&#xff1a; &#xff08;1&#xff09;提交攻擊者IP &#xff08;2&#xff09;提交攻擊者修改的管理員密碼(明文) &#xff08;…

分享一個免費的CKA認證學習資料

關于CKA考試 CKA&#xff08;Certified Kubernetes Administrator&#xff09;是CNCF基金會&#xff08;Cloud Native Computing Foundation&#xff09;官方推出的Kubernetes管理員認證計劃&#xff0c;用于證明持有人有履行Kubernetes管理的知識&#xff0c;技能等相關的能力…

【PTA題目解答】7-2 簡化的插入排序(15分)

1.題目 本題要求編寫程序&#xff0c;將一個給定的整數插到原本有序的整數序列中&#xff0c;使結果序列仍然有序。 輸入格式&#xff1a; 輸入在第一行先給出非負整數N&#xff08;<10&#xff09;&#xff1b;第二行給出N個從小到大排好順序的整數&#xff1b;第三行給…

【最新】 ubuntu24安裝 1panel 保姆級教程

系統&#xff1a;ubuntu24.04.1 安裝軟件 &#xff1a;1panel 第一步&#xff1a;更新系統 sudo apt update sudo apt upgrade 如下圖 第二步&#xff1a;安裝1panel&#xff0c;運行如下命令 curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o …

UE4-UE5虛幻引擎,前置學習一--Console日志輸出經常崩潰,有什么好的解決辦法

有些差異 這么牛逼的引擎&#xff0c;居然有這種入門級別的問題&#xff0c;一觸發清理&#xff0c;大概率(80%)會崩潰 無論虛幻5還是UE4都有這個問題&#xff0c;挺煩人的 實在忍不了了&#xff0c;這次&#xff0c;今天 就想問問有什么好的處理方法么&#xff1f;&#x…

【微服務】Nacos 配置動態刷新(簡易版)(附配置)

文章目錄 1、實現方法2、配置依賴 yaml3、驗證效果 1、實現方法 環境&#xff1a;Nacos、Java、SpringBoot等 主要是在boostrap.yaml中的data-id屬性下配置refresh:true來實現動態更新 2、配置依賴 yaml 具體的版本參考官方的說明&#xff1a;官方版本說明 <!--讀取boo…

設計模式之備忘錄設計模式

備忘錄設計模式&#xff08;Memento Pattern&#xff09; 在不破壞封閉的前提下&#xff0c;捕獲一個對象的內部狀態&#xff0c;保存對象的某個狀態&#xff0c;以便在適當的時候恢復對象&#xff0c;又叫做快照模式&#xff0c;屬于行為模式備忘錄模式實現的方式需要保證被保…

pytest 框架學習總結

視頻&#xff1a;pytest01-快速上手_嗶哩嗶哩_bilibili 資料&#xff1a;pytest 框架 - 白月黑羽 基于 Python 語言的自動化測試框架 最知名的 有如下 3 款unittest、pytest、robotframework 前兩款框架主要&#xff08;或者說很大程度上&#xff09;是 聚焦 在 白盒單元測試…

Day16:最小的k個數

倉庫管理員以數組 stock 形式記錄商品庫存表&#xff0c;其中 stock[i] 表示對應商品庫存余量。請返回庫存余量最少的 cnt 個商品余量&#xff0c;返回 順序不限。 示例 1&#xff1a; 輸入&#xff1a;stock [2,5,7,4], cnt 1 輸出&#xff1a;[2]示例 2&#xff1a; 輸入…

【最后203篇系列】016 Q201架構思考

前言 Q200已經達到了我既定的目標&#xff0c;在最近的3個月&#xff0c;我需要進一步完善&#xff0c;達到可以試產的程度。 在這個過程當中&#xff0c;許多知識和體會一直在變。 qtv200到目前&#xff0c;雖然通過習慣(每晚運行離線策略和比對)方式維持了注意力的集中&…

音視頻入門基礎:RTP專題(20)——通過FFprobe顯示RTP流每個packet的信息

通過FFprobe命令&#xff1a; ffprobe -protocol_whitelist "file,rtp,udp" -of json -show_packets XXX.sdp 可以顯示SDP描述的RTP流每個packet&#xff08;數據包&#xff09;的信息&#xff1a; 對于RTP流&#xff0c;上述的“packet”&#xff08;數據包&#…

信息系統運行管理員教程6--信息系統安全

信息系統運行管理員教程6–信息系統安全 第1節 信息系統安全概述 1.信息系統安全的概念 信息系統安全是指保障計算機及其相關設備、設施&#xff08;含網絡&#xff09;的安全&#xff0c;運行環境的安全&#xff0c;信息的安全&#xff0c;實現信息系統的正常運行。信息系統…

LLM后訓練:解鎖大型語言模型推理能力的關鍵路徑

引言&#xff1a;從語言生成到邏輯推理的躍遷 大型語言模型&#xff08;LLMs&#xff09;通過預訓練掌握了海量語言模式&#xff0c;但其核心缺陷——幻覺、邏輯斷裂、價值觀偏差——暴露了單純預訓練的局限性。后訓練&#xff08;Post-Training&#xff09;作為預訓練后的精修…

9.貪心算法

簡單貪心 1.P10452 貨倉選址 - 洛谷 #include<iostream> #include<algorithm> using namespace std;typedef long long LL; const int N 1e510; LL a[N]; LL n;int main() {cin>>n;for(int i 1;i < n;i)cin>>a[i];sort(a1,a1n);//排序 LL sum 0…

Linux 網絡:skb 數據管理

文章目錄 1. 前言2. skb 數據管理2.1 初始化2.2 數據的插入2.2.1 在頭部插入數據2.2.2 在尾部插入數據 2.2 數據的移除 3. 小結 1. 前言 限于作者能力水平&#xff0c;本文可能存在謬誤&#xff0c;因此而給讀者帶來的損失&#xff0c;作者不做任何承諾。 2. skb 數據管理 數…

批量給 Excel 添加或刪除密碼保護|Excel 批量設置打開密碼和只讀密碼

我們在將 Excel 文檔發送給第三方或者進行存檔的時候&#xff0c;對 Excel 文檔添加密碼保護是非常重要的一個操作。添加保護后的 Excel 文檔。就只能有相應權限的用戶才能夠打開或者編輯操作。尤其是當我們 Excel 文檔中內容非常敏感非常重要的時候&#xff0c;添加保護就顯得…

藍耘MaaS平臺:阿里QWQ應用拓展與調參實踐

摘要&#xff1a;本文深入探討了藍耘MaaS平臺與阿里QWQ模型的結合&#xff0c;從平臺架構、模型特點到應用拓展和調參實踐進行了全面分析。藍耘平臺憑借其強大的算力支持、彈性資源調度和全棧服務&#xff0c;為QWQ模型的高效部署提供了理想環境。通過細化語義描述、調整推理參…