天天看點

Java的synchronized關鍵字和鎖更新過程詳解(上)使用者态與核心态.CAScas彙編實作對象的記憶體布局鎖更新鎖優化

使用者态與核心态.

JDK早期,synchronized 叫做重量級鎖,因為申請鎖資源必須通過kernel,系統調用

; he11o. asm
;write(int fd,const void *buffer, size_ t nbytes)

section data
    msg db "Hello", 0xA
    1en equ $ -msg
section .text
g1obal _start
_start:
    mov edx,1en
    mov ecx, msg
    mov ebx, 1 ; 檔案描述符1 std_ _out
    mov eax, 4 ;write函數 系統調用号4
    int 0x80
    mov ebx, 0
    mov eax,1 ;exit函數系統調用号
    int 0x80      

CAS

compare and swap

compare and exchange

Java的synchronized關鍵字和鎖更新過程詳解(上)使用者态與核心态.CAScas彙編實作對象的記憶體布局鎖更新鎖優化

ABA 問題簡單了解就是你不知道你的女朋友到底經曆過幾次戀愛,可以通過加版本辨別解決.

cas彙編實作

lock cmpchgl就能保證原子性

Java的synchronized關鍵字和鎖更新過程詳解(上)使用者态與核心态.CAScas彙編實作對象的記憶體布局鎖更新鎖優化

其實 cas,volatile,synchronize 底層實作都是 lock 指令.

對象的記憶體布局

Java的synchronized關鍵字和鎖更新過程詳解(上)使用者态與核心态.CAScas彙編實作對象的記憶體布局鎖更新鎖優化

鎖更新

Java的synchronized關鍵字和鎖更新過程詳解(上)使用者态與核心态.CAScas彙編實作對象的記憶體布局鎖更新鎖優化
  • 匿名偏向是偏向鎖啟動了,但還沒有指定線程.
偏向鎖在JDK6是預設啟用的,但在應用程式啟動大概 4秒後才激活
  • 使用 -XX:BiasedLockingStartupDelay=0 參數關閉延遲,立即啟動偏向鎖
  • 如果确定應用程式中所有鎖通常情況下處于競争狀态,可以通過 XX:-UseBiasedLocking=false 參數關閉偏向鎖

-X print help on non- standard options

java -XX:+PrintFlagsFinal -version      
  • 可以看到有七百多個參數
  • Java的synchronized關鍵字和鎖更新過程詳解(上)使用者态與核心态.CAScas彙編實作對象的記憶體布局鎖更新鎖優化
  • java -XX:+PrintFlagsFinal —version (檢視jvm 的可以配置的參數)
  • java -XX:+PrintFlagsFinal —version grep BiasedLocking(檢視jvm 的可以配置的參數)
  • Java的synchronized關鍵字和鎖更新過程詳解(上)使用者态與核心态.CAScas彙編實作對象的記憶體布局鎖更新鎖優化
  • UseBiasedLocking 是否啟動偏向級鎖

我們知道synchronized是重量級鎖,效率不怎麼樣,不過在JDK6中對synchronize的實作進行了各種優化,使得它顯得不是那麼重了,那麼JVM采用了那些優化手段呢

鎖優化

如自旋鎖、适應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

鎖主要存在四種狀态:無鎖狀态、偏向鎖狀态、輕量級鎖狀态、重量級鎖狀态。鎖狀态會随着競争的激烈而逐漸更新。

鎖可以更新,但不可降級!這是為了提高獲得鎖和釋放鎖的效率。

自旋鎖(輕量級鎖)

突然有了線程來競争了,就不是偏向鎖了,開始更新為自旋鎖。

競争過程就是看誰能把自己的 id 資訊放進 markword 裡的 id 即可,通過 CAS 方式。

和偏向鎖一樣,無需經過os資源,隻需JVM即可。

線程的阻塞和喚醒需要CPU從使用者态轉為核心态,頻繁阻塞和喚醒對CPU來說是一件負擔很重的工作,勢必會給系統的并發性能帶來很大的壓力。

同時我們發現在許多應用上面,對象鎖的鎖狀态隻會持續很短一段時間,為了這段很短的時間而去頻繁阻塞和喚醒線程非常不值得,是以引入自旋鎖。

自旋鎖,就是讓該線程等待一段時間,而不會被立即挂起,看持有鎖的線程是否會很快釋放鎖。

怎麼等待呢?執行一段無意義的循環即可(自旋)。

自旋等待不能替代阻塞,先不說對處理器數量的要求,雖然它可以避免線程切換帶來的開銷,但是它占用CPU時間。

若持有鎖的線程很快就釋放了鎖,那麼自旋效果就很好;反之白白浪費CPU資源,它不會做任何有意義的工作,典型的占着茅坑不拉屎。

是以,自旋等待的時間(自旋的次數)必須有個限度,若自旋超過了定義時間,仍未擷取到鎖,則應該被挂起。

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()) {
    // Anticipate successful CAS -- the ST of the displaced mark must 預料 CAS 成功
    // be visible <= the ST performed by the CAS.
    lock->set_displaced_header(mark);
    if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
      return;
    }
    // Fall through to inflate() ... 如果失敗 =》鎖膨脹
  } 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");
    lock->set_displaced_header(NULL);
    return;
  }

  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD,
                              obj(),
                              inflate_cause_monitor_enter)->enter(THREAD);
}      

若 CAS 成功就直接 return 了,若失敗會執行下面的鎖膨脹方法,也沒看到自旋操作。

是以輕量級鎖 CAS 失敗,不會自旋,而是直接膨脹成重量級鎖。

自旋鎖在JDK 1.4.2中引入,預設關閉,但是可以使用-XX:+UseSpinning開啟。

在JDK1.6中預設開啟。同時自旋的預設次數為10次,可以通過參數-XX:PreBlockSpin來調整。

等待的線程超過 CPU 的一半數量,則更新為重量級鎖。

若通過參數-XX:preBlockSpin來調整自旋鎖的自旋次數,會帶來諸多不便。假如我将參數調整為10,但是系統很多線程都是等你剛退出時,就釋放了鎖(假如你多自旋一兩次就可以擷取鎖),是不是很尴尬?

于是JDK1.6引入自适應的自旋鎖,讓虛拟機會變得越來越聰明。