環(huán)保網(wǎng)站查詢碾米是否做備案做個(gè)網(wǎng)站需要多少錢
Synchronized同步鎖優(yōu)化方法
1.6之前比較重量級(jí),1.6后經(jīng)過優(yōu)化性能大大提升
使用Synchronized實(shí)現(xiàn)同步鎖住要是兩種方式:方法、代碼塊。
1.代碼塊
Synchronized在修飾同步代碼塊時(shí),是由 monitorenter和monitorexit指令來實(shí)現(xiàn)同步的。進(jìn)入monitorenter 指令后,線程將持有Monitor對(duì)象,退出monitorenter指令后,線程將釋放該Monitor對(duì)象。
2.方法
當(dāng)Synchronized修飾同步方法時(shí),并沒有發(fā)現(xiàn)monitorenter和monitorexit指令,而是出現(xiàn)了一個(gè)ACC_SYNCHRONIZED標(biāo)志。這是因?yàn)镴VM使用了ACC_SYNCHRONIZED訪問標(biāo)志來區(qū)分一個(gè)方法是否是同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查該方法是否被設(shè)置ACC_SYNCHRONIZED訪問標(biāo)志。如果設(shè)置了該標(biāo)志,執(zhí)行線程將先持有Monitor對(duì)象,然后再執(zhí)行方法。在該方法運(yùn)行期間,其它線程將無法獲取到該Mointor對(duì)象,當(dāng)方法執(zhí)行完成后,再釋放該Monitor對(duì)象。
JVM中的同步是基于進(jìn)入和退出管程(Monitor)對(duì)象實(shí)現(xiàn)的。每個(gè)對(duì)象實(shí)例都會(huì)有一個(gè)Monitor,Monitor可以和對(duì)象一起創(chuàng)建、銷毀。Monitor是由ObjectMonitor實(shí)現(xiàn),而ObjectMonitor是由C++的ObjectMonitor.hpp文件實(shí)現(xiàn)。
當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí),多個(gè)線程會(huì)先被存放在ContentionList和_EntryList 集合中,處于block狀態(tài)的線程,都會(huì)被加入到該列表。接下來當(dāng)線程獲取到對(duì)象的Monitor時(shí),Monitor是依靠底層操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)互斥的,線程申請(qǐng)Mutex成功,則持有該Mutex,其它線程將無法獲取到該Mutex,競(jìng)爭(zhēng)失敗的線程會(huì)再次進(jìn)入ContentionList被掛起。
如果線程調(diào)用wait() 方法,就會(huì)釋放當(dāng)前持有的Mutex,并且該線程會(huì)進(jìn)入WaitSet集合中,等待下一次被喚醒。如果當(dāng)前線程順利執(zhí)行完方法,也將釋放Mutex。
因?yàn)樯婕暗骄€程的阻塞和掛起等操作,這也是Synchronized比較重量級(jí)的原因。下面看看jdk源碼是怎么進(jìn)行優(yōu)化的。
JDK1.6引入了偏向鎖、輕量級(jí)鎖、重量級(jí)鎖概念,來減少鎖競(jìng)爭(zhēng)帶來的上下文切換,而正是新增的Java對(duì)象頭實(shí)現(xiàn)了鎖升級(jí)功能。當(dāng)Java對(duì)象被Synchronized關(guān)鍵字修飾成為同步鎖后,圍繞這個(gè)鎖的一系列升級(jí)操作都將和Java對(duì)象頭有關(guān)。對(duì)象頭內(nèi)容如下:
鎖升級(jí)過程如下:
🌟🌟🌟一句話概括總結(jié),通過一些方式去競(jìng)爭(zhēng)鎖,在競(jìng)爭(zhēng)中逐漸提高鎖的級(jí)別,代價(jià)也越來越大。一開始只需查詢對(duì)象頭,然后是CAS競(jìng)爭(zhēng),最后直接掛起阻塞線程。
鎖的不同重量級(jí)對(duì)應(yīng)著不同的場(chǎng)景,我們需要根據(jù)實(shí)際的業(yè)務(wù)情況去具體優(yōu)化。
1.偏向鎖主要用來優(yōu)化同一線程多次申請(qǐng)同一個(gè)鎖的競(jìng)爭(zhēng)。在某些情況下,大部分時(shí)間是同一個(gè)線程競(jìng)爭(zhēng)鎖資源,例如,在創(chuàng)建一個(gè)線程并在線程中執(zhí)行循環(huán)監(jiān)聽的場(chǎng)景下,或單線程操作一個(gè)線程安全集合時(shí),同一線程每次都需要獲取和釋放鎖,每次操作都會(huì)發(fā)生用戶態(tài)與內(nèi)核態(tài)的切換。
因此,在高并發(fā)場(chǎng)景下,當(dāng)大量線程同時(shí)競(jìng)爭(zhēng)同一個(gè)鎖資源時(shí),偏向鎖就會(huì)被撤銷,發(fā)生stop the word后, 開啟偏向鎖無疑會(huì)帶來更大的性能開銷,這時(shí)我們可以通過添加JVM參數(shù)關(guān)閉偏向鎖來調(diào)優(yōu)系統(tǒng)性能,示例代碼如下:
-XX:-UseBiasedLocking //關(guān)閉偏向鎖(默認(rèn)打開)
或
-XX:+UseHeavyMonitors //設(shè)置重量級(jí)鎖
2.輕量級(jí)鎖適用于線程交替執(zhí)行同步塊的場(chǎng)景,絕大部分的鎖在整個(gè)同步周期內(nèi)都不存在長(zhǎng)時(shí)間的競(jìng)爭(zhēng)。
3.自旋鎖和重量級(jí)鎖:在鎖競(jìng)爭(zhēng)不激烈且鎖占用時(shí)間非常短的場(chǎng)景下,自旋鎖可以提高系統(tǒng)性能。一旦鎖競(jìng)爭(zhēng)激烈或鎖占用的時(shí)間過長(zhǎng),自旋鎖將會(huì)導(dǎo)致大量的線程一直處于CAS重試狀態(tài),占用CPU資源,反而會(huì)增加系統(tǒng)性能開銷。所以自旋鎖和重量級(jí)鎖的使用都要結(jié)合實(shí)際場(chǎng)景。
在高負(fù)載、高并發(fā)的場(chǎng)景下,我們可以通過設(shè)置JVM參數(shù)來關(guān)閉自旋鎖,優(yōu)化系統(tǒng)性能,示例代碼如下:
-XX:-UseSpinning //參數(shù)關(guān)閉自旋鎖優(yōu)化(默認(rèn)打開)
-XX:PreBlockSpin //參數(shù)修改默認(rèn)的自旋次數(shù)。JDK1.7后,去掉此參數(shù),由jvm控制
4.動(dòng)態(tài)編譯優(yōu)化,JIT編譯器對(duì)鎖的粒度增大或減小。例如,幾個(gè)相鄰的同步塊使用的是同一個(gè)鎖實(shí)例,那么 JIT 編譯器將會(huì)把這幾個(gè)同步塊合并為一個(gè)大的同步塊,從而避免一個(gè)線程“反復(fù)申請(qǐng)、釋放同一個(gè)鎖”所帶來的性能開銷。而粒度減小的典型案例就是JDK8之前的ConcurrentHashMap中用的Segment分段鎖,減小鎖粒度實(shí)現(xiàn)增大并發(fā)量,避免鎖被升級(jí)為重量級(jí)鎖。
Lock同步鎖優(yōu)化方法
和synchronized的對(duì)比
Lock是一個(gè)接口,AQS(AbstractQueuedSynchronizer)是一個(gè)抽象類。Lock鎖是基于Java實(shí)現(xiàn)的鎖,Lock是一個(gè)接口類,常用的實(shí)現(xiàn)類有ReentrantLock、ReentrantReadWriteLock(RRW),它們都是依賴AbstractQueuedSynchronizer(AQS)類實(shí)現(xiàn)的。
AQS類結(jié)構(gòu)中包含一個(gè)基于鏈表實(shí)現(xiàn)的等待隊(duì)列(CLH隊(duì)列),用于存儲(chǔ)所有阻塞的線程,AQS中還有一個(gè)state變量,該變量對(duì)ReentrantLock來說表示加鎖狀態(tài)。
該隊(duì)列的操作均通過CAS操作實(shí)現(xiàn),我們可以通過一張圖來看下整個(gè)獲取鎖的流程。簡(jiǎn)而言之,通過CAS競(jìng)爭(zhēng)和隊(duì)首節(jié)點(diǎn)去獲得鎖。
鎖分離優(yōu)化Lock同步鎖,默認(rèn)的ReentrantLock是獨(dú)占鎖,在大部分業(yè)務(wù)場(chǎng)景中,讀業(yè)務(wù)操作要遠(yuǎn)遠(yuǎn)大于寫業(yè)務(wù)操作。而在多線程編程中,讀操作并不會(huì)修改共享資源的數(shù)據(jù),如果多個(gè)線程僅僅是讀取共享資源,那么這種情況下其實(shí)沒有必要對(duì)資源進(jìn)行加鎖。如果使用互斥鎖,反倒會(huì)影響業(yè)務(wù)的并發(fā)性能,那么在這種場(chǎng)景下,有沒有什么辦法可以優(yōu)化下鎖的實(shí)現(xiàn)方式呢?
1.讀寫鎖ReentrantReadWriteLock
RRW也是繼承AQS實(shí)現(xiàn),內(nèi)部維護(hù)了兩個(gè)鎖讀鎖和寫鎖,實(shí)現(xiàn)的關(guān)鍵是將AQS的同步變量state分為高16位和低16位,分別表示讀寫。
2.讀寫鎖再優(yōu)化之StampedLock
RRW被很好地應(yīng)用在了讀大于寫的并發(fā)場(chǎng)景中,然而RRW在性能上還有可提升的空間。在讀取很多、寫入很少的情況下,RRW會(huì)使寫入線程遭遇饑餓(Starvation)問題,也就是說寫入線程會(huì)因遲遲無法競(jìng)爭(zhēng)到鎖而一直處于等待狀態(tài)。
在JDK1.8中,Java提供了StampedLock類解決了這個(gè)問題。StampedLock不是基于AQS實(shí)現(xiàn)的,但實(shí)現(xiàn)的原理和AQS是一樣的,都是基于隊(duì)列和鎖狀態(tài)實(shí)現(xiàn)的。與RRW不一樣的是,StampedLock控制鎖有三種模式: 寫、悲觀讀以及樂觀讀,并且StampedLock在獲取鎖時(shí)會(huì)返回一個(gè)票據(jù)stamp,獲取的stamp除了在釋放鎖時(shí)需要校驗(yàn),在樂觀讀模式下,stamp還會(huì)作為讀取共享資源后的二次校驗(yàn),后面我會(huì)講解stamp的工作原理。
我們先通過一個(gè)官方的例子來了解下StampedLock是如何使用的,代碼如下:
public class Point {private double x, y;private final StampedLock s1 = new StampedLock();void move(double deltaX, double deltaY) {//獲取寫鎖long stamp = s1.writeLock();try {x += deltaX;y += deltaY;} finally {//釋放寫鎖s1.unlockWrite(stamp);}}double distanceFormOrigin() {//樂觀讀操作long stamp = s1.tryOptimisticRead(); //拷貝變量double currentX = x, currentY = y;//判斷讀期間是否有寫操作if (!s1.validate(stamp)) {//升級(jí)為悲觀讀stamp = s1.readLock();try {currentX = x;currentY = y;} finally {s1.unlockRead(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}
}
我們可以發(fā)現(xiàn):一個(gè)寫線程獲取寫鎖的過程中,首先是通過WriteLock獲取一個(gè)票據(jù)stamp,WriteLock是一個(gè)獨(dú)占鎖,同時(shí)只有一個(gè)線程可以獲取該鎖,當(dāng)一個(gè)線程獲取該鎖后,其它請(qǐng)求的線程必須等待,當(dāng)沒有線程持有讀鎖或者寫鎖的時(shí)候才可以獲取到該鎖。請(qǐng)求該鎖成功后會(huì)返回一個(gè)stamp票據(jù)變量,用來表示該鎖的版本,當(dāng)釋放該鎖的時(shí)候,需要unlockWrite并傳遞參數(shù)stamp。
接下來就是一個(gè)讀線程獲取鎖的過程。首先線程會(huì)通過樂觀鎖tryOptimisticRead操作獲取票據(jù)stamp ,如果當(dāng)前沒有線程持有寫鎖,則返回一個(gè)非0的stamp版本信息。線程獲取該stamp后,將會(huì)拷貝一份共享資源到方法棧,在這之前具體的操作都是基于方法棧的拷貝數(shù)據(jù)。
之后方法還需要調(diào)用validate,驗(yàn)證之前調(diào)用tryOptimisticRead返回的stamp在當(dāng)前是否有其它線程持有了寫鎖,如果是,那么validate會(huì)返回0,升級(jí)為悲觀鎖;否則就可以使用該stamp版本的鎖對(duì)數(shù)據(jù)進(jìn)行操作。
相比于RRW,StampedLock獲取讀鎖只是使用與或操作進(jìn)行檢驗(yàn),不涉及CAS操作,即使第一次樂觀鎖獲取失敗,也會(huì)馬上升級(jí)至悲觀鎖,這樣就可以避免一直進(jìn)行CAS操作帶來的CPU占用性能的問題,因此StampedLock的效率更高。