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

在多線程環(huán)境中,Synchronized到底該不該用?

開發(fā) 前端
在多線程環(huán)境中,鎖的使用是避免不了的,使用鎖時(shí)候有多種鎖供我們選擇,比如 ReentrantLock、CountDownLatch等等,但是作為 Java 開發(fā)者來說,剛剛接觸多線程的時(shí)候,最早接觸和使用的恐怕非 synchronized莫屬了。

[[352836]]

在多線程環(huán)境中,鎖的使用是避免不了的,使用鎖時(shí)候有多種鎖供我們選擇,比如 ReentrantLock、CountDownLatch等等,但是作為 Java 開發(fā)者來說,剛剛接觸多線程的時(shí)候,最早接觸和使用的恐怕非 synchronized莫屬了。那你真的了解synchronized嗎,今天我們就從以下幾個(gè)方面徹底搞懂 synchronized。

 

首先有一點(diǎn)要說明一下,各位可能或多或少都聽過這樣的說法:“synchronized 的性能不行,比顯式鎖差很多,開發(fā)中還是要慎用。”

大可不必有這樣的顧慮,要說在 JDK 1.6 之前,synchronized 的性能確實(shí)有點(diǎn)差,但是 JDK 1.6 之后,JDK 開發(fā)團(tuán)隊(duì)已經(jīng)持續(xù)對(duì) synchronized 做了性能優(yōu)化,其性能已經(jīng)與其他顯式鎖基本沒有差距了。所以,在考慮是不是使用 synchronized的時(shí)候,只需要根據(jù)場(chǎng)景是否合適來決定,性能問題不用作為衡量標(biāo)準(zhǔn)。

