synchronized原理及優(yōu)化

理解Java對(duì)象頭與Monitor

在JVM中平绩,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:對(duì)象頭手素、實(shí)例數(shù)據(jù)和對(duì)齊填充快毛。如下:
  • 實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實(shí)例部分還包括數(shù)組的長(zhǎng)度拯欧,這部分內(nèi)存按4字節(jié)對(duì)齊碉熄。

  • 填充數(shù)據(jù):由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍桨武。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊锈津。

而對(duì)于頂部呀酸,則是Java頭對(duì)象,它實(shí)現(xiàn)synchronized的鎖對(duì)象的基礎(chǔ)琼梆,這點(diǎn)我們重點(diǎn)分析它性誉,一般而言,synchronized使用的鎖對(duì)象是存儲(chǔ)在Java對(duì)象頭里的茎杂,jvm中采用2個(gè)字來(lái)存儲(chǔ)對(duì)象頭(如果對(duì)象是數(shù)組則會(huì)分配3個(gè)字错览,多出來(lái)的1個(gè)字記錄的是數(shù)組長(zhǎng)度),其主要結(jié)構(gòu)是由Mark Word 和 Class Metadata Address 組成煌往,其結(jié)構(gòu)說(shuō)明如下表:

其中Mark Word在默認(rèn)情況下存儲(chǔ)著對(duì)象的HashCode倾哺、分代年齡、鎖標(biāo)記位等以下是32位JVM的Mark Word默認(rèn)存儲(chǔ)結(jié)構(gòu)

由于對(duì)象頭的信息是與對(duì)象自身定義的數(shù)據(jù)沒(méi)有關(guān)系的額外存儲(chǔ)成本,因此考慮到JVM的空間效率羞海,Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)忌愚,以便存儲(chǔ)更多有效的數(shù)據(jù),它會(huì)根據(jù)對(duì)象本身的狀態(tài)復(fù)用自己的存儲(chǔ)空間却邓,如32位JVM下硕糊,除了上述列出的Mark Word默認(rèn)存儲(chǔ)結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):

其中輕量級(jí)鎖和偏向鎖是Java 6對(duì) synchronized 鎖進(jìn)行優(yōu)化后新增加的腊徙,稍后我們會(huì)簡(jiǎn)要分析癌幕。這里我們主要分析一下重量級(jí)鎖也就是通常說(shuō)synchronized的對(duì)象鎖,鎖標(biāo)識(shí)位為10昧穿,其中指針指向的是monitor對(duì)象(也稱為管程或監(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)饰潜。

先來(lái)舉個(gè)例子初坠,然后我們?cè)谏显创a。我們可以把監(jiān)視器理解為包含一個(gè)特殊的房間的建筑物彭雾,這個(gè)特殊房間同一時(shí)刻只能有一個(gè)客人(線程)碟刺。這個(gè)房間中包含了一些數(shù)據(jù)和代碼。

如果一個(gè)顧客想要進(jìn)入這個(gè)特殊的房間薯酝,他首先需要在走廊(Entry Set)排隊(duì)等待半沽。調(diào)度器將基于某個(gè)標(biāo)準(zhǔn)(比如 FIFO)來(lái)選擇排隊(duì)的客戶進(jìn)入房間。如果吴菠,因?yàn)槟承┰蛘咛睿摽蛻魰簳r(shí)因?yàn)槠渌虑闊o(wú)法脫身(線程被掛起),那么他將被送到另外一間專門用來(lái)等待的房間(Wait Set)做葵,這個(gè)房間的可以可以在稍后再次進(jìn)入那件特殊的房間占哟。如上面所說(shuō),這個(gè)建筑屋中一共有三個(gè)場(chǎng)所酿矢。

總之榨乎,監(jiān)視器是一個(gè)用來(lái)監(jiān)視這些線程進(jìn)入特殊的房間的。他的義務(wù)是保證(同一時(shí)間)只有一個(gè)線程可以訪問(wèn)被保護(hù)的數(shù)據(jù)和代碼瘫筐。
Monitor其實(shí)是一種同步工具蜜暑,也可以說(shuō)是一種同步機(jī)制,它通常被描述為一個(gè)對(duì)象严肪,主要特點(diǎn)是:
  • 對(duì)象的所有方法都被“互斥”的執(zhí)行史煎。好比一個(gè)Monitor只有一個(gè)運(yùn)行“許可”谦屑,任一個(gè)線程進(jìn)入任何一個(gè)方法都需要獲得這個(gè)“許可”,離開(kāi)時(shí)把許可歸還篇梭。
  • 通常提供singal機(jī)制:允許正持有“許可”的線程暫時(shí)放棄“許可”氢橙,等待某個(gè)謂詞成真(條件變量),而條件成立后恬偷,當(dāng)前進(jìn)程可以“通知”正在等待這個(gè)條件變量的線程悍手,讓他可以重新去獲得運(yùn)行許可。

