【JavaEE精煉寶庫】多線程進階(2)synchronized原理、JUC類——深度理解多線程編程

一、synchronized 原理

1.1 基本特點:

結合上面的鎖策略,我們就可以總結出,synchronized 具有以下特性(只考慮 JDK 1.8):

  • 開始時是樂觀鎖,如果鎖沖突頻繁,就轉換為悲觀鎖。

  • 開始是輕量級鎖實現,如果鎖被持有的時間較長,就轉換成重量級鎖。

  • 實現輕量級鎖的時候大概率用到的自旋鎖策略。

  • 是一種不公平鎖。

  • 是一種可重入鎖。

  • 不是讀寫鎖。

1.2 加鎖工作過程:

JVM 將 synchronized 鎖分為無鎖、偏向鎖、輕量級鎖、重量級鎖狀態。會根據情況,進行依次升級。
在這里插入圖片描述

1.2.1 偏向鎖:

第一個嘗試加鎖的線程,優先進入偏向鎖狀態。 偏向鎖不是真的 “加鎖”,只是給對象頭中做一個 “偏向鎖的標記”,記錄這個鎖屬于哪個線程。如果后續沒有其他線程來競爭該鎖,那么就不用進行其他同步操作了(避免了加鎖解鎖的開銷)。如果后續有其他線程來競爭該鎖(剛才已經在鎖對象中記錄了當前鎖屬于哪個線程了,很容易識別當前申請鎖的線程是不是之前記錄的線程),那就取消原來的偏向鎖狀態,進入一般的輕量級鎖狀態。偏向鎖本質上相當于 “延遲加鎖”。能不加鎖就不加鎖,盡量來避免不必要的加鎖開銷。但是該做的標記還是得做的,否則無法區分何時需要真正加鎖。

1.2.2 輕量級鎖:

隨著其他線程進入競爭,偏向鎖狀態被消除,進入輕量級鎖狀態(自適應的自旋鎖)。此處的輕量級鎖就是通過 CAS 來實現。通過 CAS 檢查并更新一塊內存(比如 null => 該線程引用)如果更新成功,則認為加鎖成功,如果更新失敗,則認為鎖被占用,繼續自旋式的等待(并不放棄CPU)。

自旋操作是一直讓 CPU 空轉,比較浪費 CPU 資源。因此此處的自旋不會一直持續進行,而是達到一定的時間 / 重試次數,就不再自旋了。也就是所謂的 “自適應” 。

1.2.3 重量級鎖:

如果競爭進一步激烈,自旋不能快速獲取到鎖狀態,就會膨脹為重量級鎖。此處的重量級鎖就是指用到內核提供的 mutex 。

執行加鎖操作,先進入內核態,在內核態判定當前鎖是否已經被占用,如果該鎖沒有占用,則加鎖成功,并切換回用戶態,如果該鎖被占用,則加鎖失敗。此時線程進入鎖的等待隊列,掛起等待被操作系統喚醒。經歷了一系列的滄海桑田,這個鎖被其他線程釋放了,操作系統也想起了這個被掛起的線程,于是喚醒這個線程,嘗試重新獲取鎖。

1.3 鎖消除:

編譯器 + JVM 判斷鎖是否可消除。 如果可以,就直接消除。

鎖消除常常運用在:有些應用程序的代碼中,用到了 synchronized,但其實沒有在多線程環境下。(例如 StringBuffer)

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此時每個 append 的調用都會涉及加鎖和解鎖,但如果只是在單線程中執行這個代碼,那么這些加鎖解鎖操作是沒有必要的,白白浪費了一些資源開銷。

1.4 鎖粗化:

一段邏輯中如果出現多次加鎖解鎖,編譯器 + JVM 會自動進行鎖的粗化。 鎖粗化這里涉及一個概念粒度(不是力度):加鎖的范圍內,包含代碼的多少,包含的代碼越多,就認為鎖的粒度就越粗,反之,鎖的粒度就越細。

