Java 中關於鎖的一些理解

jdk 6 對鎖進行了優化,讓他看起來再也不那麼笨重,synchronized有三種形式:偏向鎖,輕量級鎖,重量級鎖.數組

介紹三種鎖以前,引入幾個接下來會出現的概念安全

mark work:
對象頭,對象頭中存儲了一些對象的信息,這個是鎖的根本,任何鎖都須要依賴mark word 來維持鎖的運做,對象頭中存儲了當前持有鎖的線程,hashCode,GC的一些信息都存儲在對象頭中.
在JVM中,對象在內存中除了自己的數據外還會有個對象頭,對於普通對象而言,其對象頭中有兩類信息:mark word和類型指針。另外對於數組而言還會有一份記錄數組長度的數據.
類型指針是指向該對象所屬類對象的指針,mark word用於存儲對象的HashCode、GC分代年齡、鎖狀態等信息。在32位系統上mark word長度爲32bit,64位系統上長度爲64bit。爲了能在有限的空間裏存儲下更多的數據,其存儲格式是不固定的,在32位系統上各狀態的格式以下:數據結構

clipboard.png

能夠看到鎖信息也是存在於對象的mark word中的。當對象狀態爲偏向鎖時,mark word存儲的是偏向的線程ID;當狀態爲輕量級鎖時,mark word存儲的是指向線程棧中Lock Record的指針;當狀態爲重量級鎖時,爲指向堆中的monitor對象的指針.工具

Lock Record:
前面對象頭中提到了Lock Record,接下來講下Lock Record,Lock Record存在於線程棧中,翻譯過來就是鎖記錄,它會拷貝一份對象頭中的mark word信息到本身的線程棧中去,這個拷貝的mark word 稱爲Displaced Mark Word ,另外還有一個指針指向對象性能

monitor:
monitor存在於堆中,什麼是Monitor?咱們能夠把它理解爲一個同步工具,也能夠描述爲一種同步機制,它一般被描述爲一個對象。優化

與一切皆對象同樣,全部的Java對象是天生的Monitor,每個Java對象都有成爲Monitor的潛質,由於在Java的設計中 ,每個Java對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者Monitor鎖。spa

Monitor 是線程私有的數據結構,每個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。其結構以下:線程

  • Owner:初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲NULL
  • EntryQ:關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程
  • RcThis:表示blocked或waiting在該monitor record上的全部線程的個數
  • Nest:用來實現重入鎖的計數
  • HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)
  • Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖
    (摘自:Java中synchronized的實現原理與應用)

說完幾個關鍵概念以後來講一下鎖的問題:翻譯

  1. 偏向鎖
    偏向鎖是鎖的級別中最低的鎖,舉個例子: 在此demo中,得到操做list的一直都是main線程,沒有第二個線程參與操做,此時的鎖就是偏向鎖,偏向鎖很輕,jdk 1.6默認開啓,當第一個線程進入的時候,對象頭中的threadid爲0,表示未偏向任何線程,也叫作匿名偏向量設計

    public class SyncDemo1 {
    
       public static void main(String[] args) {
           SyncDemo1 syncDemo1 = new SyncDemo1();
           for (int i = 0; i < 100; i++) {
               syncDemo1.addString("test:" + i);
           }
       }
    
       private List<String> list = new ArrayList<>();
    
       public synchronized void addString(String s) {
           list.add(s);
       }
    
    }

當第一個線程進入的時候發現是匿名偏向狀態,則會用cas指令把mark words中的threadid替換爲當前線程的id若是替換成功,則證實成功拿到鎖,失敗則鎖膨脹;
當線程第二次進入同步塊時,若是發現線程id和對象頭中的偏向線程id一致,則通過一些比較以後,在當前線程棧的lock record中添加一個空的Displaced Mark Word,因爲操做的是私有線程棧,因此不須要cas操做,synchronized帶來的開銷基本能夠忽略;
當其餘線程進入同步塊中時,發現偏向線程不是當前線程,則進入到撤銷偏向鎖的邏輯,當達到全局安全點時,鎖開始膨脹爲輕量級鎖,原來的線程仍然持有鎖,若是發現偏向線程掛了,那麼就把對象的頭改成無鎖狀態,鎖膨脹

  1. 輕量鎖

當鎖膨脹爲輕量級鎖時,首先判斷是否有線程持有鎖(判斷mark work),若是是,則在當前線程棧中建立一個lock record 複製mark word 而且cas的把當前線程棧的lock record 的地址放到對象頭中,若是成功,則說明獲取到輕量級鎖,若是失敗,則說明鎖已經被佔用了,此時記錄線程的重入次數(把lock record 的mark word 設置爲null),鎖會自旋能夠進行自適應性自旋,確保在競爭不激烈的狀況下仍然能夠不膨脹爲重量級鎖從而減小消耗,若是cas失敗,則說明線程出現競爭,須要膨脹爲重量級的鎖,代碼以下:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  // 若是是無鎖狀態
  if (mark->is_neutral()) {
    //設置Displaced Mark Word並替換對象頭的mark word
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    // 若是是重入,則設置Displaced Mark Word爲null
    lock->set_displaced_header(NULL);
    return;
  }

  ...
  // 走到這一步說明已是存在多個線程競爭鎖了 須要膨脹爲重量級鎖
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
  1. 重量鎖

重量級鎖就是咱們傳統意義上的鎖了,當線程發生競爭,鎖膨脹爲重量級鎖,對象的mark word 指向堆中的 monitor,此時會將線程封裝爲一個objectwaiter對象插入到monitor中的contextList中去,而後暫停當前線程,當持有鎖的線程釋放線程以前,會把contextList裏面的全部線程對象插入到EntryList中去,會從EntryList中挑選一個線程喚醒,被選中的線程叫作Heir presumptive即假定繼承人(應該是這樣翻譯),就是圖中的Ready Thread,假定繼承人被喚醒後會嘗試得到鎖,但synchronized是非公平的,因此假定繼承人不必定能得到鎖(這也是它叫"假定"繼承人的緣由)。

若是線程得到鎖後調用Object#wait方法,則會將線程加入到WaitSet中,當被Object#notify喚醒後,會將線程從WaitSet移動到cxq或EntryList中去。須要注意的是,當調用一個鎖對象的wait或notify方法時,如當前鎖的狀態是偏向鎖或輕量級鎖則會先膨脹成重量級鎖。

clipboard.png

相關文章
相關標籤/搜索