監(jiān)視器的實(shí)現(xiàn)

在Java虛擬機(jī)(HotSpot)中袍患,Monitor是基于C++實(shí)現(xiàn)的坦康,由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下:

 ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有幾個(gè)關(guān)鍵屬性:

  • _owner:指向持有ObjectMonitor對(duì)象的線程

  • _WaitSet:存放處于wait狀態(tài)的線程隊(duì)列

  • _EntryList:存放處于等待鎖block狀態(tài)的線程隊(duì)列

  • _recursions:鎖的重入次數(shù)

  • _count:用來(lái)記錄該線程獲取鎖的次數(shù)

當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí)诡延,首先會(huì)進(jìn)入_EntryList隊(duì)列中滞欠,當(dāng)某個(gè)線程獲取到對(duì)象的monitor后進(jìn)入_Owner區(qū)域并把monitor中的_owner變量設(shè)置為當(dāng)前線程,同時(shí)monitor中的計(jì)數(shù)器_count加1肆良。即獲得對(duì)象鎖筛璧。

若持有monitor的線程調(diào)用wait()方法,將釋放當(dāng)前持有的monitor惹恃,_owner變量恢復(fù)為null夭谤,_count自減1,同時(shí)該線程進(jìn)入_WaitSet集合中等待被喚醒巫糙。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值朗儒,以便其他線程進(jìn)入獲取monitor(鎖)。如下圖所示

ObjectMonitor類中提供了幾個(gè)方法:

獲得鎖

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
//通過(guò)CAS嘗試把monitor的`_owner`字段設(shè)置為當(dāng)前線程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
//獲取鎖失敗
  if (cur == NULL) {         assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     // CONSIDER: set or assert OwnerIsThread == 1
     return ;
  }
//如果舊值和當(dāng)前線程一樣参淹,說(shuō)明當(dāng)前線程已經(jīng)持有鎖醉锄,此次為重入,_recursions自增承二,并獲得鎖榆鼠。
  if (cur == Self) { 
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }
//如果當(dāng)前線程是第一次進(jìn)入該monitor,設(shè)置_recursions為1亥鸠,_owner為當(dāng)前線程
  if (Self->is_lock_owned ((address)cur)) { 
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;
    // Commute owner from a thread-specific on-stack BasicLockObject address to
    // a full-fledged "Thread *".
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }
//省略部分代碼。
//通過(guò)自旋執(zhí)行ObjectMonitor::EnterI方法等待鎖的釋放
  for (;;) {
  jt->set_suspend_equivalent();
  // cleared by handle_special_suspend_equivalent_condition()
  // or java_suspend_self()
  EnterI (THREAD) ;
  if (!ExitSuspendEquivalent(jt)) break ;
  //
  // We have acquired the contended monitor, but while we were
  // waiting another thread suspended us. We don't want to enter
  // the monitor while suspended because that would surprise the
  // thread that suspended us.
  //
      _recursions = 0 ;
  _succ = NULL ;
  exit (Self) ;
  jt->java_suspend_self();
}
}

釋放鎖

