并發(fā)編程之synchronized深入理解

點(diǎn)贊再看,養(yǎng)成習(xí)慣梯醒,公眾號(hào)搜一搜【一角錢技術(shù)】關(guān)注更多原創(chuàng)技術(shù)文章。本文 GitHub org_hejianhui/JavaStudy 已收錄颠区,有我的系列文章娜饵。

前言

在并發(fā)編程中存在線程安全問(wèn)題瞻惋,主要原因有:1.存在共享數(shù)據(jù) 2.多線程共同操作共享數(shù)據(jù)括袒。關(guān)鍵字synchronized可以保證在同一時(shí)刻次兆,只有一個(gè)線程可以執(zhí)行某個(gè)方法或某個(gè)代碼塊,同時(shí)synchronized可以保證一個(gè)線程的變化可見(jiàn)(可見(jiàn)性)锹锰,即可以代替volatile芥炭。

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

多線程編程中漓库,有可能會(huì)出現(xiàn)多個(gè)線程同時(shí)訪問(wèn)同一個(gè)共享、可變資源的情況园蝠,這個(gè)資源我們稱之為 臨界資源 渺蒿。這種資源可能是:對(duì)象、變量砰琢、文件等蘸嘶。

  • 共享:資源可以由多個(gè)線程同時(shí)訪問(wèn)良瞧;
  • 可變:資源可以在其生命周期內(nèi)被修改陪汽。

引出的問(wèn)題:由于線程執(zhí)行的過(guò)程是不可控的,所以需要采用同步機(jī)制來(lái)協(xié)調(diào)對(duì)對(duì)象可變狀態(tài)的訪問(wèn)褥蚯。

如何解決線程并發(fā)安全問(wèn)題挚冤?

實(shí)際上,所有的并發(fā)模式在解決線程安全問(wèn)題時(shí)赞庶,采用的方案都是 序列化訪問(wèn)臨界資源 训挡。即在同一時(shí)刻,只能有一個(gè)線程訪問(wèn)臨界資源歧强,也稱作 同步互斥訪問(wèn) 澜薄。

Java 中,提供了兩種方式來(lái)實(shí)現(xiàn)同步互斥訪問(wèn):synchronizedLock

同步器的本質(zhì)就是加鎖摊册。加鎖目的:序列化訪問(wèn)臨界資源肤京,即同一時(shí)刻只能有一個(gè)線程訪問(wèn)臨界資源(同步互斥訪問(wèn))

不過(guò)有一點(diǎn)需要區(qū)別的是:當(dāng)多個(gè)線程執(zhí)行一個(gè)方法時(shí),該方法內(nèi)部的局部變量并不是臨界資源茅特,因?yàn)檫@些局部變量是在每個(gè)線程的私有棧中忘分,引出不具有共享性,不會(huì)導(dǎo)致線程安全問(wèn)題白修。

synchronized 原理分析

synchronized 內(nèi)在鎖是一種對(duì)象鎖鎖的是對(duì)象而非引用)妒峦,作用粒度是對(duì)象,可以用來(lái)實(shí)現(xiàn)對(duì)臨界資源的同步互斥訪問(wèn)兵睛,是可重入的肯骇。

加鎖的方式:

  1. 同步實(shí)例方法,鎖是當(dāng)前實(shí)例對(duì)象

synchronized修飾非靜態(tài)方法 鎖定的是該類的實(shí)例, 同一實(shí)例在多線程中調(diào)用才會(huì)觸發(fā)同步鎖定 所以多個(gè)被synchronized修飾的非靜態(tài)方法在同一實(shí)例下 只能多線程同時(shí)調(diào)用一個(gè)祖很。

public class Juc_LockOnThisObject {

    private Integer stock = 10;

    public synchronized void decrStock() {
        --stock;
        System.out.println(ClassLayout.parseInstance(this).toPrintable());
    }
}

  1. 同步類方法笛丙,鎖是當(dāng)前類對(duì)象

synchronized修飾靜態(tài)方法 鎖定的是類本身,而不是實(shí)例, 同一個(gè)類中的所有被synchronized修飾的靜態(tài)方法, 只能多線程同時(shí)調(diào)用一個(gè)突琳。

public class Juc_LockOnClass {
    private static int stock;
    public static synchronized void decrStock(){
        System.out.println(--stock);
    }
}
  1. 同步代碼塊若债,鎖是括號(hào)里面的對(duì)象

synchronized塊 直接鎖定指定的對(duì)象,該對(duì)象在多個(gè)地方的同步鎖定塊拆融,只能多線程同時(shí)執(zhí)行其中一個(gè)蠢琳。