綜上可以看到,synchronized 的策略是比較復雜的,在背后做了很多事情,目的為了讓程序猿哪怕啥都不懂,也不至于寫出特別慢的程序。JVM 開發者為了 Java 程序猿操碎了心。

1.5 面試題:

  1. 什么是偏向鎖?

答:偏向鎖不是真的加鎖,而只是在鎖的對象頭中記錄?個標記(記錄該鎖所屬的線程)。如果沒有其他線程參與競爭鎖,那么就不會真正執行加鎖操作,從而降低程序開銷。?旦真的涉及到其他的線程競爭,再取消偏向鎖狀態,進入輕量級鎖狀態。

  1. synchronized 實現原理是什么?

答:剛開始是一個標記,遇到所沖突升級成輕量級鎖,采用自旋鎖的方式實現,隨著鎖沖突的升級,鎖升級為重量級鎖,采用掛起等待鎖的方式,來實現鎖。

二、JUC(java.util.concurrent)的常見類

在 java.util.concurrent 中放了和多線程相關的組件。

2.1 Callable 接口:

Callable 是?個接口,相當于把線程封裝了?個 “返回值”。方便程序猿借助多線程的方式計算結果。可以認為是一個帶返回參數的 runnable 。里面要重寫的方法是 call( )。

  • 理解 Callable 和 FutureTask:

Callable 和 Runnable 相對,都是描述一個 “任務”。Callable 描述的是帶有返回值的任務,Runnable
描述的是不帶返回值的任務。Callable 通常需要搭配 FutureTask 來使用 FutureTask 用來保存 Callable 的返回結果。因為 Callable 往往是在另?個線程中執行的,啥時候執行完并不確定。FutureTask 就可以負責這個等待結果出來的工作(如果在 futureTask.get()線程還沒執行完畢就會阻塞等待)。

FutureTask 可以直接傳入 Thread 的構造方法當中,于是我們掌握的 Thread 的構造方式又多了一種。

我們可以將它們的關系理解成吃麻辣燙的情形:去吃麻辣燙,Callable 就是菜籃,重寫的 call 方法里面就是點的菜,當餐點好后,前臺會給你一張 “小票” ,后廚開始工作(Thread 啟動)。這個小票就是 FutureTask,后面我們可以隨時憑這張小票去查看自己的這份麻辣燙做出來了沒有(線程是否執行完畢)。

演示案例:創建線程計算 1 + 2 + 3 + … + 1000,使用 Callable 版本。

import java.util.concurrent.*;
public class demo2 {public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<Integer> callable = new Callable<Integer>() {//菜籃子int result = 0;@Overridepublic Integer call() throws Exception {//菜for(int i = 1;i <= 1000;i++){result += i;}return result;}};FutureTask<Integer> futureTask = new FutureTask<>(callable);//小票Thread t1 = new Thread(futureTask);//后廚t1.start();//后廚開始工作t1.join();System.out.println(futureTask.get());//小票取餐}
}

案例演示效果如下:

在這里插入圖片描述

2.2 ReentrantLock:

顧名思義:可重入互斥鎖和 synchronized 定位類似,都是用來實現互斥效果,保證線程安全。

  • ReentrantLock 的用法:
  1. lock():加鎖,如果獲取不到鎖就死等。
  2. trylock(超時時間):加鎖,如果獲取不到鎖,等待?定的時間之后就放棄加鎖。
  3. unlock():解鎖。

隨著版本的升級 synchronized 越來越好用了,ReentrantLock 就漸漸的用的少了,但是這里我們還是要學習,就說明其相對于 synchronized 有著一些特有的優勢:

  • ReentrantLock 與 synchronized 的區別:
  1. synchronized 是一個關鍵字,是 JVM 內部實現的(大概率是基于 C++ 實現)。ReentrantLock 是標準庫的一個類,在 JVM 外實現的(基于 Java 實現)。
  2. synchronized 使用時不需要手動釋放鎖。ReentrantLock 使用時需要手動釋放,使用起來更靈活但是也容易遺漏 unlock。
  3. synchronized 在申請鎖失敗時,會死等。ReentrantLock 可以通過 trylock 的方式等待一段時間就放棄。
  4. synchronized 是非公平鎖,ReentrantLock 默認是非公平鎖,可以通過構造方法傳入一個 true 開啟公平鎖模式。
  5. 更強大的喚醒機制。synchronized 是通過 Object類 的 wait / notify 實現等待-喚醒。每次喚醒的是一個隨機等待的線程。ReentrantLock 搭配 Condition 類實現等待-喚醒,可以更精確控制喚醒某個指定的線程。

如何選擇使用哪個鎖?

  1. 鎖競爭不激烈的時候,使用 synchronized,效率更高,自動釋放更方便。
  2. 鎖競爭激烈的時候,使用 ReentrantLock,搭配 trylock 更靈活控制加鎖的行為,而不是死等。
  3. 如果需要使用公平鎖,使用 ReentrantLock。

2.3 原子類:

原子類內部用的是 CAS 實現,所以性能要比加鎖實現 i++ 高很多。原子類有以下幾個:
在這里插入圖片描述

由于此模塊在上一章多線程進階(1)的 CAS 的應用中已經詳細解釋,這里就不再贅述。

2.4 Semaphore 信號量:

信號量,用來表示 “可用資源的個數”。本質上就是一個計數器。

  • 理解信號量:

可以把信號量想象成是停車場的展示牌:當前有車位 100 個。表示有 100個可用資源。當有車開進去的時候,就相當于申請一個可用資源,可用車位就 -1(這個稱為信號量的 P操作)當有車開出來的時候,就相當于釋放一個可用資源,可用車位就 +1(這個稱為信號量的 V 操作)如果計數器的值已經為 0了,還嘗試申請資源,就會阻塞等待,直到有其他線程釋放資源。

Semaphore 的 PV 操作中的加減計數器操作都是原子的,可以在多線程環境下直接使用。

  • 代碼案例演示:
import java.util.concurrent.*;
public class demo4 {public static void main(String[] args) {Semaphore semaphore = new Semaphore(1);Runnable runnable = new Runnable() {@Overridepublic void run() {try {semaphore.acquire();System.out.println(Thread.currentThread().getName() + "獲取到資源");} catch (InterruptedException e) {throw new RuntimeException(e);};try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + "釋放資源");semaphore.release();}};Thread t1 = new Thread(runnable);Thread t2 = new Thread(runnable);t1.start();t2.start();}
}
  • 案例演示結果如下:

只有在資源還有剩余的情況下進行 acquire 才不會進行阻塞。
在這里插入圖片描述

在信號量為 1 是可以將其當作鎖來使用。鎖可以看成是 semaphore 的特例。

2.5 CountDownLatch:

同時等待 N 個任務執行結束。

當我們把一個任務拆分成很多個的時候,可以通過這個工具類來識別任務是否整體執行完畢了。

使用過程:構造 CountDownLatch 實例,初始化 10 表示有 10 個任務需要完成。每個任務執行完畢,都調用 latch.countDown() 。在 CountDownLatch 內部的計數器同時自減,主線程中使用latch.await();阻塞等待。所有任務執行完畢,相當于計數器為 0 了,解除阻塞。好處是如果是多個線程不用寫多個 join()。

案例演示:

import java.util.concurrent.CountDownLatch;
public class demo5 {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(2);Runnable runnable = new Runnable() {@Overridepublic void run() {try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + "執行完畢");latch.countDown();}};Thread t1 = new Thread(runnable);Thread t2 = new Thread(runnable);t1.start();t2.start();latch.await();System.out.println("Main 線程執行完畢");}
}

效果如下:

Main 線程會等待 Thread 1,2 執行完畢后,再繼續執行。
在這里插入圖片描述

三、線程安全的集合類

原來的集合類,大部分都不是線程安全的。(Vector,Stack,HashTable,是線程安全的(不建議用),其他的集合類不是線程安全的)。

3.1 多線程環境使用 ArrayList:

  1. 自己使用同步機制(synchronized 或者 ReentrantLock)

  2. Collections.synchronizedList(new ArrayList):synchronizedList 是標準庫提供的一個基于 synchronized 進行線程同步的 List.synchronizedList 的關鍵操作上都帶有 synchronized。

  3. 使用 CopyOnWriteArrayList:當我們往一個容器中添加元素的時候,不直接往當前容器添加,而是先將當前容器進行 Copy,復制出一個新的容器,然后在新的容器里面添加元素,添加完元素之后,再將原容器的引用指向新的容器。
    這樣做的好處是我們可以對 CopyOnWrite 容器進行并發的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以 CopyOnWrite 容器也是一種讀寫分離的思想,讀和寫不同的容器。
    優點:在讀多寫少的場景下,性能很高,不需要加鎖競爭。
    缺點:1. 占用內存較多。2. 新寫的數據不能被第一時間讀取到。

3.2 多線程環境使用隊列:

  • ArrayBlockingQueue:基于數組實現的阻塞隊列。

  • LinkedBlockingQueue:基于鏈表實現的阻塞隊列。

  • PriorityBlockingQueue:基于堆實現的帶優先級的阻塞隊列。

  • TransferQueue:最多只包含一個元素的阻塞隊列。

3.3 多線程環境使用哈希表:

HashMap 本身不是線程安全的。在多線程環境下使用哈希表可以使用:Hashtable(不推薦使用),ConcurrentHashMap(推薦使用這個)。

3.3.1 Hashtable:

只是簡單的把關鍵方法加上了 synchronized 關鍵字。
在這里插入圖片描述

這相當于直接針對 Hashtable 對象本省加鎖。不推薦使用這個的原因如下:

  1. 如果多線程訪問同?個 Hashtable 就會直接造成鎖沖突(沖突率太高了)。

  2. size 屬性也是通過 synchronized 來控制同步,也是比較慢的。

  3. ?旦觸發擴容,就由該線程完成整個擴容過程。這個過程會涉及到大量的元素拷貝,效率會非常低。

在這里插入圖片描述

3.3.2 ConcurrentHashMap:

相比于 Hashtable 做出了一系列的改進和優化。以 Java1.8 為例:

  • 讀操作沒有加鎖(但是使用了 volatile 保證從內存讀取結果),只對寫操作進行加鎖,加鎖的方式仍然是用 synchronized,但不是鎖整個對象,而是 “鎖桶” (用每個鏈表的頭結點作為鎖對象))大大降低了鎖沖突的概率。

  • 充分利用 CAS 特性。比如 size 屬性通過 CAS 來更新。避免出現重量級鎖的情況。

  • 優化了擴容方式:化整為零。

發現需要擴容的線程,只需要創建?個新的數組,同時只搬幾個元素過去。擴容期間,新老數組同時存在。后續每個來操作 ConcurrentHashMap 的線程,都會參與搬家的過程。每個操作負責搬運一小部分元素。搬完最后一個元素再把老數組刪掉。這個期間,插入只往新數組中加。這個期間,查找需要同時查新數組和老數組。
在這里插入圖片描述

結語:
其實寫博客不僅僅是為了教大家,同時這也有利于我鞏固知識點,和做一個學習的總結,由于作者水平有限,對文章有任何問題還請指出,非常感謝。如果大家有所收獲的話還請不要吝嗇你們的點贊收藏和關注,這可以激勵我寫出更加優秀的文章。

在這里插入圖片描述

?

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

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

相關文章

廣州外貿建站模板