void ATTR ObjectMonitor::exit(TRAPS) {
   Thread * Self = THREAD ;
//如果當(dāng)前線程不是Monitor的所有者
   if (THREAD != _owner) { 
     if (THREAD->is_lock_owned((address) _owner)) { // 
       // Transmute _owner from a BasicLock pointer to a Thread address.
       // We don't need to hold _mutex for this transition.
       // Non-null to Non-null is safe as long as all readers can
       // tolerate either flavor.
       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {
       // NOTE: we need to handle unbalanced monitor enter/exit
       // in native code by throwing an exception.
       // TODO: Throw an IllegalMonitorStateException ?
       TEVENT (Exit - Throw IMSX) ;
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }
//如果_recursions次數(shù)不為0.自減
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

省略部分代碼识啦,根據(jù)不同的策略(由QMode指定)负蚊,從cxq或EntryList中獲取頭節(jié)點(diǎn),通過(guò)ObjectMonitor::ExitEpilog方法喚醒該節(jié)點(diǎn)封裝的線程颓哮,喚醒操作最終由unpark完成家妆。

由此看來(lái),monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向)冕茅,synchronized鎖便是通過(guò)這種方式獲取鎖的伤极,也是為什么Java中任意對(duì)象可以作為鎖的原因蛹找,同時(shí)也是notify/notifyAll/wait等方法存在于頂級(jí)對(duì)象Object中的原因,在使用這3個(gè)方法時(shí)哨坪,必須處于synchronized代碼塊或者synchronized方法中庸疾,否則就會(huì)拋出IllegalMonitorStateException異常,這是因?yàn)檎{(diào)用這幾個(gè)方法前必須拿到當(dāng)前對(duì)象的監(jiān)視器monitor對(duì)象当编,也就是說(shuō)notify/notifyAll和wait方法依賴于monitor對(duì)象届慈,在前面的分析中,我們知道m(xù)onitor 存在于對(duì)象頭的Mark Word 中(存儲(chǔ)monitor引用指針)忿偷,而synchronized關(guān)鍵字可以獲取 monitor 金顿,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調(diào)用的原因。下面我們將進(jìn)一步分析synchronized在字節(jié)碼層面的具體語(yǔ)義實(shí)現(xiàn)鲤桥。

synchronized底層原理

public class SynchronizedDemo {
//同步方法
    public synchronized void doSth(){
        System.out.println("Hello World");
    }

//同步代碼塊
    public void doSth1(){
        synchronized (SynchronizedDemo.class){
            System.out.println("Hello World");
        }
    }
}

被synchronized修飾的代碼塊及方法揍拆,在同一時(shí)間,只能被單個(gè)線程訪問(wèn)茶凳。

synchronized的實(shí)現(xiàn)原理

synchronized嫂拴,是Java中用于解決并發(fā)情況下數(shù)據(jù)同步訪問(wèn)的一個(gè)很重要的關(guān)鍵字。當(dāng)我們想要保證一個(gè)共享資源在同一時(shí)間只會(huì)被一個(gè)線程訪問(wèn)到時(shí)慧妄,我們可以在代碼中使用synchronized關(guān)鍵字對(duì)類或者對(duì)象加鎖顷牌。
我們對(duì)上面的代碼進(jìn)行反編譯,可以得到如下代碼:

public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

通過(guò)反編譯后代碼可以看出:對(duì)于同步方法塞淹,JVM采用ACC_SYNCHRONIZED標(biāo)記符來(lái)實(shí)現(xiàn)同步窟蓝。 對(duì)于同步代碼塊。JVM采用monitorenter饱普、monitorexit兩個(gè)指令來(lái)實(shí)現(xiàn)同步运挫。

方法級(jí)的同步是隱式的。同步方法的常量池中會(huì)有一個(gè)ACC_SYNCHRONIZED標(biāo)志套耕。當(dāng)某個(gè)線程要訪問(wèn)某個(gè)方法的時(shí)候谁帕,會(huì)檢查是否有ACC_SYNCHRONIZED,如果有設(shè)置冯袍,則需要先獲得監(jiān)視器鎖匈挖,然后開(kāi)始執(zhí)行方法,方法執(zhí)行之后再釋放監(jiān)視器鎖康愤。這時(shí)如果其他線程來(lái)請(qǐng)求執(zhí)行方法儡循,會(huì)因?yàn)闊o(wú)法獲得監(jiān)視器鎖而被阻斷住。值得注意的是征冷,如果在方法執(zhí)行過(guò)程中择膝,發(fā)生了異常,并且方法內(nèi)部并沒(méi)有處理該異常检激,那么在異常被拋到方法外面之前監(jiān)視器鎖會(huì)被自動(dòng)釋放肴捉。

同步代碼塊使用monitorenter和monitorexit兩個(gè)指令實(shí)現(xiàn)腹侣。可以把執(zhí)行monitorenter指令理解為加鎖齿穗,執(zhí)行monitorexit理解為釋放鎖傲隶。 每個(gè)對(duì)象維護(hù)著一個(gè)記錄著被鎖次數(shù)的計(jì)數(shù)器。未被鎖定的對(duì)象的該計(jì)數(shù)器為0缤灵,當(dāng)一個(gè)線程獲得鎖(執(zhí)行monitorenter)后伦籍,該計(jì)數(shù)器自增變?yōu)?1 ,當(dāng)同一個(gè)線程再次獲得該對(duì)象的鎖的時(shí)候腮出,計(jì)數(shù)器再次自增帖鸦。當(dāng)同一個(gè)線程釋放鎖(執(zhí)行monitorexit指令)的時(shí)候,計(jì)數(shù)器再自減胚嘲。當(dāng)計(jì)數(shù)器為0的時(shí)候作儿。鎖將被釋放,其他線程便可以獲得鎖馋劈。