public class Juc_LockOnObject {

    public static Object object = new Object();

    private Integer stock = 10;

    public void decrStock() {
        //T1,T2
        synchronized (object) {
            --stock;
            if (stock <= 0) {
                System.out.println("庫(kù)存售罄");
                return;
            }
        }
    }
}

synchronized底層原理

synchronized 是基于 JVM內(nèi)置鎖 實(shí)現(xiàn)勾拉,通過(guò)內(nèi)部對(duì)象Monitor(監(jiān)視器鎖) 實(shí)現(xiàn)扣甲,基于進(jìn)入與退出 Monitor 對(duì)象實(shí)現(xiàn)方法與代碼塊同步,監(jiān)視器鎖的實(shí)現(xiàn)依賴底層操作系統(tǒng)的 Mutex lock (互斥鎖)實(shí)現(xiàn),它是一個(gè)重量級(jí)鎖性能較低灶泵。當(dāng)然,JVM 內(nèi)置鎖在1.5之后版本做了大量的優(yōu)化甜紫,如鎖粗化(Lock Coarsening)薪贫、鎖消除(Lock Eliminaction)、輕量級(jí)鎖(Lightweight Locking)已卸、偏向鎖(Biased Locking)佛玄、適應(yīng)性自旋(Adaptive Spinning)等技術(shù)來(lái)減少鎖操作的開(kāi)銷,內(nèi)置鎖的并發(fā)性能已經(jīng)基本與 Lock 持平累澡。

synchronized 關(guān)鍵字被編譯成字節(jié)碼后會(huì)被翻譯成 monitorentermonitorexit 兩條指令分別在同步塊邏輯代碼的起始位置與結(jié)束位置梦抢。

示例

public class Juc_LockOnObject {

    public static Object object = new Object();

    private Integer stock = 10;

    public void decrStock() {
        //T1,T2
        synchronized (object) {
            --stock;
            if (stock <= 0) {
                System.out.println("庫(kù)存售罄");
                return;
            }
        }
    }
}

反編譯結(jié)果如下:


Monitor 監(jiān)視器鎖

每個(gè)同步對(duì)象都有一個(gè)自己的 Monitor(監(jiān)視器鎖),加鎖過(guò)程如下所示:


任何一個(gè)對(duì)象都有一個(gè) Monitor 與之關(guān)聯(lián)愧哟,當(dāng)且一個(gè) Monitor 被持有后奥吩,它將處于鎖定狀態(tài)。

Synchronized 在JVM里面的實(shí)現(xiàn)都是 基于進(jìn)入和退出 Monitor對(duì)象 來(lái)實(shí)現(xiàn)方法同步和代碼塊同步蕊梧,雖然具體實(shí)現(xiàn)細(xì)節(jié)不一樣霞赫,但是都可以通過(guò)成對(duì)的 MonitorEnterMonitorExit 指令來(lái)實(shí)現(xiàn)。

  • monitorenter :每個(gè)對(duì)象都是一個(gè)監(jiān)視器鎖(monitor)肥矢。當(dāng) monitor 被占用時(shí)就會(huì)處于鎖定狀態(tài)端衰,線程執(zhí)行 monitorenter 指令時(shí)嘗試獲取 monitor 的所有權(quán),其過(guò)程如下:
    • 如果 monitor 的進(jìn)入數(shù)為0橄抹,則該線程進(jìn)入monitor靴迫,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者楼誓;
    • 如果線程已經(jīng)占有該 monitor玉锌,只是重新進(jìn)入,則進(jìn)入 monitor 的進(jìn)入數(shù)加1疟羹;
    • 如果其他線程已經(jīng)占有 monitor主守,則該線程進(jìn)入阻塞狀態(tài),直到 monitor 的進(jìn)入數(shù)為0榄融,再重新嘗試獲取 monitor 的所有權(quán)参淫。
  • monitorexit :執(zhí)行 monitorexit 的線程必須是 objectref 所對(duì)應(yīng)的 monitor 的所有者。指令執(zhí)行時(shí)愧杯,monitor的進(jìn)入數(shù)減1涎才,如果減1后進(jìn)入數(shù)為0,那么線程退出 monitor,不再是這個(gè) monitor 的所有者耍铜。其他被這個(gè) monitor 阻塞的線程可以嘗試去獲取這個(gè) monitor 的所有權(quán)邑闺。

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