Yamal外貿獨立站wordpress主題 綠色的亞馬爾Yamal外貿獨立站wordpress模板&#xff0c;適用于外貿公司建獨立站的wordpress主題。 https://www.jianzhanpress.com/?p7066 賽斯科Sesko-W外貿建站WP主題 適合機械設備生產廠家出海做外貿官網的wordpress主題&#xff0c;紅橙色…

Dify自定義工具例子

1.天氣&#xff08;JSON&#xff09; {"openapi": "3.1.0","info": {"title": "Get weather data","description": "Retrieves current weather data for a location.","version": "v1…

動態規劃——打家劫舍(C++)

好像&#xff0c;自己讀的書確實有點少了。 ——2024年7月2日 198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 題目描述 你是一個專業的小偷&#xff0c;計劃偷竊沿街的房屋。每間房內都藏有一定的現金&#xff0c;影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連…

Linux 靜態庫和動態庫

不管是Linux還是Windows中的庫文件其本質和工作模式都是相同的, 只不過在不同的平臺上庫對應的文件格式和文件后綴不同。程序中調用的庫有兩種 靜態庫和動態庫&#xff0c;不管是哪種庫文件本質是還是源文件&#xff0c;只不過是二進制格式只有計算機能夠識別&#xff0c;作為一…

【Node-RED 4.0.2】4.0版本新增特性(官方版)

二、重要功能 *1.時間戳格式改進 過去&#xff0c;node-red 只提供了 最原始的 timestamp 的格式&#xff08;1970-01-01 ~ now&#xff09; 但是現在&#xff0c;額外增加了 2 種格式&#xff1a; ISO 8601 -A COMMON FORMAT&#xff08;YYYY-MM-DDTHH:mm:ss:sssZ&#xff…

思考如何學習一門編程語言?

一、什么是編程語言 編程語言是一種用于編寫計算機程序的人工語言。通過編程語言&#xff0c;程序員可以向計算機發出指令&#xff0c;控制計算機執行各種任務和操作。編程語言由一組語法規則和語義規則組成&#xff0c;這些規則定義了如何編寫代碼以及代碼的含義。 編程語言…

linux和mysql基礎指令

Linux中nano和vim讀可以打開記事文件。 ifdown ens33 ifup ens33 關閉&#xff0c;開啟網絡 rm -r lesson1 gcc -o code1 code1.c 編譯c語言代碼 ./code1 執行c語言代碼 rm -r dir 刪除文件夾 mysql> show databases-> ^C mysql> show databases; -------…

常見網絡端口號

在網絡工程領域&#xff0c;了解和掌握默認端口號是至關重要的。端口號是計算機網絡中最基本的概念之 一&#xff0c;用于標識特定的網絡服務或應用程序。 1、什么是端口號&#xff1f; 端口號是計算機網絡中的一種標識&#xff0c;用于區分不同的網絡服務和應用程序。每個端…

【C++進階學習】第五彈——二叉搜索樹——二叉樹進階及set和map的鋪墊

二叉樹1&#xff1a;深入理解數據結構第一彈——二叉樹&#xff08;1&#xff09;——堆-CSDN博客 二叉樹2&#xff1a;深入理解數據結構第三彈——二叉樹&#xff08;3&#xff09;——二叉樹的基本結構與操作-CSDN博客 二叉樹3&#xff1a;深入理解數據結構第三彈——二叉樹…

想要打造超高性能的接口API?試試這12條小技巧。

1. 并行處理 簡要說明 舉個例子&#xff1a;在價格查詢鏈路中&#xff0c;我們需要獲取多種獨立的價格配置項信息&#xff0c;如基礎價、折扣價、商戶活動價、平臺活動價等等。 CompletableFuture 是銀彈嗎&#xff1f; 使用 CompletableFuture 的確能夠幫助我們解決許多獨…

走進IT的世界