無(wú)論是ACC_SYNCHRONIZED還是monitorenter攻锰、monitorexit都是基于Monitor實(shí)現(xiàn)的,在Java虛擬機(jī)(HotSpot)中妓雾,Monitor是基于C++實(shí)現(xiàn)的娶吞,由ObjectMonitor實(shí)現(xiàn)。

ObjectMonitor類中提供了幾個(gè)方法械姻,如enter妒蛇、exit、wait楷拳、notify绣夺、notifyAll等。sychronized加鎖的時(shí)候欢揖,會(huì)調(diào)用objectMonitor的enter方法陶耍,解鎖的時(shí)候會(huì)調(diào)用exit方法。sychronized加鎖的時(shí)候她混,會(huì)調(diào)用objectMonitor的enter方法烈钞,解鎖的時(shí)候會(huì)調(diào)用exit方法。事實(shí)上坤按,只有在JDK1.6之前棵磷,synchronized的實(shí)現(xiàn)才會(huì)直接調(diào)用ObjectMonitor的enter和exit,這種鎖被稱之為重量級(jí)鎖晋涣。為什么說(shuō)這種方式操作鎖很重呢?

Java的線程是映射到操作系統(tǒng)原生線程之上的沉桌,如果要阻塞或喚醒一個(gè)線程就需要操作系統(tǒng)的幫忙谢鹊,這就要從用戶態(tài)轉(zhuǎn)換到核心態(tài)算吩,因此狀態(tài)轉(zhuǎn)換需要花費(fèi)很多的處理器時(shí)間,對(duì)于代碼簡(jiǎn)單的同步塊(如被synchronized修飾的get 或set方法)狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)佃扼,所以說(shuō)synchronized是java語(yǔ)言中一個(gè)重量級(jí)的操縱偎巢。

所以,在JDK1.6中出現(xiàn)對(duì)鎖進(jìn)行了很多的優(yōu)化兼耀,進(jìn)而出現(xiàn)輕量級(jí)鎖压昼,偏向鎖,鎖消除瘤运,適應(yīng)性自旋鎖窍霞,鎖粗化(自旋鎖在1.4就有 只不過(guò)默認(rèn)的是關(guān)閉的,jdk1.6是默認(rèn)開(kāi)啟的)拯坟,這些操作都是為了在線程之間更高效的共享數(shù)據(jù)但金,解決競(jìng)爭(zhēng)問(wèn)題。

Java虛擬機(jī)對(duì)synchronized的優(yōu)化

鎖的狀態(tài)總共有四種郁季,無(wú)鎖狀態(tài)冷溃、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖梦裂。隨著鎖的競(jìng)爭(zhēng)似枕,鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖年柠,但是鎖的升級(jí)是單向的凿歼,也就是說(shuō)只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí)彪杉,關(guān)于重量級(jí)鎖毅往,前面我們已詳細(xì)分析過(guò),下面我們將介紹偏向鎖和輕量級(jí)鎖以及JVM的其他優(yōu)化手段派近。

自旋鎖與自適應(yīng)自旋

前面我們討論互斥同步的時(shí)候攀唯,提到了互斥同步對(duì)性能最大的影響是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成渴丸,這些操作給系統(tǒng)的并發(fā)性能帶來(lái)了很大的壓力侯嘀。同時(shí),虛擬機(jī)的開(kāi)發(fā)團(tuán)隊(duì)也注意到在許多應(yīng)用上谱轨,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間戒幔,為了這段時(shí)間去掛起和恢復(fù)線程并不值得。如果物理機(jī)器有一個(gè)以上的處理器土童,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行诗茎,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程“稍等一 下”,但不放棄處理器的執(zhí)行時(shí)間献汗,看看持有鎖的線程是否很快就會(huì)釋放鎖敢订。為了讓線程等待王污,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖楚午。