通過(guò)上面兩段描述伴挚,我們應(yīng)該能很清楚的看出 Synchronized 的實(shí)現(xiàn)原理靶衍,Synchronized 的語(yǔ)義底層是通過(guò)一個(gè) monitor 的對(duì)象來(lái)完成,其實(shí) wait/notify 等方法也依賴于 monitor 對(duì)象茎芋,這就是為什么只有在同步的塊或者方法中才能調(diào)用 wait/nofity 等方法颅眶,否則會(huì)拋出 java.lang.IllegalMonitorStateException 的異常原因。

示例:看一個(gè)同步方法

package com.niuh;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello Word!");
    }
}

反編譯結(jié)果:


從編譯的結(jié)果來(lái)看败徊,方法的同步并沒(méi)有通過(guò)指令 monitorentermonitorexit 來(lái)完成(理論上其實(shí)也可以通過(guò)這兩條指令來(lái)實(shí)現(xiàn))帚呼,不過(guò)相對(duì)于普通方法,其常量池多了 ACC_SYNCHRONIZED 標(biāo)識(shí)符皱蹦。

JVM 就是根據(jù)該標(biāo)識(shí)符來(lái)實(shí)現(xiàn)方法的同步的:當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)識(shí)是否被設(shè)置眷蜈,如果設(shè)置了沪哺,執(zhí)行線程將先獲取 monitor,獲取成功之后才能執(zhí)行方法體酌儒,方法執(zhí)行完后再釋放 monitor辜妓。再方法執(zhí)行期間,其他任何線程都無(wú)法獲得同一個(gè) monitor 對(duì)象忌怎。

兩種同步方式本質(zhì)上沒(méi)有區(qū)別籍滴,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過(guò)字節(jié)碼來(lái)完成榴啸。兩個(gè)指令的執(zhí)行是 JVM 通過(guò)調(diào)用操作系統(tǒng)的互斥原語(yǔ)mutex 來(lái)實(shí)現(xiàn)孽惰,被阻塞的線程會(huì)被掛起,等待重新調(diào)度鸥印,會(huì)導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個(gè)態(tài)直接來(lái)回切換勋功,對(duì)性能有較大的影響。

什么是 Monitor 库说?

可以把它理解為 一個(gè)同步工具狂鞋,也可以描述為 一種同步機(jī)制,它通常被描述為一個(gè)對(duì)象潜的。與一切皆對(duì)象一樣骚揍,所有的Java對(duì)象是天生的 Monitor,每一個(gè)Java 對(duì)象都有成為 Monitor 的潛質(zhì)啰挪,因?yàn)樵贘ava的設(shè)計(jì)中信不,每一個(gè)Java對(duì)象自打娘胎里出來(lái)就帶了一把看不見(jiàn)的鎖纤掸,它叫做內(nèi)部鎖或者 Monitor鎖。也就是通常說(shuō)的 Synchronized 的對(duì)象鎖浑塞,MarkWord 鎖標(biāo)識(shí)位為10借跪,其中指針指向的是 Monitor 對(duì)象的起始地址。在 Java 虛擬機(jī)(HotSpot)中酌壕,Monitor 是由 ObjectMonitor 實(shí)現(xiàn)的掏愁,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于 HotSpot 虛擬機(jī)源碼 ObjectMonitor.hpp 文件,C++實(shí)現(xiàn)的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 記錄個(gè)數(shù)
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 處于wait狀態(tài)的線程卵牍,會(huì)被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 處于等待鎖block狀態(tài)的線程果港,會(huì)被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor 中有兩個(gè)隊(duì)列,_WaitSet_EntryList糊昙,用來(lái)保存 ObjectWaiter 對(duì)象列表(每個(gè)等待鎖的線程都會(huì)被封裝成 ObjectWaiter 對(duì)象)辛掠,_owner 指向持有 ObjectWaiter 對(duì)象的線程,當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí):

  1. 首先會(huì)先進(jìn)入 _EntryList 集合释牺,當(dāng)線程獲取到對(duì)象的 monitor 后萝衩,進(jìn)入 _owner 區(qū)域并把 monitor 中的 _owner 變量設(shè)置為當(dāng)前線程,同時(shí) monitor 中的計(jì)數(shù)器 count 加1没咙;
  2. 若線程調(diào)用 wait() 方法猩谊,將釋放當(dāng)前持有的 monitor,owner 變量恢復(fù)為 null祭刚,count 自減1牌捷,同時(shí)該線程進(jìn)入 _WaitSet 集合中等待被喚醒
  3. 若當(dāng)前線程執(zhí)行完畢涡驮,也架構(gòu)釋放 monitor(鎖) 并復(fù)位 count 的值暗甥,以便其他線程進(jìn)入獲取 monitor(鎖);