引言 隨著高考的結束&#xff0c;對于即將踏入IT&#xff08;信息技術&#xff09;領域的新生而言&#xff0c;這個假期不僅是放松身心的時間&#xff0c;更是提前規劃、深化專業知識、為大學生活奠定堅實基礎的寶貴機會。以下是一份詳盡的高考假期預習與規劃指南&#xff0c;…

Android自動化測試實踐:uiautomator2 核心功能與應用指南

Android自動化測試實踐&#xff1a;uiautomator2 核心功能與應用指南 uiautomator2 是一個用于Android應用的自動化測試Python庫&#xff0c;支持多設備并行測試操作。它提供了豐富的API來模擬用戶對App的各種操作&#xff0c;如安裝、卸載、啟動、停止以及清除應用數據等。此外…

30個!2024重大科學問題、工程技術難題和產業技術問題發布

【SciencePub學術】中國科協自2018年開始&#xff0c;組織開展重大科技問題難題征集發布活動&#xff0c;引導廣大科技工作者緊跟世界科技發展大勢&#xff0c;聚焦國家重大需求&#xff0c;開展原創性、引領性研究&#xff0c;不斷夯實高質量發展的科技支撐。 自2024年征集活動…

飛書文檔轉markdown 超級快捷方法。

直接使用那個github的高贊官方的工具轉換&#xff0c;需要設置什么小應用那種東西&#xff0c;還要審批&#xff0c;社恐人表示怕了怕了。而且文檔我分享出去&#xff0c;是有權限的&#xff0c;反正無論如何生成不了 我的方法是 直接全選&#xff0c;然后粘貼進Arya - 在線 …

C#的五大設計原則-solid原則

什么是C#的五大設計原則&#xff0c;我們用人話來解釋一下&#xff0c;希望小伙伴們能學會&#xff1a; 好的&#xff0c;讓我們以一種幽默的方式來解釋C#的五大設計原則&#xff08;SOLID&#xff09;&#xff1a; 單一職責原則&#xff08;Single Responsibility Principle…

PCL 漸進形態過濾器實現地面分割

點云地面分割 一、代碼實現二、結果示例?? 概述 漸進形態過濾器:采用先腐蝕后膨脹的運算過程,可以有效濾除場景中的建筑物、植被、車輛、行人以及交通附屬設施,保留道路路面及路緣石點云。 一、代碼實現 #include <iostream> #include <pcl/io/pcd_io.h> #in…

【LeetCode】976. 三角形的最大周長

1. 題目 2. 分析 需要分析好再動手編程。 如果要構成三角形的最大周長&#xff0c;那么就需要盡可能用最長的邊構建。所以可以先對數組排個序&#xff0c;然后基于排序得到的結果從大往小的逐個檢查長度為3的窗口&#xff0c;判斷該窗口的值是否滿足三角形的構成條件&#x…

鴻蒙開發Ability Kit(程序訪問控制):【安全控件概述】

安全控件概述 安全控件是系統提供的一組系統實現的ArkUI組件&#xff0c;應用集成這類組件就可以實現在用戶點擊后自動授權&#xff0c;而無需彈窗授權。它們可以作為一種“特殊的按鈕”融入應用頁面&#xff0c;實現用戶點擊即許可的設計思路。 相較于動態申請權限的方式&am…

構造,析構,拷貝【類和對象(中)】

P. S.&#xff1a;以下代碼均在VS2019環境下測試&#xff0c;不代表所有編譯器均可通過。 P. S.&#xff1a;測試代碼均未展示頭文件stdio.h的聲明&#xff0c;使用時請自行添加。 博主主頁&#xff1a;LiUEEEEE ??????????????????? ?? …

Excel_VBA編程

在Excel中&#xff0c;VBA&#xff08;Visual Basic for Applications&#xff09;是一種強大的工具&#xff0c;可以用來自動化各種任務。下面介紹一些常用的VBA函數和程序結構&#xff1a; 常用函數 MsgBox&#xff1a;用于顯示消息框。 MsgBox "Hello, World!"In…