自旋鎖在JDK 1.4.2中就已經(jīng)引入昭齐,只不過(guò)默認(rèn)是關(guān)閉的,可以使用 :XX:+UseSpinning 參數(shù)來(lái)開(kāi)啟矾柜,在JDK 1.6 中就已經(jīng)改為默認(rèn)開(kāi)啟了阱驾。自旋等待不能代替阻塞,且先不說(shuō)對(duì)處理器數(shù)量的要求怪蔑,自旋等待本身雖然避免了線程切換的開(kāi)銷里覆,但它是要占用處理器時(shí)間的,因此饮睬,如果鎖被占用的時(shí)間很短租谈,自旋等待的效果就會(huì)非常好,反之捆愁,如果鎖被占用的時(shí)間很長(zhǎng)割去,那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作昼丑,反而會(huì)帶來(lái)性能上的浪費(fèi)呻逆。因此,自旋等待的時(shí)間必須要有一定的限度菩帝,如果自旋超過(guò)了限定的次數(shù)仍然沒(méi)有成功獲得鎖咖城,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了。自旋次數(shù)的默認(rèn)值是 10 次呼奢,用戶可以使用參數(shù)-XX:PreBlockSpin 來(lái)更改宜雀。

在JDK 1.6中引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的時(shí)間不再固定了握础,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定辐董。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖禀综,并且持有鎖的線程正在運(yùn)行中简烘,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間定枷,比如100個(gè)循環(huán)孤澎。另外,如果對(duì)于某個(gè)鎖欠窒,自旋很少成功獲得過(guò)覆旭,那在以后要獲取這個(gè)鎖時(shí)將可能省略掉自旋過(guò)程,以避免浪費(fèi)處理部資源。有了自適應(yīng)自旋姐扮,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善絮供,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來(lái)越準(zhǔn)確。虛擬機(jī)就會(huì)變得越來(lái)越 “聰明” 了茶敏。

鎖消除

鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步缚俏,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除惊搏。鎖消除的主要判定依據(jù)來(lái)源于逃逸分析的數(shù)據(jù)支持,如果判斷在一段代碼中忧换,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其他線程訪問(wèn)到恬惯,那就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的亚茬,同步加鎖自然就無(wú)須進(jìn)行酪耳。

也許讀者會(huì)有疑問(wèn),變量是否逃逸刹缝,對(duì)于虛擬機(jī)來(lái)說(shuō)需要使用數(shù)據(jù)流分析來(lái)確定碗暗,但是程序員自己所該是很清楚的,怎么會(huì)在明知道不存在數(shù)據(jù)爭(zhēng)用的情況下要求同步呢梢夯?答案是有許多同步措施并不是程序員自己加入的言疗,同步的代碼在Java程序中的普遍程度也許超過(guò)了 大部分讀者的想象。我們來(lái)看看下面代碼中的例子颂砸,這段非常簡(jiǎn)單的代碼僅僅是輸出 3 個(gè)字符串相加的結(jié)果噪奄,無(wú)論是源碼字面上還是程序語(yǔ)義上都沒(méi)有同步。

我們也知道人乓,由于 String 是一個(gè)不可變的類勤篮,對(duì)字符串的連接操作總是通過(guò)生成新的String 對(duì)象來(lái)進(jìn)行的,因此 Javac 編譯器會(huì)對(duì) String 連接做自動(dòng)優(yōu)化色罚。在 JDK 1.5 之前碰缔,會(huì)轉(zhuǎn)化為 StringBuffer 對(duì)象的連續(xù) append()操作,在JDK 1.5及以后的版本中保屯,會(huì)轉(zhuǎn)化為 StringBuilder 對(duì)象的連續(xù) append()操作手负,上述代碼可能會(huì)變成下面的樣子。

現(xiàn)在大家還認(rèn)為這段代碼沒(méi)有涉及同步嗎姑尺?每個(gè)StringBuffer.append() 方法中都有一個(gè)同步塊竟终,鎖就是 sb 對(duì)象。虛擬機(jī)觀察變量sb切蟋,很快就會(huì)發(fā)現(xiàn)它的動(dòng)態(tài)作用域被限制在 concatString() 方法內(nèi)部统捶。也就是說(shuō),sb 的所有引用永遠(yuǎn)不會(huì) “逃逸” 到 concatString()方法之外,其他線程無(wú)法訪問(wèn)到它喘鸟,因此匆绣,雖然這里有鎖,但是可以被安全地消除掉什黑,在即時(shí)編譯之后崎淳,這段代碼就會(huì)忽略掉所有的同步而直接執(zhí)行了。