同時(shí)捉捅,Monitor 對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭 Mark Word 中(存儲(chǔ)的指針的指向)撤防,Synchronized 鎖便是通過(guò)這種方式獲取鎖的,也是為什么 Java 中任意對(duì)象可以作為鎖的原因锯梁,同時(shí) notify/notifyAll/wait 等方法會(huì)使用到 Monitor 鎖對(duì)象即碗,所以必須在同步代碼塊中使用。監(jiān)視器 Monitor 有兩種同步方式:互斥與協(xié)作陌凳。多線程環(huán)境下線程之間如果需要共享數(shù)據(jù)剥懒,需要解決互斥訪問(wèn)數(shù)據(jù)的問(wèn)題,監(jiān)視器可以確保監(jiān)視器上的數(shù)據(jù)在同一時(shí)刻只會(huì)有一個(gè)線程在訪問(wèn)合敦。

那么有個(gè)問(wèn)題來(lái)了初橘,我們知道 synchronized 加鎖加在對(duì)象上,對(duì)象是如何記錄鎖狀態(tài)的呢?答案是鎖狀態(tài)被記錄在每個(gè)對(duì)象的對(duì)象頭(Mark Word)中保檐,下面一起來(lái)認(rèn)識(shí)下對(duì)象的內(nèi)存布局耕蝉。

對(duì)象的內(nèi)存布局

HostSpot 虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三塊區(qū)域:對(duì)象頭(Header)夜只、實(shí)例數(shù)據(jù)(Instance Data)垒在、對(duì)齊填充(Padding)。

  • 對(duì)象頭:比如 hash 碼扔亥,對(duì)象所屬的年代场躯,對(duì)象鎖,鎖狀態(tài)標(biāo)識(shí)旅挤,偏向鎖(線程)ID踢关,偏向時(shí)間,數(shù)組長(zhǎng)度(數(shù)組對(duì)象)等粘茄。Java 對(duì)象頭一般占用2個(gè)機(jī)器碼(在32位虛擬機(jī)中签舞,1個(gè)機(jī)器碼等于4字節(jié),也就是32bit柒瓣,在64位虛擬機(jī)中儒搭,1個(gè)機(jī)器碼是8個(gè)字節(jié),也就是64big)嘹朗,但是 如果對(duì)象是數(shù)組類型师妙,則需要3個(gè)機(jī)器碼,因?yàn)?JVM 虛擬機(jī)可以通過(guò) Java 對(duì)象的元數(shù)據(jù)信息確定Java 對(duì)象的大小屹培,但是無(wú)法從數(shù)組的元數(shù)據(jù)來(lái)確認(rèn)數(shù)組的大小,所以用一塊記錄數(shù)組長(zhǎng)度怔檩。
  • 實(shí)例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息褪秀,包括父類的屬性信息;
  • 對(duì)齊填充:由于虛擬機(jī)要求 對(duì)象起始位置必須是8字節(jié)的整數(shù)倍薛训。填充數(shù)據(jù)不是必須存在的媒吗,僅僅是為了字節(jié)對(duì)齊;

對(duì)象頭

HotSpot 虛擬機(jī)的 對(duì)象頭 包括兩部分信息

第一部分是 “Mark Word”乙埃,用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)闸英,如哈希碼(HashCode)、GC分代年齡介袜、鎖狀態(tài)標(biāo)識(shí)甫何、線程持有的鎖、偏向鎖ID遇伞、偏向時(shí)間戳等等辙喂,它是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵

這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(暫不考慮開(kāi)啟壓縮指針的場(chǎng)景)中分別為32個(gè)和64個(gè)Bits,官方稱它為“Mark Word”巍耗。對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)的數(shù)據(jù)很多秋麸,其實(shí)已經(jīng)超出了 32、64 位Bitmap 結(jié)構(gòu)所能記錄的限度炬太,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本灸蟆,考慮到虛擬機(jī)的空間效率, Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的信息亲族,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用到自己的存儲(chǔ)空間炒考。

例如在32位的HotSpot虛擬機(jī)中對(duì)象未被鎖定的狀態(tài)下,Mark Word 的32位Bits空間中的:

  • 25Bits用于存儲(chǔ)對(duì)象哈希碼(HashCode)
  • 4Bits用于存儲(chǔ)對(duì)象分代年齡
  • 2Bits用于存儲(chǔ)鎖標(biāo)識(shí)位孽水,
  • 1Bits固定為0
  • 其他狀態(tài)(輕量級(jí)鎖定票腰、重量級(jí)鎖定、GC標(biāo)記女气、可偏向)下的對(duì)象存儲(chǔ)內(nèi)容如下表所示