使用方法synchronized 是一個(gè)關(guān)鍵字,它的一個(gè)明顯特點(diǎn)就是使用簡(jiǎn)單,一個(gè)關(guān)鍵字搞定。它可以在一個(gè)方法上使用,也可以在一個(gè)方法中的某些代碼塊上使用,非常方便。

  1. public class SyncLock { 
  2.  
  3.    private Object lock = new Object(); 
  4.    
  5.     /** 
  6.      * 直接在方法上加關(guān)鍵字 
  7.      */ 
  8.     public synchronized void methodLock() { 
  9.         System.out.println(Thread.currentThread().getName()); 
  10.     } 
  11.  
  12.     /** 
  13.      * 在代碼塊上加關(guān)鍵字,鎖住當(dāng)前實(shí)例 
  14.      */ 
  15.     public void codeBlockLock() { 
  16.         synchronized (this) { 
  17.             System.out.println(Thread.currentThread().getName()); 
  18.         } 
  19.     } 
  20.    
  21.    /** 
  22.      * 在代碼塊上加關(guān)鍵字,鎖住一個(gè)變量 
  23.      */ 
  24.     public void codeBlockLock() { 
  25.         synchronized (lock) { 
  26.             System.out.println(Thread.currentThread().getName()); 
  27.         } 
  28.     } 

依靠 JVM 中的 monitorenter 和 monitorexit 指令控制。通過 javap -v命令可以看到前面的實(shí)例代碼中對(duì) synchronized 關(guān)鍵字在字節(jié)碼層面的處理,對(duì)于在代碼塊上加 synchronized 關(guān)鍵字的情況,會(huì)通過 monitorenter和monitorexit指令來表示同步的開始和退出標(biāo)識(shí)。而在方法上加關(guān)鍵字的情況,會(huì)用 ACC_SYNCHRONIZED作為方法標(biāo)識(shí),這是一種隱式形式,底層原理都是一樣的。

  1. public synchronized void methodLock(); 
  2.    descriptor: ()V 
  3.    flags: ACC_PUBLIC, ACC_SYNCHRONIZED 
  4.    Code: 
  5.      stack=2, locals=1, args_size=1 
  6.         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream; 
  7.         3: invokestatic  #3                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 
  8.         6: invokevirtual #4                  // Method java/lang/Thread.getName:()Ljava/lang/String; 
  9.         9: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
  10.        12: return 
  11.      LineNumberTable: 
  12.        line 12: 0 
  13.        line 13: 12 
  14.  
  15.  public void codeBlockLock(); 
  16.    descriptor: ()V 
  17.    flags: ACC_PUBLIC 
  18.    Code: 
  19.      stack=2, locals=3, args_size=1 
  20.         0: aload_0 
  21.         1: dup 
  22.         2: astore_1 
  23.         3: monitorenter     # 
  24.         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream; 
  25.         7: invokestatic  #3                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 
  26.        10: invokevirtual #4                  // Method java/lang/Thread.getName:()Ljava/lang/String; 
  27.        13: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
  28.        16: aload_1 
  29.        17: monitorexit 
  30.        18: goto          26 
  31.        21: astore_2 
  32.        22: aload_1 
  33.        23: monitorexit 
  34.        24: aload_2 
  35.        25: athrow 
  36.        26: return 

對(duì)象布局

為什么介紹 synchronized 要說到對(duì)象頭呢,這和它的鎖升級(jí)過程有關(guān)系,具體的鎖升級(jí)過程稍后會(huì)講到,作為鎖升級(jí)過程的數(shù)據(jù)支撐,必須要掌握對(duì)象頭的結(jié)構(gòu)才能了解鎖升級(jí)的完整過程。

在 Java 中,任何的對(duì)象實(shí)例的內(nèi)存布局都分為對(duì)象頭、對(duì)象實(shí)例數(shù)據(jù)和對(duì)齊填充數(shù)據(jù)三個(gè)部分,其中對(duì)象頭又包括 MarkWord 和 類型指針。 

 

對(duì)象實(shí)例數(shù)據(jù): 這部分就是對(duì)象的實(shí)際數(shù)據(jù)。

對(duì)齊填充: 因?yàn)?HotSpot 虛擬機(jī)內(nèi)存管理要求對(duì)象的大小必須是8字節(jié)的整數(shù)倍,而對(duì)象頭正好是8個(gè)字節(jié)的整數(shù)倍,但是實(shí)例數(shù)據(jù)不一定,所以需要對(duì)齊填充補(bǔ)全。

對(duì)象頭:

Klass 指針: 對(duì)象頭中的 Klass 指針是用來指向?qū)ο笏鶎兕愋偷?,一個(gè)類實(shí)例究竟屬于哪個(gè)類,需要有地方記錄,就在這里記。

MarkWord: 還有一部分就是和 synchronized 緊密相關(guān)的 MarkWord 了,主要用來存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如hashcode、gc 分代年齡等信息。MarkWord 的位長度為 JVM 的一個(gè) Word 大小,32位 JVM 的大小為32位,64位JVM的大小為64位。

下圖是 64 位虛擬機(jī)下的 MarkWord 結(jié)構(gòu)說明,根據(jù)對(duì)象鎖狀態(tài)不同,某些比特位代表的含義會(huì)動(dòng)態(tài)的變化,之所以要這么設(shè)計(jì),是因?yàn)椴幌胱寣?duì)象頭占用過大的空間,如果為每一個(gè)標(biāo)示都分配固定的空間,那對(duì)象頭占用的空間將會(huì)比較大。

 

數(shù)組長度: 要說明一下,如果是數(shù)組對(duì)象的話, 由于數(shù)組無法通過本身內(nèi)容求得自身長度,所以需要在對(duì)象頭中記錄數(shù)組的長度。

源碼中的定義

追根溯源,對(duì)象在 JVM 中是怎么定義的呢?打開 JVM 源碼,找到其中對(duì)象的定義文件,可以看到關(guān)于前面說的對(duì)象頭的定義。

  1. class oopDesc { 
  2.   friend class VMStructs; 
  3.   friend class JVMCIVMStructs; 
  4.  private: 
  5.   volatile markOop _mark; 
  6.   union _metadata { 
  7.     Klass*      _klass; 
  8.     narrowKlass _compressed_klass; 
  9.   } _metadata; 
  10. }   