鎖粗化

原則上愕把,我們?cè)诰帉懘a的時(shí)候拣凹,總是推薦將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步恨豁,這樣是為了使得需要同步的操作數(shù)量盡可能變小嚣镜,如果存在鎖競(jìng)爭(zhēng),那等待鎖的線程也能盡快拿到鎖橘蜜。

大部分情況下菊匿,上面的原則都是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖计福,甚至加鎖操作是出現(xiàn)在循環(huán)體中的跌捆,那即使沒(méi)有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗棒搜。

上述代碼中連續(xù)的append()方法就屬于這類情況疹蛉。如果虛擬機(jī)探測(cè)到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展 (粗化)到整個(gè)操作序列的外部力麸,以上述代碼為例可款,就是擴(kuò)展到第一個(gè) append()操作之前直至最后一個(gè) append()操作之后,這樣只需要加鎖一次就可以了克蚂。

輕量級(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í)鎖是JDK1.6之中加入的新型鎖機(jī)制涩僻,它名字中的 “輕量級(jí)” 是相對(duì)于使用操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn)的傳統(tǒng)鎖而言的缭召,因此傳統(tǒng)的鎖機(jī)制就稱為 “重量級(jí)” 鎖栈顷。首先需要強(qiáng)調(diào)一點(diǎn)的是,輕量級(jí)鎖并不是用來(lái)代替重量級(jí)鎖的嵌巷,它的本意是在沒(méi)有多線程競(jìng)爭(zhēng)的前提下萄凤,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。

在代碼進(jìn)入同步塊的時(shí)候搪哪,如果此同步對(duì)象沒(méi)有被鎖定(鎖標(biāo)志位為“01”狀態(tài))靡努,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝 (官方把這份拷貝加了一個(gè) Displaced 前轍噩死,即Displaced Mark Word )颤难,這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖所示。

然后已维,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針。如果這個(gè)更新動(dòng)作成功了已日,那么這個(gè)線程就擁有了該對(duì)象的鎖垛耳,并且對(duì)象 Mark Word 的鎖標(biāo)志位(Mark Word 的最后 2bit )將轉(zhuǎn)變?yōu)?“00”,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)飘千,這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖所示堂鲜。

如果這個(gè)更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀护奈,如果是說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖缔莲,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了霉旗。如果有兩條以上的線程爭(zhēng)用同一個(gè)鎖痴奏,那輕量級(jí)鎖就不再有效,要膨脹為重量級(jí)鎖厌秒,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”读拆,Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后而等待鎖的線程也要進(jìn)入阻塞狀態(tài)鸵闪。

上而描述的是輕量級(jí)鎖的加鎖過(guò)程檐晕,它的解鎖過(guò)程也是通過(guò)CAS操作來(lái)進(jìn)行的,如果對(duì)象的 Mark Word 仍然指向著線程的鎖記錄蚌讼,那就用 CAS 操作把對(duì)象當(dāng)前的 Mark Word 和 線程中復(fù)制的 Displaced Mark Word 替換回來(lái)辟灰,如果替換成功,越個(gè)同步過(guò)程就完成了篡石。如果替換失敗芥喇,說(shuō)明有其他線程嘗試過(guò)獲取該鎖,那就要在釋放鎖的同時(shí)夏志,喚醒被掛起的線程乃坤。

輕量級(jí)鎖能提升程序同步性能的依據(jù)是“對(duì)于絕大部分的鎖苛让,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的”,這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)湿诊。如果沒(méi)有競(jìng)爭(zhēng)狱杰,輕量級(jí)鎖使用 CAS 操作避免了使用互斥量的開(kāi)銷,但如果存在鎖競(jìng)爭(zhēng)厅须,除了互斥量的開(kāi)銷外仿畸,還額外發(fā)生了 CAS 操作,因此在有競(jìng)爭(zhēng)的情況下朗和,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢错沽。

偏向鎖