但是如果對(duì)象是數(shù)組類型杏慰,則需要三個(gè)機(jī)器碼,因?yàn)镴VM虛擬機(jī)可以通過(guò)Java 對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小炼鞠,但是無(wú)法從數(shù)組的元數(shù)據(jù)來(lái)確認(rèn)數(shù)組的大小缘滥,所以用一塊記錄數(shù)組的長(zhǎng)度。

對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本谒主,但是考慮到虛擬機(jī)的空間效率朝扼,Mark Word 被設(shè)計(jì)稱一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù),它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間霎肯,也就是說(shuō)擎颖,Mark Word會(huì)隨著程序的運(yùn)行發(fā)生變化。變化狀態(tài)如下:

32位虛擬機(jī)

64位虛擬機(jī)


現(xiàn)在我們虛擬機(jī)基本是64位的观游,而64位的對(duì)象頭有點(diǎn)浪費(fèi)空間搂捧,JVM默認(rèn)會(huì)開(kāi)啟指針壓縮,所以基本上也是按32位的形式記錄對(duì)象頭的懂缕。

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

哪些信息會(huì)被壓縮允跑?

  1. 對(duì)象的全局靜態(tài)變量(即類屬性)
  2. 對(duì)象頭信息:64位平臺(tái)下,原生對(duì)象頭大小為16字節(jié)搪柑,壓縮后為12字節(jié)
  3. 對(duì)象的引用類型:64位平臺(tái)下聋丝,引用類型本身大小為8字節(jié),壓縮后為4字節(jié)
  4. 對(duì)象數(shù)組類型:64位平臺(tái)下工碾,數(shù)組類型本身大小為24字節(jié)弱睦,壓縮后16字節(jié)

在Scott oaks寫的《java性能權(quán)威指南》第八章8.22節(jié)提到了當(dāng)heap size堆內(nèi)存大于32GB是用不了壓縮指針的,對(duì)象引用會(huì)額外占用20%左右的堆空間倚喂,也就意味著要38GB的內(nèi)存才相當(dāng)于開(kāi)啟了指針壓縮的32GB堆空間每篷。

這是為什么呢瓣戚?看下面引用中的紅字(來(lái)自openjdk wiki:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops)。32bit最大尋址空間是4GB焦读,開(kāi)啟了壓縮指針之后呢子库,一個(gè)地址尋址不再是1byte,而是8byte矗晃,因?yàn)椴还苁?2bit的機(jī)器還是64bit的機(jī)器仑嗅,java對(duì)象都是8byte對(duì)齊的,而類是java中的基本單位张症,對(duì)應(yīng)的堆內(nèi)存中都是一個(gè)一個(gè)的對(duì)象仓技。

Compressed oops represent managed pointers (in many but not all places in the JVM) as 32-bit values which must be scaled by a factor of 8 and added to a 64-bit base address to find the object they refer to. This allows applications to address up to four billion objects (not bytes), or a heap size of up to about 32Gb. At the same time, data structure compactness is competitive with ILP32 mode.

對(duì)象頭分析工具

運(yùn)行時(shí)對(duì)象頭鎖狀態(tài)分析工具JOL,他是OpenJDK開(kāi)源工具包俗他,引入下方maven依賴

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

打印markword

System.out.println(ClassLayout.parseInstance(object).toPrintable());
//object為我們的鎖對(duì)象

鎖的膨脹升級(jí)過(guò)程

鎖的狀態(tài)總共有四種脖捻,無(wú)鎖狀態(tài)、偏向鎖兆衅、輕量級(jí)鎖和重量級(jí)鎖地沮。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖羡亩,再升級(jí)到重量級(jí)鎖摩疑,但是鎖的升級(jí)是單向的,也就是說(shuō)只能從低到高升級(jí)畏铆,不會(huì)出現(xiàn)鎖的降級(jí)雷袋。從JDK1.6中默認(rèn)是開(kāi)啟偏向鎖和輕量級(jí)鎖的,可以通過(guò) -XX:-UseBiasedLocking 來(lái)禁用偏向鎖辞居。下圖為鎖的升級(jí)全過(guò)程:

synchronized鎖實(shí)現(xiàn)與升級(jí)過(guò)程.png

偏向鎖