oop 是對(duì)象的基礎(chǔ)類定義,也就是或 Java 中的 Object 類的定義其實(shí)就是用的 oop,而任何類都由 Object 繼承而來。oopDesc 只是 oop 的一個(gè)別名而已。

可以看到里面有關(guān)于 Klass 的聲明,還有 markOop 的聲明,這個(gè) markOop 就是對(duì)應(yīng)上面說到的 MarkWord。

  1. class markOopDesc: public oopDesc { 
  2.  private: 
  3.   // Conversion 
  4.   uintptr_t value() const { return (uintptr_t) this; } 
  5.  
  6.  public
  7.   // Constants 
  8.   enum { age_bits                 = 4, //分代年齡 
  9.          lock_bits                = 2, //鎖標(biāo)志位 
  10.          biased_lock_bits         = 1, //偏向鎖標(biāo)記   
  11.          max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits, 
  12.          hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits, 
  13.          cms_bits                 = LP64_ONLY(1) NOT_LP64(0), 
  14.          epoch_bits               = 2 
  15.   }; 
  16. }   

以上代碼只是截取了其中一部分,可以看到其中有關(guān)于分代年齡、鎖標(biāo)志位、偏向鎖的定義。

雖然源碼咱也看不太懂,但是當(dāng)我看到它們的時(shí)候,恍惚之間,內(nèi)心會(huì)感嘆到,原來如此。有種宇宙之間,已盡在我掌控之中的感覺。過兩天才發(fā)現(xiàn),原來只是一種心理安慰。但是,已經(jīng)不重要了。

提示

如果你有興趣翻源碼看看,這部分的定義在 /src/hotspot/share/oops目錄下,能告訴你的就這么多了。

鎖升級(jí)JDK 1.6 之后,對(duì) synchronized 做了優(yōu)化,主要就是 CAS 自旋、鎖消除、鎖膨脹、輕量級(jí)鎖、偏向鎖等,這些技術(shù)都是為了在線程之間更高效地共享數(shù)據(jù)及解決競(jìng)爭(zhēng)問題,從而提高程序的執(zhí)行效率,進(jìn)而產(chǎn)生了一套鎖升級(jí)的規(guī)則。

 

synchronized 的鎖升級(jí)過程是通過動(dòng)態(tài)改變對(duì)象 MarkWord 各個(gè)標(biāo)志位來表示當(dāng)前的鎖狀態(tài)的,那修改的是哪個(gè)對(duì)象的 MarkWord 呢,看上面的代碼中,synchronized 關(guān)鍵字是加在 lock 變量上的,那就會(huì)控制 lock 的 MarkWord。如果是 synchronized(this)或者在方法上加關(guān)鍵字,那控制的就是當(dāng)前實(shí)例對(duì)象的 MarkWord。 

 

synchronized 的核心準(zhǔn)則概括起來大概是這個(gè)樣子。

  1. 能不加鎖就不加鎖。
  2. 能偏向就盡量偏向。
  3. 能加輕量級(jí)鎖就不用重量級(jí)鎖。

無鎖轉(zhuǎn)向偏向鎖

偏向鎖的意思是說,這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來的執(zhí)行過程中,該鎖一直沒有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。

 

當(dāng)線程嘗試獲取鎖對(duì)象的時(shí)候,先檢查 MarkWord 中的線程ID 是否為空。如果為空,則虛擬機(jī)會(huì)將 MarkWord 中的偏向標(biāo)記設(shè)置為 1,鎖標(biāo)記位為 01。同時(shí),使用 CAS 操作嘗試將線程ID記錄到 MarkWord 中,如果 CAS 操作成功,那之后這個(gè)持有偏向鎖的線程再次進(jìn)入相關(guān)同步塊的時(shí)候,將不需要再進(jìn)行任何的同步操作。

