概念
當(dāng)使用多個(gè)線程來(lái)訪問(wèn)同一個(gè)數(shù)據(jù)時(shí),將會(huì)導(dǎo)致數(shù)據(jù)不準(zhǔn)確,相互之間產(chǎn)生沖突,非常容易出現(xiàn)線程安全問(wèn)題,比如多個(gè)線程都在操作同一數(shù)據(jù),都打算修改商品庫(kù)存,這樣就會(huì)導(dǎo)致數(shù)據(jù)不一致的問(wèn)題。
所以我們通過(guò)線程同步機(jī)制來(lái)保證線程安全,加入同步鎖以避免在該線程沒(méi)有完成操作之前,被其他線程的調(diào)用,從而保證了該變量的唯一性和準(zhǔn)確性。線程同步本質(zhì)就是“排隊(duì)“,多個(gè)線程之間要排隊(duì),然后一個(gè)一個(gè)對(duì)共享資源進(jìn)行操作,而不是同時(shí)進(jìn)行操作,從而保證線程安全(即保證原子性、可見(jiàn)性、有序性)。
鎖
概述
在Java多線程環(huán)境下我們通過(guò)鎖這種方式來(lái)保證共享資源的正確、線程安全,即在線程操作某個(gè)共享資源之前先對(duì)資源加鎖,保證操作期間沒(méi)有其他線程訪問(wèn)資源,當(dāng)操作完成后再對(duì)共享資源釋放鎖供其他線程訪問(wèn)
- Java中鎖是一種同步機(jī)制,用于控制多個(gè)線程對(duì)共享資源的訪問(wèn)。
- 鎖可以防止多個(gè)線程同時(shí)對(duì)同一個(gè)共享資源進(jìn)行寫操作,從而避免數(shù)據(jù)的不一致性和錯(cuò)誤。
- 鎖是一種互斥工具,它能夠確保同一時(shí)間只有一個(gè)線程可以訪問(wèn)共享資源
- Java中的鎖可以用來(lái)保護(hù)代碼塊、對(duì)象、方法、類等各種粒度的共享資源。
- 通過(guò)鎖可以讓多個(gè)線程按照特定的順序訪問(wèn)共享資源,從而避免死鎖、競(jìng)爭(zhēng)條件等并發(fā)問(wèn)題
- Java中常用的鎖有synchronized關(guān)鍵字、ReentrantLock、ReadWriteLock、Semaphore等,這些鎖提供了不同的功能和性能特征
分類
從并發(fā)的角度可將線程安全策略分為三種(我們?nèi)粘i_(kāi)發(fā)主要涉及到前兩種)
- 第一種是悲觀鎖,核心是互斥同步(synchronized,Lock體系)
- 第二種是樂(lè)觀鎖,核心是非阻塞同步,通過(guò)CAS進(jìn)行原子類操作,即不加鎖(底層為volatile+CAS)
- 第三種是無(wú)同步方案,包括可重入代碼和線程本地存儲(chǔ)
常用鎖介紹
- 重入鎖(ReentrantLock):可重入鎖是一種可多次獲取的鎖,它允許一個(gè)線程在獲得鎖的同時(shí)再次獲取鎖。它提供了與synchronized關(guān)鍵字相同的互斥訪問(wèn)控制,但具有更大的靈活性和更強(qiáng)的功能
- 讀寫鎖(ReadWriteLock):讀寫鎖是一種特殊類型的鎖,它允許多個(gè)線程同時(shí)讀取共享資源,但只允許一個(gè)線程寫入共享資源。在讀多寫少的情況下,讀寫鎖可以提高程序的并發(fā)性能
- 公平鎖(FairLock):公平鎖保證線程獲取鎖的順序與線程請(qǐng)求鎖的順序相同。如果存在一個(gè)等待隊(duì)列,那么等待時(shí)間最長(zhǎng)的線程將獲得鎖
- 互斥鎖(Mutex):互斥鎖是一種最簡(jiǎn)單的鎖,它通過(guò)對(duì)共享資源加鎖來(lái)確保同一時(shí)間只有一個(gè)線程可以訪問(wèn)該資源
- 信號(hào)量(Semaphore):信號(hào)量是一種同步工具,它可以用來(lái)控制對(duì)共享資源的訪問(wèn)。它允許多個(gè)線程同時(shí)訪問(wèn)共享資源,但限制了同時(shí)訪問(wèn)該資源的線程數(shù)量
- 偏向鎖(Biased Locking):偏向鎖是一種優(yōu)化手段,它可以減少多線程環(huán)境下鎖的競(jìng)爭(zhēng)。它的基本思想是在沒(méi)有競(jìng)爭(zhēng)的情況下將鎖偏向于第一個(gè)獲取鎖的線程,從而避免其他線程競(jìng)爭(zhēng)鎖
應(yīng)用場(chǎng)景
多線程鎖是一種用于在多線程編程中保護(hù)共享資源的同步機(jī)制。如下是適合使用多線程鎖的場(chǎng)景:
- 數(shù)據(jù)庫(kù)訪問(wèn):多個(gè)線程同時(shí)訪問(wèn)數(shù)據(jù)庫(kù)可能導(dǎo)致數(shù)據(jù)一致性問(wèn)題,使用鎖可以保證數(shù)據(jù)的完整性和正確性
- 文件讀寫:多個(gè)線程同時(shí)讀寫同一個(gè)文件可能會(huì)導(dǎo)致文件損壞或者數(shù)據(jù)丟失,使用鎖可以保證文件的完整性和正確性
- 共享內(nèi)存:多個(gè)線程訪問(wèn)同一塊共享內(nèi)存時(shí),使用鎖可以保證每個(gè)線程都能正確讀取或?qū)懭牍蚕韮?nèi)存的數(shù)據(jù)
- 隊(duì)列操作:多個(gè)線程同時(shí)對(duì)隊(duì)列進(jìn)行操作可能會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)亂或者數(shù)據(jù)丟失,使用鎖可以保證隊(duì)列的操作順序和數(shù)據(jù)的正確性
- 網(wǎng)絡(luò)通信:多個(gè)線程同時(shí)進(jìn)行網(wǎng)絡(luò)通信時(shí),使用鎖可以保證數(shù)據(jù)傳輸?shù)耐暾院驼_性
注意: 過(guò)多的鎖使用會(huì)降低程序的性能。在使用鎖的時(shí)候應(yīng)該注意權(quán)衡鎖的粒度和性能的需求
同步機(jī)制
Synchronized
概述
- synchronized是Java中的關(guān)鍵字,是一種同步的悲觀鎖
- 常用來(lái)修飾的對(duì)象有以下幾種:
- 修飾一個(gè)代碼塊,被修飾的代碼塊稱為同步語(yǔ)句塊,其作用的范圍是大括號(hào){}括起來(lái)的代碼,作用的對(duì)象是調(diào)用這個(gè)代碼塊的對(duì)象
- 修飾一個(gè)方法,被修飾的方法稱為同步方法,其作用的范圍是整個(gè)方法,作用的對(duì)象是調(diào)用這個(gè)方法的對(duì)象
- 修改一個(gè)靜態(tài)的方法,其作用的范圍是整個(gè)靜態(tài)方法,作用的對(duì)象是這個(gè)類的所有對(duì)象
- 修改一個(gè)類,其作用的范圍是synchronized后面括號(hào)括起來(lái)的部分,作用的對(duì)象是這個(gè)類的所有對(duì)象
- 通過(guò)synchronized+wait+notify實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者問(wèn)題(在后文并發(fā)實(shí)踐篇會(huì)有相關(guān)示例)
實(shí)現(xiàn)原理
synchronized是基于Java對(duì)象頭中的標(biāo)志位實(shí)現(xiàn)的,其中在Java對(duì)象頭中有兩個(gè)標(biāo)志位用于存儲(chǔ)synchronized鎖的信息:
- 一個(gè)是表示當(dāng)前對(duì)象是否被鎖定的標(biāo)志位
- 一個(gè)是表示持有鎖的線程的標(biāo)識(shí)符
執(zhí)行過(guò)程描述
- 當(dāng)一個(gè)線程嘗試獲得一個(gè)被synchronized鎖保護(hù)的資源時(shí)(即執(zhí)行到monitorenter指令時(shí)),JVM會(huì)首先檢查該對(duì)象的鎖標(biāo)志位,如果鎖標(biāo)志位為0表示該對(duì)象沒(méi)有被鎖定,JVM會(huì)將鎖標(biāo)志位設(shè)置為1,并將持有鎖的線程標(biāo)識(shí)符設(shè)置為當(dāng)前線程的標(biāo)識(shí)符。如果鎖標(biāo)志位為1表示該對(duì)象已經(jīng)被其他線程鎖定,當(dāng)前線程會(huì)進(jìn)入阻塞狀態(tài),等待其他線程釋放鎖;
- 當(dāng)一個(gè)線程釋放一個(gè)被synchronized鎖保護(hù)的資源時(shí)(即執(zhí)行到monitorexit指令時(shí)),JVM會(huì)將鎖標(biāo)志位設(shè)置為0并且清空線程id釋放該對(duì)象,同時(shí)JVM會(huì)喚醒等待該對(duì)象鎖的其他線程,使它們可以繼續(xù)競(jìng)爭(zhēng)鎖
monitor指令
通過(guò)反編譯字節(jié)碼文件后發(fā)現(xiàn)synchronized底層借助monitor指令實(shí)現(xiàn)同步,;monitor指令包括monitorenter和monitorexit可以理解為代碼開(kāi)始同步/開(kāi)始加鎖和結(jié)束同步/結(jié)束加鎖;
- monitorenter指令進(jìn)行加鎖: 進(jìn)入同步代碼后,每次進(jìn)行操作前后,都需要獲取最新的數(shù)據(jù),執(zhí)行完畢,及時(shí)寫回主內(nèi)存
- monitorexit指令進(jìn)行釋放鎖: 設(shè)置對(duì)象的鎖標(biāo)志為0,線程id清空,喚醒等待該對(duì)象鎖的其他線程,使它們可以繼續(xù)競(jìng)爭(zhēng)鎖
![Java并發(fā)編程(三)線程同步 上[synchronized/volatile],# Java,java](https://imgs.yssmx.com/Uploads/2023/08/643175-4.png)
![Java并發(fā)編程(三)線程同步 上[synchronized/volatile],# Java,java](https://imgs.yssmx.com/Uploads/2023/08/643175-5.png)
注意:monitorexit指令為何出現(xiàn)2次?
- 第一個(gè)monitorexit指令是同步代碼塊正常釋放鎖的一個(gè)標(biāo)志
- 如果同步代碼塊中出現(xiàn)Exception或者Error,則會(huì)調(diào)用第二個(gè)monitorexit指令來(lái)保證釋放鎖
鎖優(yōu)化
概述
JDK5升級(jí)到JDK6后一項(xiàng)重要的改進(jìn)項(xiàng),HotSpot虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)花費(fèi)了大量的資源去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù),如適應(yīng)性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖粗化(Lock Coarsening)、偏向鎖(Biased Locking)、輕量級(jí)鎖(Lightweight Locking)等,這些技術(shù)都是為了在線程之間更高效的共享數(shù)據(jù)及解決競(jìng)爭(zhēng)問(wèn)題,從而提高程序的執(zhí)行效率。
鎖粗化
假設(shè)一系列的連續(xù)操作都會(huì)對(duì)同一個(gè)對(duì)象反復(fù)加鎖及解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,即使沒(méi)有出現(xiàn)線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。如果JVM檢測(cè)到有一連串零碎的操作都是對(duì)同一對(duì)象的加鎖,將會(huì)擴(kuò)大加鎖同步的范圍(即鎖粗化)到整個(gè)操作序列的外部
鎖消除
鎖消除即刪除不必要的加鎖操作。鎖消除是Java虛擬機(jī)在JIT編譯期間,通過(guò)對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過(guò)鎖消除,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間。
- 比如StringBuffer.append()方法使用了synchronized關(guān)鍵字來(lái)進(jìn)行線程安全的保護(hù).但若僅在線程內(nèi)部把StringBuffer的對(duì)象當(dāng)作一個(gè)局部變量來(lái)使用,其實(shí)就不會(huì)發(fā)生所謂的線程不安全的情況.此時(shí)Java以Server模式啟動(dòng)的,且已經(jīng)開(kāi)啟了逃逸分析的配置,那么編譯器就會(huì)將這段代碼優(yōu)化, 鎖消除
偏向鎖和輕量級(jí)鎖
- 偏向鎖:在無(wú)競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉,也無(wú)CAS操作。簡(jiǎn)單的講,就是在鎖對(duì)象的對(duì)象頭中有個(gè)ThreadId字段,這個(gè)字段如果是空的,第一次獲取鎖時(shí)將自身的ThreadId寫入到鎖的ThreadId字段內(nèi),將鎖頭內(nèi)的是否偏向鎖的狀態(tài)置為1(上面的標(biāo)識(shí)位),此后獲取鎖時(shí)直接檢查ThreadId是否和自身線程Id一致,若一致則認(rèn)為當(dāng)前線程已經(jīng)獲取了鎖。但當(dāng)鎖有競(jìng)爭(zhēng)關(guān)系的時(shí)候,需要解除偏向鎖,使鎖進(jìn)入競(jìng)爭(zhēng)的狀態(tài)(目前JDK偏向鎖默認(rèn)是開(kāi)啟的)
- 輕量級(jí)鎖:在無(wú)競(jìng)爭(zhēng)的情況下使用CAS操作對(duì)象頭,將替換線程ID和指向鎖記錄的指針。成功則獲得鎖,失敗則自旋等待獲得鎖。機(jī)制:每個(gè)鎖都關(guān)聯(lián)一個(gè)請(qǐng)求計(jì)數(shù)器和一個(gè)占有他的線程,當(dāng)請(qǐng)求計(jì)數(shù)器為0時(shí),這個(gè)鎖可以被認(rèn)為是unhled的,當(dāng)一個(gè)線程請(qǐng)求一個(gè)unheld的鎖時(shí),JVM記錄鎖的擁有者,并把鎖的請(qǐng)求計(jì)數(shù)加1,如果同一個(gè)線程再次請(qǐng)求這個(gè)鎖時(shí),請(qǐng)求計(jì)數(shù)器就會(huì)增加,當(dāng)該線程退出syncronized塊時(shí),計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí)鎖被釋放(這就保證了鎖是可重入的,不會(huì)發(fā)生死鎖的情況)
鎖升級(jí)
概述
JDK1.6之后對(duì)synchronized進(jìn)行了性能上的優(yōu)化,引入了輕量級(jí)鎖和偏向鎖來(lái)減少性能消耗,所以不完全認(rèn)為它是一個(gè)重量級(jí)鎖,鎖升級(jí)的過(guò)程是由JVM
自動(dòng)完成,JVM
會(huì)根據(jù)同步競(jìng)爭(zhēng)的情況來(lái)自動(dòng)
選擇合適的鎖級(jí)別,以提供更好的性能和效率。JDK1.6中鎖有四種狀態(tài),分別是無(wú)鎖、輕量級(jí)鎖(自旋)、偏向鎖、重量級(jí)。鎖升級(jí)過(guò)程從偏向鎖->輕量級(jí)鎖->重量級(jí)鎖,而且鎖升級(jí)之后不可降級(jí)文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-643175.html
鎖升級(jí)過(guò)程
當(dāng)?shù)谝粋€(gè)線程訪問(wèn)同步塊時(shí),JVM將該線程ID記錄在對(duì)象頭部,并將對(duì)象的標(biāo)記狀態(tài)設(shè)置為偏向鎖(偏向鎖發(fā)生于同一時(shí)刻只有一個(gè)線程競(jìng)爭(zhēng)鎖的場(chǎng)景)。若有多個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖,則偏向鎖會(huì)升級(jí)為輕量級(jí)鎖。如果線程的 CAS 自旋操作達(dá)到一定次數(shù)仍未競(jìng)爭(zhēng)到鎖,則輕量級(jí)鎖會(huì)升級(jí)為重量級(jí)鎖文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-643175.html
- 初始狀態(tài):對(duì)象沒(méi)有鎖標(biāo)記,即為無(wú)鎖狀態(tài)
- 偏向鎖申請(qǐng):當(dāng)?shù)谝粋€(gè)線程訪問(wèn)同步塊時(shí),JVM將該線程ID記錄在對(duì)象頭部,并將對(duì)象的標(biāo)記狀態(tài)設(shè)置為偏向鎖
- 偏向鎖撤銷:當(dāng)其他線程嘗試獲取鎖時(shí),發(fā)現(xiàn)對(duì)象的偏向鎖被占用,會(huì)撤銷偏向鎖,升級(jí)為輕量級(jí)鎖
- 輕量級(jí)鎖(Lightweight Locking): 輕量級(jí)鎖是指當(dāng)多個(gè)線程輕度競(jìng)爭(zhēng)同步塊時(shí),JVM會(huì)將對(duì)象的鎖記錄存儲(chǔ)在線程的棧幀中,而不是在對(duì)象頭中。線程在進(jìn)入同步塊之前,通過(guò)CAS(比較并交換)自旋操作嘗試獲取鎖。如果CAS自旋操作成功則表示獲取鎖成功,進(jìn)入同步塊,則當(dāng)前鎖仍然處于輕量級(jí)鎖狀態(tài);如果CAS失敗表示存在競(jìng)爭(zhēng),升級(jí)為重量級(jí)鎖
- 重量級(jí)鎖是指當(dāng)多個(gè)線程激烈競(jìng)爭(zhēng)同步塊時(shí),JVM會(huì)將對(duì)象的鎖升級(jí)為重量級(jí)鎖,使用操作系統(tǒng)提供的互斥量來(lái)實(shí)現(xiàn)鎖機(jī)制。重量級(jí)鎖涉及到線程的阻塞和喚醒操作,開(kāi)銷較大
volatile
概述
- 相比于synchronized(重量級(jí)鎖),volitate是JVM提供的輕量級(jí)同步機(jī)制關(guān)鍵字,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度
- 無(wú)法保證線程安全,因?yàn)樗痪邆洹盎コ庑浴保荒鼙WC變量的原子性
特點(diǎn)
-
保證可見(jiàn)性(緩存一致性原理)
- 當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去
- 這個(gè)寫會(huì)操作會(huì)導(dǎo)致其他線程中的volatile變量緩存無(wú)效
-
保證有序性
- 通過(guò)內(nèi)存屏障相關(guān)指令(lock指令)禁止指令重排實(shí)現(xiàn)有序性(重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行排序的一種手段,在單線程下一定能保證結(jié)果的正確性,但在多線程環(huán)境下結(jié)果不一定正確)
-
內(nèi)存屏障作用
- 它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成
- 會(huì)強(qiáng)制將對(duì)緩存(線程中私有的工作內(nèi)存)的修改操作立即寫入主存(堆內(nèi)存)
- 如果是寫操作,它會(huì)導(dǎo)致其他線程中對(duì)應(yīng)的緩存行無(wú)效
- 無(wú)法保證原子性
- volatile不適合復(fù)合操作(如volatile++),就是因?yàn)闊o(wú)法保證原子性
常見(jiàn)使用場(chǎng)景
- 狀態(tài)量標(biāo)記,如:volatile bool flag = false;對(duì)變量的讀寫操作,標(biāo)記為volatile可以保證變量的修改對(duì)線程立刻可見(jiàn),比synchronized,Lock實(shí)現(xiàn)有一定的效率提升
- 單例模式中通過(guò)使用典型的雙重檢查鎖定(DCL)保證線程安全示例
//懶漢單例模式
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) { //將同步的粒度降到方法內(nèi)部,提高了程序的性能
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
到了這里,關(guān)于Java并發(fā)編程(三)線程同步 上[synchronized/volatile]的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!