偏向鎖 是Java 6 之后加入的新鎖楷怒,它是一種針對(duì)加鎖操作的優(yōu)化手段,經(jīng)過(guò)研究發(fā)現(xiàn)瓦灶,在大多數(shù)情況下率寡,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得倚搬,因此為來(lái)減少同一線程獲取鎖(會(huì)涉及到一些 CAS 操作,耗時(shí))的代價(jià)而引入偏向鎖乾蛤。偏向鎖的核心思想是每界,如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式家卖,此時(shí) Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)眨层,當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無(wú)需再做任何同步操作上荡,即獲取鎖的過(guò)程趴樱,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作馒闷,從而也就提供程序的性能。所以叁征,對(duì)于沒(méi)有競(jìng)爭(zhēng)的場(chǎng)合纳账,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖捺疼。但是對(duì)于鎖競(jìng)爭(zhēng)畢竟激烈的場(chǎng)合疏虫,偏向鎖就失效了,因?yàn)檫@樣的場(chǎng)合極有可能每次申請(qǐng)鎖的線程都是不同的啤呼,因此這種場(chǎng)合下不應(yīng)該使用偏向鎖卧秘,否則會(huì)得不償失,需要注意的是官扣,偏向鎖失敗后翅敌,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖惕蹄。

默認(rèn)開(kāi)啟偏向鎖
開(kāi)啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
關(guān)閉偏向鎖:-XX:-UseBiasedLocking

輕量級(jí)鎖

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

自旋鎖

輕量級(jí)鎖失敗后婿滓,虛擬機(jī)為了避免線程真實(shí)地操作系統(tǒng)層面掛起老速,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下凸主,線程持有鎖的時(shí)間都不會(huì)太長(zhǎng)橘券,如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)卿吐,這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間旁舰,時(shí)間成本相對(duì)較高,因此自旋鎖會(huì)假設(shè)在不久將來(lái)嗡官,當(dāng)前的線程可以獲得鎖箭窜,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久衍腥,可能是50個(gè)循環(huán)或100個(gè)循環(huán)磺樱,在經(jīng)過(guò)若干次循環(huán)后纳猫,如果得到鎖,就順利進(jìn)入臨界區(qū)竹捉。如果還不能獲得鎖芜辕,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式活孩,這種方式確實(shí)也是可以提升效率的物遇。最后沒(méi)有辦法也就只能升級(jí)為重量級(jí)鎖了。

鎖粗化

通常情況下憾儒,為了保證多線程間的有效并發(fā)询兴,會(huì)要求每個(gè)線程持有鎖的時(shí)間盡可能短,但是在某些情況下起趾,一個(gè)程序?qū)ν粋€(gè)鎖不間斷诗舰、高頻地請(qǐng)求、同步與釋放训裆,會(huì)消耗掉一定的系統(tǒng)資源眶根,因?yàn)殒i的講求、同步與釋放本身會(huì)帶來(lái)性能損耗边琉,這樣高頻的鎖請(qǐng)求就反而不利于系統(tǒng)性能的優(yōu)化了属百,雖然單次同步操作的時(shí)間可能很短。鎖粗化就是告訴我們?nèi)魏问虑槎加袀€(gè)度变姨,有些情況下我們反而希望把很多次鎖的請(qǐng)求合并成一個(gè)請(qǐng)求族扰,以降低短時(shí)間內(nèi)大量鎖請(qǐng)求、同步定欧、釋放帶來(lái)的性能損耗渔呵。

一種極端的情況如下:


public void doSomethingMethod(){
    synchronized(lock){
        //do some thing
    }
    //這是還有一些代碼,做其它不需要同步的工作砍鸠,但能很快執(zhí)行完畢
    synchronized(lock){
        //do other thing
    }
}

上面的代碼是有兩塊需要同步操作的扩氢,但在這兩塊需要同步操作的代碼之間,需要做一些其它的工作爷辱,而這些工作只會(huì)花費(fèi)很少的時(shí)間录豺,那么我們就可以把這些工作代碼放入鎖內(nèi),將兩個(gè)同步代碼塊合并成一個(gè)饭弓,以降低多次鎖請(qǐng)求巩检、同步、釋放帶來(lái)的系統(tǒng)性能消耗示启,合并后的代碼如下:

public void doSomethingMethod(){
    //進(jìn)行鎖粗化:整合成一次鎖請(qǐng)求、同步领舰、釋放
    synchronized(lock){
        //do some thing
        //做其它不需要同步但能很快執(zhí)行完的工作
        //do other thing
    }
}

注意:這樣做是有前提的夫嗓,就是中間不需要同步的代碼能夠很快速地完成迟螺,如果不需要同步的代碼需要花很長(zhǎng)時(shí)間,就會(huì)導(dǎo)致同步塊的執(zhí)行需要花費(fèi)很長(zhǎng)的時(shí)間舍咖,這樣做也就不合理了矩父。

