【Java ee初階】多線程(4)

一、java是怎么做到可重入的

java中,通過synchronized進行加鎖,指定一個()包含了一個鎖對象。(鎖對象本身是一個啥樣的對象,這并不重要,重點關注鎖對象是不是同一個對象)

后面搭配{}.進入遇到{就觸發加鎖操作 遇到 } 就觸發解鎖操作 防止解鎖操作被遺忘

如果一個線程加鎖,一個線程不加鎖;一個線程針對locker1加鎖,一個線程針對locker2加鎖......

鎖相當于都不會產生沖突,不會產生阻塞。

二、synchronized的特性

1.互斥

2.可重入 一個線程,一把鎖,這個線程針對這個鎖,連續加鎖兩次

synchronized(locker){

synchronized(locker)

}

locker已經是被加鎖的狀態了.嘗試對一個已經上了鎖進行加鎖,就會產生阻塞

此處阻塞的接觸,需要先釋放第一次鎖

要想釋放第一次加鎖,需要先加上第二次的鎖

一個線程針對一把鎖,連續加鎖多次,不會觸發死鎖——>可重入

可重入這個現象是如何做到的呢?

讓鎖對象本身,記錄下來擁有者是哪個線程(把線程id給保存下來了)

Object...Java的對象,除了又一個內存區域,保存程序員自定義的成員之外,還有一個隱藏區域,用來保存“對象頭”。

對象頭是JVM去維護的,保存了這個對象的一些其他運行信息,例如,加鎖狀態,哪個線程加了鎖等等。

當我們已經給一個對象加鎖了,后序再去針對這個對象加鎖,那么就會先判定,當前嘗試加鎖的線程,是不是已經持有這個鎖的線程。如果沒有,才觸發阻塞,如果有,不觸發阻塞,直接放行。

二、死鎖的情況

可重入鎖,只能處理死鎖的其中一種情況,沒辦法處理其他情況

1.一個線程一把鎖,連續加鎖兩次

2.兩個線程兩把鎖,每個線程先獲得一把鎖,再嘗試獲取對方的鎖

