<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JUC并發(fā)編程之Synchronized關(guān)鍵字詳解

          共 15834字,需瀏覽 32分鐘

           ·

          2021-07-03 16:43

          點擊上方藍字 關(guān)注我吧



          1
          設(shè)計同步器的意義

          在多線程中,有可能會出現(xiàn)多個線程同時訪問同一個共享、可變資源的情況,這個資源我們稱之其為臨界資源;這種資源可能是:對象、變量、文件等。
          共享:資源可以由多個線程同時訪問
          可變:資源可以在其生命周期內(nèi)被修改
          根據(jù)這段話引出來一個問題,由于線程在CPU中執(zhí)行的過程是不可控的,所以我們需要采取同步機制來協(xié)同對對象的可變狀態(tài)的訪問。


          2
          什么是Synchronized


          synchronized關(guān)鍵字,它是java內(nèi)置鎖,在程序界中俗稱同步,在多線程下能夠保證線程安全問題,是解決高并發(fā)的一種解決方案。其實synchronized在1.6這個臨界版本有一段故事的,在1.6版本前synchronized的效率是非常低的,因為在多線程下無論線程競爭鎖是否激烈它都會創(chuàng)建重量級鎖,直接對CPU操作是比較浪費時間的,因此國外的一位并發(fā)大佬"Doug Lea",覺得這個同步鎖很low太浪費程序資源了,所以它自己獨自開發(fā)了一套并發(fā)框架"AQS",其中ReenLock鎖就是代替synchronized的,因此在1.5版本后,開發(fā)synchronized團隊的技術(shù)人員對該關(guān)鍵字進行了一個較大的優(yōu)化,后續(xù)文章中會講到優(yōu)化的幾點,后來ReenLock和synchronized兩者的性能都相差不大,因此我們在程序中這兩個鎖也都用的比較多。


          對了,后續(xù)我也會對ReenLock鎖進行一個源碼分析,大家伙可以敬請期待。



          3
          Synchronized原理詳解


          synchronized內(nèi)置鎖是一種對象鎖(鎖的是對象而非引用),作用粒度是對象,可以用來實現(xiàn)對臨界資源的同步互斥訪問,是可重入的。
          它的加鎖方式分為以下幾種:

          第一種方式:靜態(tài)方法上加同步塊(demo比較簡陋,且鎖在哪里代碼中也詳細說到了)
          public class StaticTest01 {    /**     * 靜態(tài)方法加鎖,它的鎖就是加在 StaticTest01.class 上     * 因為是靜態(tài)方法,所以是通過類進行調(diào)用的,那么鎖就加在類上面     */    public synchronized static void decrStock() {        System.out.println("上鎖");    }}


          第二種方式:非靜態(tài)方法上加同步塊(demo比較簡陋,且鎖在哪里代碼中也詳細說到了)
          public class StaticTest02 {    /**     * 非靜態(tài)方法加鎖,因為非靜態(tài),所以需要進行 new對象,然后才能使用該方法     * 那么鎖就是加在 new 對象的這個對象上,例如:StaticTest02 syn = new StaticTest02();     * 那么鎖就是加在 syn 這個對象上     */    public synchronized void decrStock() {        System.out.println("上鎖");    }}


          第三種方式:方法內(nèi)的代碼塊加同步塊(demo比較簡陋,且鎖在哪里代碼中也詳細說到了)
          public class StaticTest03 {    private static Object object = new Object();     /**     * 非靜態(tài)方法代碼塊加鎖,那么鎖就是加在 object 這個成員變量上     * 可以針對一部分代碼塊,而非整個方法     */    public void decrStock() {        synchronized (object) {            System.out.println("上鎖");        }    }}


          Synchronized底層原理
          synchronized是基于JVM內(nèi)置鎖實現(xiàn),通過內(nèi)部對象Monitor(監(jiān)視器鎖)實現(xiàn),基于進入與退出Monitor對象實現(xiàn)方法與代碼塊同步,監(jiān)視器鎖的實現(xiàn)依賴底層操作系統(tǒng)的Mutex lock(互斥鎖)實現(xiàn),它是一個重量級鎖性能較低。當(dāng)然,JVM內(nèi)置鎖在1.5之后版本做了重大的優(yōu)化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應(yīng)性自旋(Adaptive Spinning)等技術(shù)來減少鎖操作的開銷。


          需要注意的是:synchronized關(guān)鍵字被編譯成字節(jié)碼后會被翻譯成monitorenter monitorexit 兩條指令分別在同步塊邏輯代碼的起始位置與結(jié)束位置。


          synchronized在JVM里的實現(xiàn)都是 基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,雖然具體實現(xiàn)細節(jié)不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現(xiàn)。


          基于字節(jié)碼文件,來看看同步塊代碼與同步方法它們之間的區(qū)別

          同步塊代碼:
          public class StaticTest03 {    private static Object object = new Object();    /**     * 非靜態(tài)方法代碼塊加鎖,那么鎖就是加在 object 這個成員變量上     * 只不過代碼塊,可以針對一部分代碼塊,而非整個方法     */    public void decrStock() {        synchronized (object) {            System.out.println("上鎖");        }    }}


          反編譯后的結(jié)果


          看到我上圖中所標(biāo)記的三個方塊,分別是對對象上鎖和兩次釋放鎖操作,來詳細分析它們是什么。
          每個對象有一個監(jiān)視器鎖(monitor)。當(dāng)monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權(quán),過程如下:
          1.monitorenter
          1.如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設(shè)置為1,該線程即為monitor的所有者;
          2.如果線程已經(jīng)占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1;
          3.如果其他線程已經(jīng)占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為0,再重新嘗試獲取monitor的所有權(quán);
          2.monitorexit
          1.執(zhí)行monitorexit的線程必須是objectref所對應(yīng)的monitor的所有者。
          2.指令執(zhí)行時,monitor的進入數(shù)減1,如果減1后進入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權(quán)。


          monitorexit,指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖;第2次為發(fā)生異步退出釋放鎖


          通過上面兩段描述,我們應(yīng)該能很清楚的看出Synchronized的實現(xiàn)原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因


          接著來看同步方法:

          public class StaticTest02 {    /**     * 非靜態(tài)方法加鎖,因為非靜態(tài),所以需要進行 new對象,然后才能使用該方法     * 那么鎖就是加在 new 對象的 這個 對象上,例如:StaticTest02 syn = new StaticTest02();     * 那么鎖就是加在 syn 這個對象上     */    public synchronized void decrStock() {        System.out.println("上鎖");    }}


          反編譯后的結(jié)果


          從上圖編譯的結(jié)果來看,方法的同步并沒有通過指令 monitorentermonitorexit 來完成(理論上其實也可以通過這兩條指令來實現(xiàn)),不過相對于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標(biāo)示符。JVM就是根據(jù)該標(biāo)示符來實現(xiàn)方法的同步的


          當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。


          兩種同步方式本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來實現(xiàn),無需通過字節(jié)碼來完成。兩個指令的執(zhí)行是JVM通過調(diào)用操作系統(tǒng)的互斥原語mutex來實現(xiàn),被阻塞的線程會被掛起、等待重新調(diào)度,會導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個態(tài)之間來回切換,對性能有較大影響。


          我們前面也有說到,同步鎖它是加在對象上的,那他在對象里面是如何存儲的呢?來分析分析


          HotSpot虛擬機中,對象在內(nèi)存中存儲的布局可以分為三塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對齊填充(Padding)。

          1.對象頭:比如 hash碼,對象所屬的年代,對象鎖,鎖狀態(tài)標(biāo)志,偏向鎖(線程)ID,偏向時間,數(shù)組長度(數(shù)組對象)等。Java對象頭一般占有2個機器碼(在32位虛擬機中,1個機器碼等于4字節(jié),也就是32bit,在64位虛擬機中,1個機器碼是8個字節(jié),也就是64bit),但是如果對象是數(shù)組類型,則需要3個機器碼,因為JVM虛擬機可以通過Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是無法從數(shù)組的元數(shù)據(jù)來確認數(shù)組的大小,所以用一塊來記錄數(shù)組長度。
          2.實例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息;
          3.對齊填充:由于虛擬機要求 對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊;


          但看這段文字估計還有點懵,我在這里放上一張圖片。


          synchronized在jdk1.6版本后,在其內(nèi)部加了偏向鎖、輕量鎖等鎖技術(shù),那么這些鎖它們是怎么進化的呢?又或者說這些鎖的信息都存放在對象哪里呢?來,往下看。


          前面也說到了對象的組成部分,結(jié)合上圖進行分析,在HotSpot虛擬機的對象頭包括兩部分信息,第一部分是“Mark Word”,用于存儲對象自身的運行時數(shù)據(jù), 如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等等,它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵,這部分數(shù)據(jù)的長度在32位和64位的虛擬機(暫不考慮開啟壓縮指針的場景)中分別為32個和64個Bits,為了節(jié)省內(nèi)存,如果我們機器是64位系統(tǒng),則jvm會自動開啟指針壓縮,將它壓縮成32位,所以本文就基于32位來進行分析。


          再繼續(xù)放上,基于32位虛擬機的一個對象頭表格

          鎖狀態(tài)
          25bit
          4bit
          1bit
          2bit
          23bit
          2bit
          是否偏向鎖(是否禁用偏向)
          鎖標(biāo)志位
          無鎖態(tài)
          對象的hashCode
          分代年齡
          0
          01
          輕量級鎖
          指向棧中鎖記錄的指針
          00
          重量級鎖
          指向Monitor的指針
          10
          GC標(biāo)記
          11
          偏向鎖
          線程ID
          Epoch
          分代年齡
          1
          01



          在32位的HotSpot虛擬機中,對象未被鎖定的狀態(tài)下,Mark Word的32個Bits空間中的25Bits用于存儲對象哈希碼(HashCode),4Bits用于存儲對象分代年齡,1Bit固定為0,2Bits用于存儲鎖標(biāo)志位,在其他狀態(tài)(輕量級鎖定、重量級鎖定、GC標(biāo)記、可偏向)下對象的存儲內(nèi)容如上表所示。


          注意:對象頭信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù),它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發(fā)生變化


          說到這,前面我有說到j(luò)vm它默認開啟了指針壓縮,其實我們也可以手動將其關(guān)閉,主要看場景決定吧

          手動設(shè)置-XX:+UseCompressedOops


          那么哪些信息會被壓縮呢?
          1.對象的全局靜態(tài)變量(即類屬性)
          2.對象頭信息:64位平臺下,原生對象頭大小為16字節(jié),壓縮后為12字節(jié)
          3.對象的引用類型:64位平臺下,引用類型本身大小為8字節(jié),壓縮后為4字節(jié)
          4.對象數(shù)組類型:64位平臺下,數(shù)組類型本身大小為24字節(jié),壓縮后16字節(jié)


          有了以上內(nèi)容的鋪墊,我們就可以來聊一聊偏向鎖、輕量級鎖、自旋鎖,它們是什么東西,然后再來分析它們在對象頭中產(chǎn)生了什么樣的差異。

          偏向鎖:
          偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優(yōu)化手段,經(jīng)過研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關(guān)鎖申請的操作,從而也就提供程序的性能。所以,對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應(yīng)該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著了解輕量級鎖。(偏向鎖一般只有一個線程訪問,超過一個線程以上則會晉升為輕量級鎖,注意偏向鎖在JVM啟動后4秒才會生效,因為jvm底層默認會啟動多個線程,也就是底層啟動執(zhí)行的方法有synchronized方法,內(nèi)部會存在CPU競爭行為,所以為了效率原因(去掉偏向鎖轉(zhuǎn)到輕量級鎖的過程),后4秒才啟動偏向鎖)主要也是為了提高效率


          輕量級鎖

          倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu)。輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖,在整個同步周期內(nèi)都不存在競爭”,注意這是經(jīng)驗數(shù)據(jù)。需要了解的是,輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導(dǎo)致輕量級鎖膨脹為重量級鎖。(超過兩個以上的線程訪問,輕量級鎖會晉升到重量級鎖)


          自旋鎖

          輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統(tǒng)層面掛起,還會進行一項稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統(tǒng)層面的線程可能會得不償失,畢竟操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖,因此虛擬機會讓當(dāng)前想要獲取鎖的線程做幾個空循環(huán)(這也是稱為自旋的原因),一般不會太久,可能是50個循環(huán)或100循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進入臨界區(qū)。如果還不能獲得鎖,那就會將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。(自旋鎖是在重量級鎖中的,重量級鎖性能開銷會很大,所以一般多線程部分業(yè)務(wù)執(zhí)行會很快,CPU沒必要釋放,所以弄個空循環(huán)占用CPU執(zhí)行,如果超過自旋鎖的次數(shù)后,就退出掛著,等待鎖的釋放,在進行搶占鎖資源)


          鎖的對象頭分析:

          分析對象頭我們需要引入以下依賴,該依賴是OpenJdk開源的工具包,用于分析對象頭鎖(從對象頭中分析鎖的晉升過程)
          <dependency>    <groupId>org.openjdk.jol</groupId>    <artifactId>jol-core</artifactId>    <version>0.10</version></dependency>


          如下這段代碼,通過開源的工具,將無鎖對象的一些對象頭信息打印出來
          public class StaticTest04 {    public static void main(String[] args) {        Object o = new Object();        System.out.println(ClassLayout.parseInstance(o).toPrintable());    }}


          運行結(jié)果圖


          上圖中,有很多信息,而屬于對象頭中的mark word,就在我標(biāo)記的這塊區(qū)域中,在操作系統(tǒng)中分為了大端和小端,而在大部分操作系統(tǒng)中都是小端,而通訊協(xié)議是大端,所以我們查看的數(shù)據(jù)要倒序查看。


          以上圖為例,將信息頭反過來后,我們結(jié)合上面的表格查看,在最后的兩位是 "01" ,是01則就是無所狀態(tài)的標(biāo)識

          //對象頭信息00000001 00000000 00000000 00000000//將信息返回來后00000000 00000000 00000000 00000001


          接著,我們在這段代碼的基礎(chǔ)上,在加一個同步塊操作,鎖的對象則是方法中的局部變量,然后再來看看效果
          public class StaticTest04 {    public static void main(String[] args) {        Object o = new Object();        System.out.println(ClassLayout.parseInstance(o).toPrintable());        synchronized (o) {            System.out.println(ClassLayout.parseInstance(o).toPrintable());        }    }}


          運行結(jié)果圖


          上圖我輸出了兩次該對象頭的信息,第一次無鎖狀態(tài)是 "01" 這個是沒問題,奇怪的是,第二次輸出的對象頭,它居然是 "00",結(jié)合上面表格不就是輕量級鎖了么?根據(jù)前面我鎖講的鎖晉升,當(dāng)只有一個線程訪問時,就進入偏向鎖,怎么程序中輸出的對象頭信息是輕量級鎖呢?因為偏向鎖是在jvm啟動的4秒后生效,因為jvm在剛開始啟動的時候,會有多個線程進行初始化數(shù)據(jù),放了防止一開始就造成鎖的膨脹,所以jvm延緩了偏向鎖的使用。
          //加同步塊后對象頭信息01001000 11110010 11001110 00000010//倒序轉(zhuǎn)換后的對象頭信息00000010 11001110 11110010 01001000 


          根據(jù)以上這段話的分析,那我們是不是可以在程序啟動后,先讓線程暫停5秒,然后再讓它去執(zhí)行同步塊操作,然后再看看我的這段話是否說的正確
          public class StaticTest04 {    public static void main(String[] args) throws InterruptedException {        TimeUnit.SECONDS.sleep(5);        Object o = new Object();        System.out.println(ClassLayout.parseInstance(o).toPrintable());        synchronized (o) {            System.out.println(ClassLayout.parseInstance(o).toPrintable());        }    }}


          運行結(jié)果圖


          當(dāng)我們將程序暫停5秒后,再去查看對象頭信息,發(fā)現(xiàn)怎么對象頭又變成 "01" 無鎖狀態(tài)了呢?我們再看到表格中,有一個1bit是專門標(biāo)識該對象是無鎖還是偏向鎖,0為無鎖,1為偏向鎖,那我們就直接看后三位啦,下面的后三位是不是101,那么101對著表格不就是我們的偏向鎖了么。
          //加同步塊后對象頭信息00000101 01010000 01101110 00000011//倒序轉(zhuǎn)換后的對象頭信息00000011 01101110 01010000 00000101 


          接下來演示,從無鎖狀態(tài)的對象 晉升為偏向鎖 然后晉升為輕量級鎖,看下方這段代碼
          @Slf4jpublic class StaticTest05 {    public static void main(String[] args) throws InterruptedException {        TimeUnit.SECONDS.sleep(5);        Object o = new Object();        log.info(ClassLayout.parseInstance(o).toPrintable());        new Thread(() -> {            synchronized (o){                log.info(ClassLayout.parseInstance(o).toPrintable());            }        }).start();        TimeUnit.SECONDS.sleep(2);        new Thread(() -> {            synchronized (o){                log.info(ClassLayout.parseInstance(o).toPrintable());            }        }).start();    }}


          運行結(jié)果圖


          從上圖中,首先我們看到主線程和第一個線程打印的對象頭信息,它們的頭信息都是 "101" 為偏向鎖,因為該對象現(xiàn)在還只被一個線程所占用(主線程沒加鎖打印,所以不算),然后接著第二個線程也對該對象進行加鎖,實現(xiàn)同步塊代碼操作,那么這個時候就存在多個線程進行訪問鎖對象,從而將鎖的等級從偏向鎖晉升為輕量級鎖啦


          最后再來看看,如何晉升成的重量級鎖的,先看代碼

          @Slf4jpublic class StaticTest06 {    public static void main(String[] args) throws InterruptedException {        TimeUnit.SECONDS.sleep(5);        Object o = new Object();        Thread threadA = new Thread(() -> {            synchronized (o) {                log.info(ClassLayout.parseInstance(o).toPrintable());                try {                    //讓線程晚點死亡                    TimeUnit.SECONDS.sleep(2);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        Thread threadB = new Thread(() -> {            synchronized (o) {                log.info(ClassLayout.parseInstance(o).toPrintable());                try {                    //讓線程晚點死亡                    TimeUnit.SECONDS.sleep(2);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        //兩個線程同時啟動,模擬并發(fā)同時請求        threadA.start();        threadB.start();    }}


          運行結(jié)果圖,從圖中我們看到,我們的對象頭中的鎖是不是已經(jīng)成了重量級鎖了,那么再來看看這段代碼它是怎么模擬的,首先我們需要知道重量級鎖它是在鎖競爭非常激烈的時候才成為的,這段代碼模擬的是,我啟動兩個線程,第一個線程對對象進行加鎖,然后睡眠兩秒,模擬程序在處理業(yè)務(wù),然后第二個線程一直在等待第一個線程釋放鎖,在等待的過程中,會觸發(fā)自旋鎖,如果自旋鎖達到了閾值,則會直接讓第二個線程進行阻塞,從而線程2晉升為重量級鎖


          這以上這幾段代碼,我們是否就能夠了解到,鎖是如何晉升的,以及也驗證了上面我介紹了幾個鎖的特性(什么時機進行觸發(fā))。


          當(dāng)然synchronized在1.6版本優(yōu)化中還加了兩個細節(jié)點的優(yōu)化,例如鎖粗化、鎖消除這兩個點。


          鎖粗化:

          通常情況下,為了保證多線程間的有效并發(fā),會要求每個線程持有鎖的時間盡可能短,但是某些情況下,一個程序?qū)ν粋€鎖不間斷、高頻地請求、同步與釋放,會消耗掉一定的系統(tǒng)資源,因為鎖的請求、同步與釋放本身會帶來性能損耗,這樣高頻的鎖請求就反而不利于系統(tǒng)性能的優(yōu)化了,雖然單次同步操作的時間可能很短。鎖粗化就是告訴我們?nèi)魏问虑槎加袀€度,有些情況下我們反而希望把很多次鎖的請求合并成一個請求,以降低短時間內(nèi)大量鎖請求、同步、釋放帶來的性能損耗。


          例如以下這段代碼的極端情況

          public class Test06 {    public static void main(String[] args) {        Object o = new Object();        synchronized (o) {            //業(yè)務(wù)邏輯處理            System.out.println("鎖粗化1");        }        synchronized (o) {            //業(yè)務(wù)邏輯處理            System.out.println("鎖粗化2");        }        synchronized (o) {            //業(yè)務(wù)邏輯處理            System.out.println("鎖粗化3");        }    }}


          上面的代碼是有三塊需要同步操作的,但在這三塊需要同步操作的代碼之間,需要做業(yè)務(wù)邏輯的工作,而這些工作只會花費很少的時間,那么我們就可以把這些工作代碼放入鎖內(nèi),將三個同步代碼塊合并成一個,以降低多次鎖請求、同步、釋放帶來的系統(tǒng)性能消耗,合并后的代碼如下:

          public class Test06 {    public static void main(String[] args) {        Object o = new Object();        synchronized (o) {            //業(yè)務(wù)邏輯處理            System.out.println("鎖粗化1");            //業(yè)務(wù)邏輯處理            System.out.println("鎖粗化2");            //業(yè)務(wù)邏輯處理            System.out.println("鎖粗化3");        }    }}


          鎖消除
          消除鎖是虛擬機另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請求鎖時間。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持。

          例如下面這段代碼
          public class Test07 {    public static void main(String[] args) {        method();    }    public static void method() {        Object o = new Object();        synchronized (o) {            System.out.println("鎖消除");        }    }}


          先簡單說一下逃逸分析是什么?然后再來分析上面這段就好理解啦
          逃逸分析:
          逃逸分析是jvm的一種優(yōu)化機制,它是默認開啟的,我們知道線程都有屬于自己的棧幀,在java中我們所創(chuàng)建對象都是存放在堆中,當(dāng)線程用完該對象之后,該對象不會立刻被回收掉,而是當(dāng)堆空間滿了,才會被垃圾回收器進行回收,然而我們開啟逃逸分析后,當(dāng)線程調(diào)用一個方法,會進行分析該方法內(nèi)的對象是否會被其他線程所共享,如果不會則創(chuàng)建對象將存儲在自己線程的棧幀中,而不會存儲在堆中,這樣當(dāng)線程調(diào)用結(jié)束對象也會隨著線程結(jié)束一同銷毀掉,這樣就減少了gc回收次數(shù)了,提高的程序的性能。


          分析上面這段代碼,前面說到過鎖消除的依據(jù)是逃逸分析,當(dāng)線程在調(diào)用我們的方法的時候,會對該方法進行逃逸分析,發(fā)現(xiàn)該方法里的對象不會被其他線程所共享,那么它會認為在里面進行加synchronized沒有任何用處,所以最后會底層會將進行優(yōu)化,將synchronized進行刪除。那么這就是鎖消除啦。

          我是黎明大大,我知道我沒有驚世的才華,也沒有超于凡人的能力,但畢竟我還有一個不屈服,敢于選擇向命運沖鋒的靈魂,和一個就是傷痕累累也要義無反顧走下去的心。


          如果您覺得本文對您有幫助,還請關(guān)注點贊一波,后期將不間斷更新更多技術(shù)文章


          掃描二維碼關(guān)注我
          不定期更新技術(shù)文章哦



          JUC并發(fā)編程之MESI緩存一致協(xié)議詳解

          JUC并發(fā)編程之Volatile關(guān)鍵字詳解

          JUC并發(fā)編程之JMM內(nèi)存模型詳解

          深入Hotspot源碼與Linux內(nèi)核理解NIO與Epoll

          基于Python爬蟲爬取有道翻譯實現(xiàn)翻譯功能

          JAVA集合之ArrayList源碼分析

          Mysql幾種join連接算法



          發(fā)現(xiàn)“在看”和“贊”了嗎,因為你的點贊,讓我元氣滿滿哦
          瀏覽 85
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久草视频福利在线 | 日本HD高清视频 | 青娱乐在线观看网址 | 成人人妻视频 | 夜夜撸视频 |