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