自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

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

 前言

  • 并發(fā)編程從操作系統(tǒng)底層工作的整體認(rèn)識(shí)開始
  • 深入理解Java內(nèi)存模型(JMM)及volatile關(guān)鍵字
  • 深入理解CPU緩存一致性協(xié)議(MESI)

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

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

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

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

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

如何解決線程并發(fā)安全問題?

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

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

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

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

synchronized 原理分析

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

加鎖的方式:

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

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

  1. public class Juc_LockOnThisObject { 
  2.  
  3.     private Integer stock = 10; 
  4.  
  5.     public synchronized void decrStock() { 
  6.         --stock; 
  7.         System.out.println(ClassLayout.parseInstance(this).toPrintable()); 
  8.     } 

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

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

  1. public class Juc_LockOnClass { 
  2.     private static int stock; 
  3.     public static synchronized void decrStock(){ 
  4.         System.out.println(--stock); 
  5.     } 

 3.同步代碼塊,鎖是括號(hào)里面的對(duì)象

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

  1. public class Juc_LockOnObject { 
  2.  
  3.     public static Object object = new Object(); 
  4.  
  5.     private Integer stock = 10; 
  6.  
  7.     public void decrStock() { 
  8.         //T1,T2 
  9.         synchronized (object) { 
  10.             --stock; 
  11.             if (stock <= 0) { 
  12.                 System.out.println("庫(kù)存售罄"); 
  13.                 return
  14.             } 
  15.         } 
  16.     } 

 synchronized底層原理

synchronized 是基于 JVM內(nèi)置鎖 實(shí)現(xiàn),通過內(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)減少鎖操作的開銷,內(nèi)置鎖的并發(fā)性能已經(jīng)基本與 Lock 持平。

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


示例:

  1. public class Juc_LockOnObject { 
  2.  
  3.     public static Object object = new Object(); 
  4.  
  5.     private Integer stock = 10; 
  6.  
  7.     public void decrStock() { 
  8.         //T1,T2 
  9.         synchronized (object) { 
  10.             --stock; 
  11.             if (stock <= 0) { 
  12.                 System.out.println("庫(kù)存售罄"); 
  13.                 return
  14.             } 
  15.         } 
  16.     } 

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


Monitor 監(jiān)視器鎖

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


任何一個(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é)不一樣,但是都可以通過成對(duì)的 MonitorEnter 和 MonitorExit 指令來(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),其過程如下:如果 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ā)生異常退出釋放鎖;

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

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

  1. package com.niuh; 
  2.  
  3. public class SynchronizedMethod { 
  4.     public synchronized void method() { 
  5.         System.out.println("Hello Word!"); 
  6.     } 

 反編譯結(jié)果:


從編譯的結(jié)果來(lái)看,方法的同步并沒有通過指令 monitorenter 和 monitorexit 來(lái)完成(理論上其實(shí)也可以通過這兩條指令來(lái)實(shí)現(xiàn)),不過相對(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 訪問標(biāo)識(shí)是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取 monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放 monitor。在方法執(zhí)行期間,其他任何線程都無(wú)法獲得同一個(gè) monitor 對(duì)象。

兩種同步方式本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過字節(jié)碼來(lái)完成。兩個(gè)指令的執(zhí)行是 JVM 通過調(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)就帶了一把看不見的鎖,它叫做內(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)的):

  1. ObjectMonitor() { 
  2.     _header       = NULL
  3.     _count        = 0; // 記錄個(gè)數(shù) 
  4.     _waiters      = 0, 
  5.     _recursions   = 0; 
  6.     _object       = NULL
  7.     _owner        = NULL
  8.     _WaitSet      = NULL; // 處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet 
  9.     _WaitSetLock  = 0 ; 
  10.     _Responsible  = NULL ; 
  11.     _succ         = NULL ; 
  12.     _cxq          = NULL ; 
  13.     FreeNext      = NULL ; 
  14.     _EntryList    = NULL ; // 處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表 
  15.     _SpinFreq     = 0 ; 
  16.     _SpinClock    = 0 ; 
  17.     OwnerIsThread = 0 ; 
  18.   } 

 ObjectMonitor 中有兩個(gè)隊(duì)列,_WaitSet 和 _EntryList,用來(lái)保存 ObjectWaiter 對(duì)象列表(每個(gè)等待鎖的線程都會(huì)被封裝成 ObjectWaiter 對(duì)象),_owner 指向持有 ObjectWaiter 對(duì)象的線程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(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 鎖便是通過這種方式獲取鎖的,也是為什么 Java 中任意對(duì)象可以作為鎖的原因,同時(shí) notify/notifyAll/wait 等方法會(huì)使用到 Monitor 鎖對(duì)象,所以必須在同步代碼塊中使用。監(jiān)視器 Monitor 有兩種同步方式:互斥與協(xié)作。多線程環(huán)境下線程之間如果需要共享數(shù)據(jù),需要解決互斥訪問數(shù)據(jù)的問題,監(jiān)視器可以確保監(jiān)視器上的數(shù)據(jù)在同一時(shí)刻只會(huì)有一個(gè)線程在訪問。

那么有個(gè)問題來(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ī)可以通過 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ī)(暫不考慮開啟壓縮指針的場(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ī)可以通過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ì)開啟指針壓縮,所以基本上也是按32位的形式記錄對(duì)象頭的。

  1. 手動(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)于開啟了指針壓縮的32GB堆空間。

這是為什么呢?看下面引用中的紅字(來(lái)自openjdk wiki:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops)。32bit最大尋址空間是4GB,開啟了壓縮指針之后呢,一個(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開源工具包,引入下方maven依賴

  1. <dependency> 
  2.     <groupId>org.openjdk.jol</groupId> 
  3.     <artifactId>jol-core</artifactId> 
  4.     <version>0.10</version> 
  5. </dependency> 

 打印markword 

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

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

鎖的狀態(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)是開啟偏向鎖和輕量級(jí)鎖的,可以通過 -XX:-UseBiasedLocking 來(lái)禁用偏向鎖。下圖為鎖的升級(jí)全過程: 


偏向鎖

偏向鎖 是Java 6 之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段,經(jīng)過研究發(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ān)鎖申請(qǐng)的操作,從而也就提供程序的性能。所以,對(duì)于沒有競(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í)鎖。

  1. 默認(rèn)開啟偏向鎖 
  2. 開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 
  3. 關(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í)間訪問同一鎖的場(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)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒有辦法也就只能升級(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)的性能損耗。

一種極端的情況如下:

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

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

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

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

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

  1. for(int i=0;i<size;i++){ 
  2.     synchronized(lock){ 
  3.     } 

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

鎖粗化后的代碼如下: 

  1. synchronized(lock){ 
  2.     for(int i=0;i<size;i++){ 
  3.     } 

 鎖消除

鎖消除是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單立即為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱為即時(shí)編譯),通過對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無(wú)意義的請(qǐng)求鎖時(shí)間,如下 StringBuffer 的 append 是一個(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í)必須開啟逃逸分析

  1. -XX:+DoEscapeAnalysis 開啟逃逸分析 
  2. -XX:+EliminateLoacks  表示開啟鎖消除 

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

  1. @Override 
  2. public synchronized StringBuffer append(String str) { 
  3.     toStringCache = null
  4.     super.append(str); 
  5.     return this; 

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

  1. package com.niuh; 
  2.  
  3. public class Juc_LockAppend { 
  4.  
  5.     public static void main(String[] args) { 
  6.         long start = System.currentTimeMillis(); 
  7.         int size = 10000; 
  8.         for (int i = 0; i < size; i++) { 
  9.             createStringBuffer("一角錢技術(shù)""為分享技術(shù)而生"); 
  10.         } 
  11.         long timeCost = System.currentTimeMillis() - start; 
  12.         System.out.println("createStringBuffer:" + timeCost + " ms"); 
  13.     } 
  14.  
  15.     public static String createStringBuffer(String str1, String str2) { 
  16.         StringBuffer sBuf = new StringBuffer(); 
  17.         sBuf.append(str1);// append方法是同步操作 
  18.         sBuf.append(str2); 
  19.         return sBuf.toString(); 
  20.     } 

 

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

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

  1. -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks 

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

逃逸分析

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

  1. 同步省略。如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問到,那么對(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)也可以被訪問到,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而是存儲(chǔ)在CPU寄存器中。

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

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

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

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

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

  1. package com.niuh; 
  2.  
  3. public class T0_ObjectStackAlloc { 
  4.  
  5.     /** 
  6.      * 進(jìn)行兩種測(cè)試 
  7.      * 關(guān)閉逃逸分析,同時(shí)調(diào)大堆空間,避免堆內(nèi)GC的發(fā)生,如果有GC信息將會(huì)被打印出來(lái) 
  8.      * VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 
  9.      * 
  10.      * 開啟逃逸分析 
  11.      * VM運(yùn)行參數(shù):-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 
  12.      * 
  13.      * 執(zhí)行main方法后 
  14.      * jps 查看進(jìn)程 
  15.      * jmap -histo 進(jìn)程ID 
  16.      */ 
  17.     public static void main(String[] args) { 
  18.         long start = System.currentTimeMillis(); 
  19.         for (int i = 0; i < 500000; i++) { 
  20.             alloc(); 
  21.         } 
  22.         long end = System.currentTimeMillis(); 
  23.         //查看執(zhí)行時(shí)間 
  24.         System.out.println("cost-time " + (end - start) + " ms"); 
  25.         try { 
  26.             Thread.sleep(100000); 
  27.         } catch (InterruptedException e1) { 
  28.             e1.printStackTrace(); 
  29.         } 
  30.     } 
  31.  
  32.     private static NiuhStudent alloc() { 
  33.         //Jit對(duì)編譯時(shí)會(huì)對(duì)代碼進(jìn)行 逃逸分析 
  34.         //并不是所有對(duì)象存放在堆區(qū),有的一部分存在線程??臻g 
  35.         NiuhStudent student = new NiuhStudent(); 
  36.         return student; 
  37.     } 
  38.  
  39.     static class NiuhStudent { 
  40.         private String name
  41.         private int age; 
  42.     } 

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

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

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

 第二種情況:開啟逃逸分析

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

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


PS:以上代碼提交在 Github :

https://github.com/Niuh-Study/niuh-juc-final.git

 

責(zé)任編輯:姜華 來(lái)源: 今日頭條
相關(guān)推薦

2020-12-11 07:32:45

編程ThreadLocalJava

2022-10-12 07:53:46

并發(fā)編程同步工具

2021-05-27 11:30:54

SynchronizeJava代碼

2022-07-04 08:01:01

鎖優(yōu)化Java虛擬機(jī)

2024-12-31 09:00:12

Java線程狀態(tài)

2019-06-25 10:32:19

UDP編程通信

2023-10-27 07:47:58

Java語(yǔ)言順序性

2024-03-19 14:14:27

線程開發(fā)

2021-03-10 15:59:39

JavaSynchronize并發(fā)編程

2021-07-26 07:47:37

無(wú)鎖編程CPU

2020-12-07 09:40:19

Future&Futu編程Java

2023-10-27 07:47:37

計(jì)算機(jī)內(nèi)存模型

2018-03-22 18:30:22

數(shù)據(jù)庫(kù)MySQL并發(fā)控制

2019-07-24 16:04:47

Java虛擬機(jī)并發(fā)

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過濾器

2024-05-24 14:35:49

2020-12-08 08:53:53

編程ThreadPoolE線程池

2013-09-22 14:57:19

AtWood
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)