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

        共 15834字,需瀏覽 32分鐘

         ·

        2021-07-03 16:43

        點擊上方藍字 關注我吧



        1
        設計同步器的意義

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


        2
        什么是Synchronized


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


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



        3
        Synchronized原理詳解


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

        第一種方式:靜態(tài)方法上加同步塊(demo比較簡陋,且鎖在哪里代碼中也詳細說到了)
        public class StaticTest01 {    /**     * 靜態(tài)方法加鎖,它的鎖就是加在 StaticTest01.class 上     * 因為是靜態(tài)方法,所以是通過類進行調用的,那么鎖就加在類上面     */    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("上鎖");    }}


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


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


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


        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ān)視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
        1.monitorenter
        1.如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設置為1,該線程即為monitor的所有者;
        2.如果線程已經占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1;
        3.如果其他線程已經占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為0,再重新嘗試獲取monitor的所有權;
        2.monitorexit
        1.執(zhí)行monitorexit的線程必須是objectref所對應的monitor的所有者。
        2.指令執(zhí)行時,monitor的進入數(shù)減1,如果減1后進入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。


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


        通過上面兩段描述,我們應該能很清楚的看出Synchronized的實現(xiàn)原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調用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("上鎖");    }}


        反編譯后的結果


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


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


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


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


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

        1.對象頭:比如 hash碼,對象所屬的年代,對象鎖,鎖狀態(tài)標志,偏向鎖(線程)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版本后,在其內部加了偏向鎖、輕量鎖等鎖技術,那么這些鎖它們是怎么進化的呢?又或者說這些鎖的信息都存放在對象哪里呢?來,往下看。


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


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

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



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


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


        說到這,前面我有說到jvm它默認開啟了指針壓縮,其實我們也可以手動將其關閉,主要看場景決定吧

        手動設置-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é)


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

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


        輕量級鎖

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


        自旋鎖

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


        運行結果圖


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


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

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


        接著,我們在這段代碼的基礎上,在加一個同步塊操作,鎖的對象則是方法中的局部變量,然后再來看看效果
        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());        }    }}


        運行結果圖


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


        運行結果圖


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


        運行結果圖


        從上圖中,首先我們看到主線程和第一個線程打印的對象頭信息,它們的頭信息都是 "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();    }}


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


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


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


        鎖粗化:

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


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

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


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

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


        鎖消除
        消除鎖是虛擬機另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執(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)建對象都是存放在堆中,當線程用完該對象之后,該對象不會立刻被回收掉,而是當堆空間滿了,才會被垃圾回收器進行回收,然而我們開啟逃逸分析后,當線程調用一個方法,會進行分析該方法內的對象是否會被其他線程所共享,如果不會則創(chuàng)建對象將存儲在自己線程的棧幀中,而不會存儲在堆中,這樣當線程調用結束對象也會隨著線程結束一同銷毀掉,這樣就減少了gc回收次數(shù)了,提高的程序的性能。


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

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


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


        掃描二維碼關注我
        不定期更新技術文章哦



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

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

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

        深入Hotspot源碼與Linux內核理解NIO與Epoll

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

        JAVA集合之ArrayList源碼分析

        Mysql幾種join連接算法



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

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
          
          

            1. 一级片老女人 | 国产一国产精品一级毛片视频 | 久久综合伊人7777777 | 91免费看毛片 | 亲子乱一区二区三区 |