如果檢查線程ID不為空,并且不為當(dāng)前線程ID,或者進(jìn)行 CAS 操作設(shè)置線程ID失敗的情況下,都要撤銷偏向狀態(tài),這時(shí)候就要升級(jí)為偏向鎖了。

 

偏向鎖升級(jí)到輕量級(jí)鎖

當(dāng)多個(gè)線程競(jìng)爭(zhēng)鎖時(shí),偏向鎖會(huì)向輕量級(jí)鎖狀態(tài)升級(jí)。

 

首先,線程嘗試獲取鎖的時(shí)候,先檢查鎖標(biāo)志為是否為 01 狀態(tài),也就是未鎖定狀態(tài)。

如果是未鎖定狀態(tài),那就在當(dāng)前線程的棧幀中建立一個(gè)鎖記錄(Lock Record)區(qū)域,這個(gè)區(qū)域存儲(chǔ) MarkWord 的拷貝。

之后,嘗試用 CAS 操作將 MarkWord 更新為指向鎖記錄的指針(就是上一步在線程棧幀中的 MarkWord 拷貝),如果 CAS 更新成功了,那偏向鎖正式升級(jí)為輕量級(jí)鎖,鎖標(biāo)志為變?yōu)?00。

 

如果 CAS 更新失敗了,那檢查 MarkWord 是否已經(jīng)指向了當(dāng)前線程的鎖記錄,如果已經(jīng)指向自己,那表示已經(jīng)獲取了鎖,否則,輕量級(jí)鎖要膨脹為重量級(jí)鎖。

 

輕量級(jí)鎖升級(jí)到重量級(jí)鎖

上面的圖中已經(jīng)有了關(guān)于輕量級(jí)鎖膨脹為重量級(jí)鎖的邏輯。當(dāng)鎖已經(jīng)是輕量級(jí)鎖的狀態(tài),再有其他線程來競(jìng)爭(zhēng)鎖,此時(shí)輕量級(jí)鎖就會(huì)膨脹為重量級(jí)鎖。

 

重量級(jí)鎖的實(shí)現(xiàn)原理

為什么叫重量級(jí)鎖呢?在重量級(jí)鎖中沒有競(jìng)爭(zhēng)到鎖的對(duì)象會(huì) park 被掛起,退出同步塊時(shí) unpark 喚醒后續(xù)線程。喚醒操作涉及到操作系統(tǒng)調(diào)度會(huì)有額外的開銷,這就是它被稱為重量級(jí)鎖的原因。

當(dāng)鎖升級(jí)為重量級(jí)鎖的時(shí)候,MarkWord 會(huì)指向重量級(jí)鎖的指針 monitor,monitor 也稱為管程或監(jiān)視器鎖, 每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián) ,對(duì)象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如monitor可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成,但當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。

ObjectMonitor中有兩個(gè)隊(duì)列,_WaitSet 和 _EntryList,用來保存 ObjectWaiter 對(duì)象列表( 每個(gè)等待鎖的線程都會(huì)被封裝成 ObjectWaiter對(duì)象),_owner 指向持有 ObjectMonitor 對(duì)象的線程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí),首先會(huì)進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對(duì)象的monitor 后進(jìn)入 _Owner 區(qū)域并把 monitor 中的 owner 變量設(shè)置為當(dāng)前線程同時(shí) monitor 中的計(jì)數(shù)器 count 加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的 monitor,owner 變量恢復(fù)為 null,count 自減1,同時(shí)該線程進(jìn)入 WaitSet 集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放 monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取 monitor(鎖)