另一種需要鎖粗化的極端的情況是:

for(int i=0;i<size;i++){
    synchronized(lock){
    }
}

上面代碼每次循環(huán)都會(huì)進(jìn)行鎖的請(qǐng)求、同步與釋放排霉,看起來(lái)貌似沒(méi)什么問(wèn)題窍株,且在jdk內(nèi)部會(huì)對(duì)這類代碼鎖的請(qǐng)求做一些優(yōu)化,但是還不如把加鎖代碼寫在循環(huán)體的外面攻柠,這樣一次鎖的請(qǐng)求就可以達(dá)到我們的要求球订,除非有特殊的需要:循環(huán)需要花很長(zhǎng)時(shí)間,但其它線程等不起瑰钮,要給它們執(zhí)行的機(jī)會(huì)冒滩。

鎖粗化后的代碼如下:

synchronized(lock){
    for(int i=0;i<size;i++){
    }
}

鎖消除

鎖消除是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底浪谴,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單立即為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯开睡,又稱為即時(shí)編譯),通過(guò)對(duì)運(yùn)行上下文的掃描苟耻,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖篇恒,通過(guò)這種方式消除沒(méi)有必要的鎖,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間凶杖,如下 StringBufferappend 是一個(gè)同步方法胁艰,但是在 add 方法中的 StringBuffer 屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用官卡,因此 StringBuffer 不可能在共享資源競(jìng)爭(zhēng)的情景蝗茁,JVM 會(huì)自動(dòng)將其鎖消除。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持寻咒。

鎖消除哮翘,前提是java必須運(yùn)行在 server模式(server模式會(huì)比client模式做更多的優(yōu)化),同時(shí)必須開(kāi)啟逃逸分析

-XX:+DoEscapeAnalysis 開(kāi)啟逃逸分析
-XX:+EliminateLoacks  表示開(kāi)啟鎖消除

鎖消除是發(fā)生在編譯器級(jí)別的一種鎖優(yōu)化方式毛秘,有時(shí)候我們寫的代碼完全不需要加鎖饭寺,卻執(zhí)行了加鎖操作。比如叫挟,StringBuffer 類的 append 操作

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

從源碼中可以看出艰匙,append 方法使用了 synchronized 關(guān)鍵字,它是線程安全的抹恳。但我們可能僅在線程內(nèi)部把 StringBuffer 當(dāng)作局部變量使用:

package com.niuh;

public class Juc_LockAppend {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int size = 10000;
        for (int i = 0; i < size; i++) {
            createStringBuffer("一角錢技術(shù)", "為分享技術(shù)而生");
        }
        long timeCost = System.currentTimeMillis() - start;
        System.out.println("createStringBuffer:" + timeCost + " ms");
    }

    public static String createStringBuffer(String str1, String str2) {
        StringBuffer sBuf = new StringBuffer();
        sBuf.append(str1);// append方法是同步操作
        sBuf.append(str2);
        return sBuf.toString();
    }
}

代碼中 createStringBuffer 方法中的局部對(duì)象 sBuf 员凝,就只在該方法內(nèi)的作用域有效,不同線程同時(shí)調(diào)用 createStringBuffer() 方法時(shí)奋献,都會(huì)創(chuàng)建不同的 sBuf 對(duì)象健霹,因此此時(shí)的 append 操作若是使用同步操作旺上,就是拜拜浪費(fèi)系統(tǒng)資源。

這時(shí)我們可以通過(guò)編譯器將其優(yōu)化糖埋,將鎖消除 宣吱,前提是 Java 必須運(yùn)行在 server 模式(server模式會(huì)比client模式作更多的優(yōu)化),同時(shí)必須開(kāi)啟逃逸分析:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

逃逸分析:比如上面的代碼瞳别,它要看 sBuf 是否可能逃出它的作用域征候?如果將 sBuf 作為方法的返回值進(jìn)行返回,那么它在方法外部可能被當(dāng)作一個(gè)全局對(duì)象使用祟敛,就有可能發(fā)生線程安全問(wèn)題疤坝,這時(shí)就可以說(shuō) sBuf 這個(gè)對(duì)象發(fā)生逃逸了,因而不應(yīng)將 append 操作的鎖消除垒棋,但我們上面的代碼沒(méi)有發(fā)生鎖逃逸卒煞,鎖消除就可以帶來(lái)一定的性能提升。

逃逸分析