偏向鎖是Java 6之后加入的新鎖,它是一種針對(duì)加鎖操作的優(yōu)化手段眶拉,經(jīng)過(guò)研究發(fā)現(xiàn)千埃,在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng)忆植,而且總是由同一線程多次獲得放可,因此為了減少同一線程獲取鎖(會(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í)鎖臀脏。

偏向鎖的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ)劝堪,進(jìn)一步提高程序的運(yùn)行性能冀自。如果說(shuō)輕量級(jí)鎖是在無(wú)競(jìng)爭(zhēng)的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無(wú)競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉秒啦,連 CAS 操作都不做了熬粗。

偏向鎖的“偏”,就是偏心的 “偏”余境、偏袒的 “偏”驻呐,它的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來(lái)的執(zhí)行過(guò)程中芳来,該鎖沒(méi)有被其他的線程獲取含末,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。

如果讀者讀懂了前面輕量級(jí)鎖中關(guān)于對(duì)象頭Mark Word與線程之間的操作過(guò)程即舌,那偏向鎖的原理理解起來(lái)就會(huì)很簡(jiǎn)單佣盒。假設(shè)當(dāng)前虛擬機(jī)啟用了偏向(啟用參數(shù)-XX:+UseBiasedLocking,這是 JDK 1.6 的默認(rèn)值)顽聂,那么沼撕,當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)為 “01 ”芜飘,即偏向模式。同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程的 ID記錄在對(duì)象的 Mark Word 之中磨总,如果 CAS 操作成功嗦明,持有偏向鎖的錢程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如 Locking 蚪燕、Unlocking 及對(duì) Mark Word的Update 等)娶牌。

當(dāng)有另外一個(gè)線程去嘗試獲取這個(gè)鎖時(shí),偏向模式就宣告結(jié)束馆纳。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)诗良,撤銷偏向(Revoke Bias)后恢復(fù)到未鎖定(標(biāo)志位為“01”) 或輕量級(jí)鎖定(標(biāo)志位為 “00”)的狀態(tài),后續(xù)的同步操作就如上面介紹的輕量級(jí)鎖那樣執(zhí)行鲁驶。偏向鎖鉴裹、 輕量級(jí)鎖的狀態(tài)轉(zhuǎn)化及對(duì)象 Mark Word 的關(guān)系如圖所示。

同步但無(wú)競(jìng)爭(zhēng)的程序性能钥弯。它同樣是一個(gè)帶有效益權(quán)衡(Trade Off)性質(zhì)的優(yōu)化径荔,也就是說(shuō),它并不一定總是對(duì)程序運(yùn)行有利脆霎,如果程序中大多數(shù)的鎖總是被多個(gè)不同的線程訪問(wèn)总处,那偏向模式就是多余的。在具體問(wèn)題具體分析的前提下睛蛛,有時(shí)候使用參數(shù)XX:-UseBiasedLocking來(lái)禁止偏向鎖優(yōu)化反而可以提升性能鹦马。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末胧谈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子荸频,更是在濱河造成了極大的恐慌菱肖,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件试溯,死亡現(xiàn)場(chǎng)離奇詭異蔑滓,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)遇绞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門键袱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人摹闽,你說(shuō)我怎么就攤上這事蹄咖。” “怎么了付鹿?”我有些...
    開(kāi)封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵澜汤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我舵匾,道長(zhǎng)俊抵,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任坐梯,我火速辦了婚禮徽诲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吵血。我一直安慰自己谎替,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布蹋辅。 她就那樣靜靜地躺著钱贯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪侦另。 梳的紋絲不亂的頭發(fā)上秩命,一...
    開(kāi)封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音淋肾,去河邊找鬼硫麻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛樊卓,可吹牛的內(nèi)容都是我干的拿愧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼碌尔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼浇辜!你這毒婦竟也來(lái)了券敌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤柳洋,失蹤者是張志新(化名)和其女友劉穎待诅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體熊镣,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卑雁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绪囱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片测蹲。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鬼吵,靈堂內(nèi)的尸體忽然破棺而出扣甲,到底是詐尸還是另有隱情,我是刑警寧澤齿椅,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布琉挖,位于F島的核電站,受9級(jí)特大地震影響涣脚,放射性物質(zhì)發(fā)生泄漏示辈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一遣蚀、第九天 我趴在偏房一處隱蔽的房頂上張望顽耳。 院中可真熱鬧,春花似錦妙同、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至限次,卻和暖如春芒涡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卖漫。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工费尽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羊始。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓旱幼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親突委。 傳聞我的和親對(duì)象是個(gè)殘疾皇子柏卤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

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