package Thread;public class demo24 {private static Object locker1 = new Object(); private static Object locker2 = new Object(); public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> { synchronized (locker1) { System.out.println("t1拿到了locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}synchronized (locker2) { System.out.println("t1拿到了locker2");  }}
});Thread t2 = new Thread(() -> {synchronized (locker2) { System.out.println("t2拿到了locker2");try {Thread.sleep(1000);}   catch (InterruptedException e) {}synchronized (locker1) { System.out.println("t2拿到了locker1");}  }   });t1.start();t2.start();t1.join(); // 等待t1線程執行完畢,才能繼續執行后面的代碼t2.join(); // 等待t2線程執行完畢,才能繼續執行后面的代碼}}

輸出:

3.N個線程M把鎖,也會構成死鎖

“哲學家就餐問題”

三、如何避免死鎖的出現

死鎖這樣的情況就是會客觀發生的,線程一旦出現死鎖,線程就卡死了,不動了,后序的邏輯就無法正常執行了,這是bug

如何避免代碼中出現死鎖呢?

關鍵在于理解死鎖的“四個必要條件”

1.鎖是互斥的——我們現在正在學習的synchronized是互斥的

2.鎖不可被搶占——線程1拿到鎖之后,線程2也想要這個鎖,線程2會阻塞等待,而不是直接把鎖搶過來

(對于synchronized來說,條件1和條件2 都是synchronized的基本特點)

3.請求和保持——拿到第一把鎖的情況下,不去釋放第一把鎖,再嘗試請求第二把鎖(*確實有一定的場景是需要拿到鎖1 的前提下再嘗試去拿鎖2)

4.循環等待——等待鎖釋放,等待的關系(順序)構成了循環

(*也就是不要讓等待關系構成循環 針對鎖進行編號

;約定,加多個鎖的時候,必須按照一定的順序來加鎖,比如按照編號從小到大的順序)

上述兩種是開發中比較實用的方法,還有一些其他的方案,也能解決死鎖問題。

package Thread;public class demo24 {private static Object locker1 = new Object(); private static Object locker2 = new Object(); public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> { synchronized (locker1) { System.out.println("t1拿到了locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}synchronized (locker2) { System.out.println("t1拿到了locker2");  }
});Thread t2 = new Thread(() -> {synchronized (locker2) { System.out.println("t2拿到了locker2");try {Thread.sleep(1000);}   catch (InterruptedException e) {}}   //把第二把鎖的加鎖操作放到第一把鎖的外面,先釋放第一把鎖,再獲取第二把鎖,這樣就不會出現死鎖的情況了。synchronized (locker1) { System.out.println("t2拿到了locker1");}  });t1.start();t2.start();t1.join(); // 等待t1線程執行完畢,才能繼續執行后面的代碼t2.join(); // 等待t2線程執行完畢,才能繼續執行后面的代碼}}

四、Java 標準庫中的線程安全類

Java 標準庫中很多都是線程不安全的. 這些類可能會涉及到多線程修改共享數據, 也沒有任何加鎖措施

這些常用的集合類,大多是線程不安全的,把加鎖策略交給程序員

但是還有?些是線程安全的. 使用了一些鎖機制來進行控制

其中 Vector 和 HashTable 是Java早年間起,各位java大佬還不夠成熟的時候引入的設定

現在的話這些設定已經被推翻,不建議再使用

有的雖然沒有加鎖, 但是不涉及 "修改", 仍然是線程安全的

*解決線程安全問題,我們使用加鎖的方式。但是加鎖是有代價的,加鎖會非常明顯地影響到程序的執行效率。加鎖意味著可能觸發鎖競爭,一旦觸發競爭就會產生阻塞。某個線程一旦因為加鎖阻塞,能回來繼續執行任務的時間就不確定了。寫代碼的時候需要考慮清楚某個地方是否要加鎖。

五、內存可見性引起的線程安全問題

package Thread;public class Demo15 {public static int count = 0; // 共享變量,多個線程共同修改的變量,稱為共享變量public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> { // 線程t1for (int i = 0; i < 5000; i++) { // 循環5000次count++; // 自增操作,相當于count = count + 1}});Thread t2 = new Thread(() -> { // 線程t2for (int i = 0; i < 5000; i++) { // 循環5000次count++; // 自增操作,相當于count = count + 1}});t1.start(); // 啟動線程t1t2.start(); // 啟動線程t2// 等待線程t1和線程t2執行完畢t1.join(); // 等待線程t1執行完畢t2.join(); // 等待線程t2執行完畢System.out.println(count); // 打印count的值,應該是10000,因為每個線程都自增了5000次}}

這個問題產生的原因,就是“內存可見性”

flag變量的修改,對于t1線程“不可見了”,t2修改了flag,但是t1看不見

編譯器優化

主流編程語言,編譯器的設計者(對于Java來說,談到的編譯器包括javac和jvm)考慮到一個問題:實際上寫代碼的程序員,水平是參差不齊的(具有一定的差距)

雖然有的程序員水平不高,寫的代碼效率比較低,編譯器在編譯執行的時候,分析理解現有代碼的意圖和效果,然后自動對這個代碼進行調整和優化,在確保程序執行邏輯不變的前提下,提高程序的效率。

編譯器優化的效果是很明顯,但是大前提是“程序的邏輯不變”

大多數情況下,編譯器優化,都可以做到“邏輯不變的前提”

但是在有些特定場景下,編譯器優化可能出現“誤判”,導致邏輯發生改變。

“多線程代碼”

對于這個程序來說,編譯器看到的效果是:有一個變量flag,會快速地,反復地讀取整個內存的值(反復執行load\cmp\load\cmp);同時,反復執行的過程中,每次拿到的flag的值還都是一樣的,上述的load操作相比cmp,耗時會多很多,讀取內存,比讀取寄存器,效率會慢很多(幾百倍,幾千倍)

既然load讀取的值都是一樣的,而且load開銷這么多,于是編譯器直接把從內存讀取flag這個操作給優化掉了。上述操作只是前幾次讀內存,后面發現一樣,就干脆從讀好的寄存器中直接獲取這個flag的值,此時,循環的俠侶就大幅度地提升了。

編譯器不確定這里的flag修改代碼到底能不能執行,以及啥時候執行。

上述內存可見性問題,是編譯器優化機制,自身出現的bug。

六、volatile關鍵字

通過這個關鍵字,提醒編譯器,某個變量是“易變”的,此時就不要針對這個易變的變量進行上述優化。

給變量添加了volatile關鍵字,編譯器在看到volatile的時候,就會提醒JVM運行的時候不進行上述的優化。

在讀寫volatile變量的指令前后添加“內存屏障相關的指令”

JMM Java Memory Model

Java的內存模型

首先一個Java進程,會有一個“主內存”存儲空間,每個Java線程又會有自己的“工作內存”存儲空間

形如上述的代碼,t1進行flag變量的判定時,就會把flag的值從主內存,先讀取到工作內存,再用工作內存中的值進行判定。同時,t2對flag進行修改,修改的則是主內存的值,主內存的值的修改不會影響到t1的工作內存。

上述解釋,出自于Java的官方文檔

main memory(主內存)就是內存?

work memory (工作內存)相當于是打了個比方,本質上這一塊區域并不是內存,而是CPU的寄存器和CPU的緩存構成的統稱

Java自身是希望做成“跨平臺”,Java用戶不需要了解系統底層和硬件差異。Java的設計者是不希望用戶了解這些底層細節的。另一方面,不同的CPU底層結構也不一定相同。

拋開Java上下文不談,只關注操作系統和硬件,沒有上面“主內存”“工作內存”的說法的。

存儲數據,不只是有內存,還有外存(硬盤),還有cpu寄存器,cpu上還有緩存。

現代CPU都引入了緩存,CPU的緩存空間比寄存器要大,速度要比寄存器要慢,但是比起內存還是要快。

CPU的寄存器和緩存,就統稱為work memory?

越往上,速度就越快,空間就越小,成本就越高。

編譯器優化,就是把本來要從內存中讀取的值,優化成從寄存器中讀取。

可能是優化成從寄存器上讀取,也可能是優化成從L1緩存上讀取,也可能是優化成從L2緩存上讀取,也可能是優化成從L3緩存上讀取……(都沒有從內存上重新讀取,因此讀不到最新的修改之后的數值)

編譯器優化,并非是100%觸發,根據不同的代碼結構,可能產生出不同的優化效果(有優化/無優化/優化方式)

此處雖然沒有寫volatile,但是加了sleep也會使得上述程序不在優化。

因為:

1.循環速度大幅度降低了

2.有了sleep一次循環的瓶頸,就不是load,此時再優化load,就沒有什么用了。

3.sleep本身會觸發線程調度,調度過程觸發上下文切換。

volatile這個關鍵字,能夠解決內存可見性引起的線程安全問題,但是不具備原子性這樣的特點。

synchronized和volatile是兩個不同的維度,前者是兩個線程都修改,volatile是一個線程讀,另一個線程修改。

六、wait/notify

這兩個關鍵字是用來協調線程之間的執行順序的

兩個線程在運行的時候,都是希望持續運行下去的(不涉及結束)。但是兩個線程中的某些環節,我們希望能夠有一定的先后順序。

*線程執行本身是隨即調度的(順序不確定),join控制線程的結束順序

例如線程1 ,線程2

希望線程1 先執行完某個邏輯之后,再讓線程2去執行。

此時就可以讓線程2通過wait主動進行阻塞,讓線程1先參與調度,等線程1把對應的邏輯執行完了,就可以通過notify喚醒線程2.

另外,wait / notify 也能解決“線程餓死”的問題。

當線程1釋放鎖之后,其他線程就要競爭這個鎖(線程1 自身也可以重復參與到競爭中)

由于其他線程還要等待操作系統喚醒,此時線程1就是在cpu上執行,就有很大的可能性,“捷足先登”

不像死鎖,死鎖發生,就僵硬住,除非程序啟動,否則就會一直僵持。

線程餓死,沒那么嚴重,在線程1反復獲取幾次鎖之后,其他線程也是有機會拿到鎖的,但是其他線程拿到鎖的時間會延長,降低了程序的效率。

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

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

相關文章

LaTex、pdfLaTex、XeLaTex和luaLaTex的區別和聯系

之前一直搞不懂這些亂七八糟的Tex到底有啥區別&#xff0c;不同引擎不同編譯器換來換去&#xff0c;查了些資料又問了下AI&#xff0c;總算是搞懂了。 大概是這樣&#xff0c;很久以前有人寫了個Tex排版引擎&#xff0c;輸入一些代碼命令&#xff0c;輸出dvi文件&#xff08;設…

【Unity】一個UI框架例子

使用框架前置條件&#xff1a;調整腳本運行順序, Canvas掛載UIManager, Panel掛載對應的UIController、UI控件掛載UIControl。 UIManager:UI管理器&#xff0c;用于處理和管理各個UIController和UIControl的業務邏輯&#xff0c;掛載在Canvas上&#xff1b; UIController:界面層…

kalibr:相機模型

文章目錄 ??簡介Kalibr標定支持的相機模型及適用場景?? 針孔相機模型(Pinhole)?? 全向相機模型(Omnidirectional)?? 特殊模型?? 選型建議?? 注意事項??簡介 Kalibr作為多傳感器標定的重要工具,支持多種相機模型以適應不同光學特性的視覺傳感器。其核心相機…

今日行情明日機會——20250430

指數目前仍然在震蕩區間&#xff0c;等后續的方向選擇以及放量后的主線~ 2025年4月30日漲停主要行業方向分析 一、核心主線方向 機器人概念&#xff08;政策催化技術突破&#xff09; ? 漲停家數&#xff1a;18家。 ? 代表標的&#xff1a; ? 全筑股份&#xff08;工業機器…

量子加密通信:打造未來信息安全的“銅墻鐵壁”

在數字化時代&#xff0c;信息安全已成為全球關注的焦點。隨著量子計算技術的飛速發展&#xff0c;傳統的加密算法面臨著前所未有的挑戰。量子計算機的強大計算能力能夠輕易破解現有的加密體系&#xff0c;這使得信息安全領域急需一種全新的加密技術來應對未來的威脅。量子加密…

微信小程序中基于 SSE 實現輕量級實時通訊 —— 原理、實踐與對比分析

本文系統梳理了在微信小程序開發中&#xff0c;如何使用 SSE&#xff08;Server-Sent Events&#xff09;方式實現輕量級即時通訊&#xff0c;結合實際項目實踐&#xff0c;詳細講解原理、實現流程、對比 WebSocket/TCP/UDP 通訊方式&#xff0c;并給出完整模塊封裝與最佳實踐建…

OpenCV 圖形API(73)圖像與通道拼接函數-----執行 查找表操作圖像處理函數LUT()

操作系統&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 編程語言&#xff1a;C11 算法描述 對矩陣執行查找表變換。 函數 LUT 使用來自查找表中的值填充輸出矩陣。輸入矩陣中的值作為查找表的索引。也就是說&#xff0c;函數對 src 中的…

MyBatis 類型處理器(TypeHandler)注冊與映射機制:JsonListTypeHandler和JsonListTypeHandler注冊時機

下面幾種機制會讓你的 List<String>/Map<String,?> 能正確讀寫成 JSON 數組&#xff0f;對象文本&#xff1a; MyBatis-Plus 自動注冊 最新版本的 MyBatis-Plus starter 會把類路徑下所有帶 MappedTypes({List.class})、MappedJdbcTypes(JdbcType.VARCHAR) 這類注…

專題二十一:無線局域網——WLAN

一、WLAN簡介 WLAN&#xff08;Wireless Local Area Network &#xff09;無線局域網&#xff0c;使用的是 IEEE 802.11 標準系列。 標準版本發布年份最大傳輸速率頻段Wi-Fi代數特點/描述IEEE 802.1119971–2 Mbps2.4 GHzWi-Fi 0最早的無線局域網標準&#xff0c;傳輸速率低&…

python多進程的使用

多進程編程全面指南&#xff1a;從入門到實踐 摘要&#xff1a;本文是為初學者設計的Python多進程編程全攻略&#xff0c;涵蓋基礎概念、核心函數詳解、系統特性分析&#xff0c;并附帶流程圖、測試用例、開源項目推薦和經典書籍清單。通過8個實戰代碼示例和3個性能對比實驗&am…

數據庫管理與安全:從用戶權限到備份恢復的全面指南

引言 在數字化時代&#xff0c;數據已成為組織最寶貴的資產之一。數據庫作為存儲和管理這些數據的核心系統&#xff0c;其安全性和可靠性直接關系到企業的運營和發展。無論是金融交易記錄、醫療健康信息&#xff0c;還是電子商務平臺的用戶數據&#xff0c;都需要通過完善的數…

Electron Forge【實戰】帶圖片的 AI 聊天

改用支持圖片的 AI 模型 qwen-turbo 僅支持文字&#xff0c;要想體驗圖片聊天&#xff0c;需改用 qwen-vl-plus src/initData.ts {id: 2,name: "aliyun",title: "阿里 -- 通義千問",desc: "阿里百煉 -- 通義千問",// https://help.aliyun.com/z…

在 Elastic 中使用 JOIN 進行威脅狩獵!

作者&#xff1a;來自 Elastic Paul Ewing, Jonhnathan Ribeiro Elastic 的管道查詢語言 ES | QL 為查詢帶來了 join 功能。 威脅狩獵者歡呼吧&#xff01;你是否一直在尋找一種通過 Elastic 的速度和強大功能來連接數據的方法&#xff1f;好消息&#xff01;Elastic 現在可以通…

從實列中學習linux shell5: 利用shell 腳本 檢測硬盤空間容量,當使用量達到80%的時候 發送郵件

下面是用于檢測硬盤空間并在使用量達到80%時發送郵件的Shell腳本 第一步 編寫腳本 #!/bin/bash# 郵件配置 recipient"zhaoqingyou99qhzt.com" subject"磁盤空間警報" mail_cmd"/usr/bin/mail" # 根據實際郵件命令路徑修改# 檢查是否安裝郵件工…

Ethan獨立開發產品日報 | 2025-04-30

1. Daytona 安全且靈活的基礎設施&#xff0c;用于運行你的人工智能生成代碼。 Daytona Cloud重新定義了AI代理的基礎設施&#xff0c;具備低于90毫秒的啟動時間、原生性能和有狀態執行能力&#xff0c;這些是傳統云服務無法比擬的。您可以以前所未有的速度和靈活性來創建、管…

Unity SpriteMask(精靈遮罩)

&#x1f3c6; 個人愚見&#xff0c;沒事寫寫筆記 &#x1f3c6;《博客內容》&#xff1a;Unity3D開發內容 &#x1f3c6;&#x1f389;歡迎 &#x1f44d;點贊?評論?收藏 &#x1f50e;SpriteMask&#xff1a;精靈遮罩 &#x1f4a1;作用就是對精靈圖片產生遮罩&#xff0c…

OpenHarmony全局資源調度管控子系統之內存管理部件

OpenHarmony之內存管理部件 內存管理部件 簡介目錄框架 進程回收優先級列表 補充 回收策略/查殺策略 使用說明參數配置說明 availbufferSizeZswapdParamkillConfignandlife 相關倉 簡介 內存管理部件位于全局資源調度管控子系統中&#xff0c;基于應用的生命周期狀態&#…

姜老師的MBTI課程筆記小結(1)ENFJ人格

課程文稿&#xff1a; 好&#xff0c;今天我們的重點其實并不在ENTJ&#xff0c;而是在于如果一個人其他都很像&#xff0c;只是在思考和感受這兩端選擇的時候&#xff0c;他缺了思考而更尊重感受&#xff0c;它會是什么樣的一個人格特質呢&#xff1f;這就是ENFG在16人格的學派…

Node.js 應用場景

Node.js 應用場景 引言 Node.js 是一個基于 Chrome V8 JavaScript 引擎的開源、跨平臺 JavaScript 運行環境。它主要用于服務器端開發&#xff0c;通過非阻塞 I/O 模型實現了高并發處理能力。本文將詳細介紹 Node.js 的應用場景&#xff0c;幫助你了解其在實際項目中的應用。…

Qt/C++面試【速通筆記六】—Qt 中的線程同步

在多線程編程中&#xff0c;多個線程同時訪問共享資源時&#xff0c;可能會出現數據不一致或者錯誤的情況。這時&#xff0c;我們需要線程同步機制來保證程序的正確性。Qt 提供了多種線程同步方式&#xff0c;每種方式適用于不同的場景。 1. 互斥鎖&#xff08;QMutex&#xff…