使用逃逸分析叼架,編譯器可以對(duì)代碼做如下優(yōu)化:

  1. 同步省略畔裕。如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問(wèn)到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步乖订。
  2. 將堆分配轉(zhuǎn)化為棧分配扮饶。如果一個(gè)對(duì)象在子程序中被分配,要使指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸乍构,對(duì)象可能是棧分配的候選甜无,而不是堆分配。
  3. 分離對(duì)象或標(biāo)量替換哥遮。有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)也可以被訪問(wèn)到岂丘,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而是存儲(chǔ)在CPU寄存器中眠饮。

是不是所有的對(duì)象和數(shù)組都會(huì)在堆內(nèi)存分配空間奥帘?答案是:不一定

在Java代碼運(yùn)行時(shí),通過(guò) JVM 參數(shù)可以指定釋放開(kāi)啟逃逸分析:

-XX:+DoEscapeAnalysis  表示開(kāi)啟逃逸分析
-XX:-DoEscapeAnalysis  表示關(guān)閉逃逸分析

從JDK1.7 開(kāi)始已經(jīng)默認(rèn)開(kāi)啟逃逸分析仪召,如需關(guān)閉寨蹋,需要指定 -XX:-DoEscapeAnalysis

逃逸分析案例:循環(huán)創(chuàng)建50W次 NiuhStudent 對(duì)象

package com.niuh;

public class T0_ObjectStackAlloc {

    /**
     * 進(jìn)行兩種測(cè)試
     * 關(guān)閉逃逸分析,同時(shí)調(diào)大堆空間扔茅,避免堆內(nèi)GC的發(fā)生已旧,如果有GC信息將會(huì)被打印出來(lái)
     * VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
     *
     * 開(kāi)啟逃逸分析
     * VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
     *
     * 執(zhí)行main方法后
     * jps 查看進(jìn)程
     * jmap -histo 進(jìn)程ID
     */
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        //查看執(zhí)行時(shí)間
        System.out.println("cost-time " + (end - start) + " ms");
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

    private static NiuhStudent alloc() {
        //Jit對(duì)編譯時(shí)會(huì)對(duì)代碼進(jìn)行 逃逸分析
        //并不是所有對(duì)象存放在堆區(qū),有的一部分存在線程椪倌龋空間
        NiuhStudent student = new NiuhStudent();
        return student;
    }

    static class NiuhStudent {
        private String name;
        private int age;
    }
}

第一種情況:關(guān)閉逃逸分析运褪,同時(shí)調(diào)大堆空間,避免堆內(nèi)GC的發(fā)生,如果有GC信息將會(huì)被打印出來(lái)

設(shè)置VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

如下所示創(chuàng)建了50W個(gè)實(shí)例對(duì)象


第二種情況:開(kāi)啟逃逸分析

設(shè)置VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

如下未創(chuàng)建50W個(gè)實(shí)例對(duì)象


PS:以上代碼提交在 Githubhttps://github.com/Niuh-Study/niuh-juc-final.git

文章持續(xù)更新吐句,可以公眾號(hào)搜一搜「 一角錢技術(shù) 」第一時(shí)間閱讀胁后, 本文 GitHub org_hejianhui/JavaStudy 已經(jīng)收錄,歡迎 Star嗦枢。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市屯断,隨后出現(xiàn)的幾起案子文虏,更是在濱河造成了極大的恐慌,老刑警劉巖殖演,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氧秘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡趴久,警方通過(guò)查閱死者的電腦和手機(jī)丸相,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)彼棍,“玉大人灭忠,你說(shuō)我怎么就攤上這事∽叮” “怎么了弛作?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)华匾。 經(jīng)常有香客問(wèn)我映琳,道長(zhǎng),這世上最難降的妖魔是什么蜘拉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任萨西,我火速辦了婚禮,結(jié)果婚禮上旭旭,老公的妹妹穿的比我還像新娘谎脯。我一直安慰自己,他們只是感情好您机,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布穿肄。 她就那樣靜靜地躺著,像睡著了一般际看。 火紅的嫁衣襯著肌膚如雪咸产。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,816評(píng)論 1 290
  • 那天仲闽,我揣著相機(jī)與錄音脑溢,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛屑彻,可吹牛的內(nèi)容都是我干的验庙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼社牲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼粪薛!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起搏恤,我...
    開(kāi)封第一講書(shū)人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤违寿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后熟空,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體藤巢,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年息罗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掂咒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迈喉,死狀恐怖绍刮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弊添,我是刑警寧澤录淡,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站油坝,受9級(jí)特大地震影響嫉戚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜澈圈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一彬檀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞬女,春花似錦窍帝、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至报慕,卻和暖如春深浮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背眠冈。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工飞苇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓布卡,卻偏偏與公主長(zhǎng)得像雨让,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忿等,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容