monitor 對(duì)象存在于每個(gè) Java 對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向),synchronized 鎖便是通過這種方式獲取鎖的,也是為什么 Java 中任意對(duì)象可以作為鎖的原因,同時(shí)也是notify/notifyAll/wait等方法存在于頂級(jí)對(duì)象Object中的原因。

適用場(chǎng)景

偏向鎖

優(yōu)點(diǎn): 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級(jí)的差距。

缺點(diǎn): 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來額外的鎖撤銷的消耗。

適用場(chǎng)景: 適用于只有一個(gè)線程訪問同步塊場(chǎng)景。

有的同學(xué)可能會(huì)有疑惑,適用于只有一個(gè)線程的場(chǎng)景是什么鬼,一個(gè)線程還加什么鎖。

要知道,有些鎖不是你想不加就不加的。比方說你在使用一個(gè)第三方庫,調(diào)用它里面的一個(gè) API,你雖然知道是在單線程下使用,并不需要加鎖,但是第三方庫不知道啊,你調(diào)用的這個(gè) API 正好是用 synchronized 做了同步的。這種情況下,使用偏向鎖可以達(dá)到最高的性能。

輕量級(jí)鎖

優(yōu)點(diǎn): 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度。

缺點(diǎn): 如果始終得不到鎖競(jìng)爭(zhēng)的線程使用自旋會(huì)消耗CPU。

適用場(chǎng)景: 追求響應(yīng)時(shí)間。同步塊執(zhí)行速度非常快。

重量級(jí)鎖

優(yōu)點(diǎn): 線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU。

缺點(diǎn): 線程阻塞,響應(yīng)時(shí)間緩慢。

適用場(chǎng)景: 追求吞吐量。同步塊執(zhí)行速度較長。

總結(jié)

1、synchronized 是可重入鎖,是一個(gè)非公平的可重入鎖,所以如果場(chǎng)景比較復(fù)雜的情況,還是要考慮其他的顯式鎖,比如 Reentrantlock、CountDownLatch等。

2、synchronized 有鎖升級(jí)的過程,當(dāng)有線程競(jìng)爭(zhēng)的情況下,除了互斥量的本身開銷外,還額外發(fā)生了CAS操作的開銷。因此在有競(jìng)爭(zhēng)的情況下,synchronized 會(huì)有一定的性能損耗。

本文轉(zhuǎn)載自微信公眾號(hào)「古時(shí)的風(fēng)箏」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系古時(shí)的風(fēng)箏公眾號(hào)。

責(zé)任編輯:武曉燕 來源: 古時(shí)的風(fēng)箏
相關(guān)推薦

2022-09-13 07:31:50

工具代碼Lombok

2023-08-14 18:03:32

2024-12-20 09:12:00

Vue項(xiàng)目Pinia

2015-10-20 09:07:44

2009-07-01 15:33:23

JAVA認(rèn)證

2016-12-22 23:55:40

架構(gòu)師代碼技術(shù)

2024-11-27 15:58:49

2021-01-18 16:43:14

人工智能互聯(lián)網(wǎng)科技

2019-11-11 07:49:00

5G套餐運(yùn)營商

2012-02-15 09:48:31

移動(dòng)娛樂廣告封殺

2013-02-18 09:40:28

2013-01-23 14:19:56

健康類App移動(dòng)應(yīng)用

2010-04-28 16:34:28

Unix操作系統(tǒng)

2019-03-28 09:44:33

程序員加薪創(chuàng)業(yè)

2014-12-30 09:30:57

.net語言選擇

2013-05-23 15:24:57

Wi-Fi無線網(wǎng)絡(luò)設(shè)置無線網(wǎng)絡(luò)展望

2018-01-26 11:20:19

電腦攝像頭硬件

2021-09-15 14:40:04

iOS蘋果系統(tǒng)

2011-04-14 13:27:53

Synchronize多線程

2020-03-09 09:33:31

網(wǎng)絡(luò)安全人臉識(shí)別技術(shù)
點(diǎn)贊
收藏

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