關於Synchronized的偏向鎖,輕量級鎖,重量級鎖,鎖升級過程,自旋優化,你該瞭解這些

目錄

前言

synchronized的常見使用方式

修飾代碼塊(同步代碼塊)

修飾方法

synchronized不能繼承?(插曲)

修飾靜態方法

修飾類

Java對象 Mark Word

偏向鎖

什麼是偏向鎖

偏向鎖演示

偏向鎖原理圖解

優勢

白話翻譯

輕量鎖

什麼是輕量級鎖

輕量級圖解

優勢

白話翻譯

重量級鎖

什麼是重量級鎖

重量級鎖原理圖解

Monitor源碼分析

環境搭建

構造函數

鎖競爭的過程

白話翻譯

自旋優化

結語

參考資料


前言

毫無疑問,synchronized是我們用過的第一個併發關鍵字,很多博文都在講解這個技術。不過大多數講解還停留在對synchronized的使用層面,其底層的很多原理和優化,很多人可能並不知曉。因此本文將通過對synchronized的大量C源碼分析,讓大家對他的瞭解更加透徹點。

本篇將從爲什麼要引入synchronized,常見的使用方式,存在的問題以及優化部分這四個方面描述,話不多說,開始表演。

synchronized的常見使用方式

修飾代碼塊(同步代碼塊)

synchronized (object) {
      //具體代碼
}

修飾方法

synchronized void test(){
  //具體代碼
}

 

synchronized不能繼承?(插曲)

父類A:

public class A {
    synchronized void test() throws Exception {
        try {
            System.out.println("main 下一步 sleep begin threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("main 下一步 sleep end threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

子類B:(未重寫test方法)

public class B extends A {

}

子類C:(重寫test方法)

public class C extends A {

    @Override
     void test() throws Exception{
        try {
            System.out.println("sub 下一步 sleep begin threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("sub 下一步 sleep end threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

 

線程A:

public class ThreadA extends Thread {
    private A a;

    public void setter  (A a) {
        this.a = a;
    }

    @Override
    public void run() {
        try{
            a.test();
        }catch (Exception e){

        }
    }
}

線程B:

public class ThreadB extends Thread {
    private B b;
    public void setB(B b){
        this.b=b;
    }

    @Override
    public void run() {
        try{
            b.test();
        }catch (Exception e){

        }
    }
} 

線程C:

public class ThreadC extends Thread{
    private C c;
    public void setC(C c){
        this.c=c;
    }

    @Override
    public void run() {
        try{
            c.test();
        }catch (Exception e){

        }
    }
}

測試類test:

public class test {
    public static void main(String[] args) throws Exception {
        A a = new A();
        ThreadA A1 = new ThreadA();
        A1.setter(a);
        A1.setName("A1");
        A1.start();
        ThreadA A2 = new ThreadA();
        A2.setter(a);
        A2.setName("A2");
        A2.start();
        A1.join();
        A2.join();

        System.out.println("=============");
        B b = new B();
        ThreadB B1 = new ThreadB();
        B1.setB(b);
        B1.setName("B1");
        B1.start();
        ThreadB B2 = new ThreadB();
        B2.setB(b);
        B2.setName("B2");
        B2.start();
        B1.join();
        B2.join();
        System.out.println("=============");

        C c = new C();
        ThreadC C1 = new ThreadC();
        C1.setName("C1");
        C1.setC(c);
        C1.start();
        ThreadC C2 = new ThreadC();
        C2.setName("C2");
        C2.setC(c);
        C2.start();
        C1.join();
        C2.join();
    }
}

運行結果:

子類B繼承了父類A,但是沒有重寫test方法,ThreadB仍然是同步的。子類C繼承了父類A,也重寫了test方法,但是未明確寫上synchronized,所以這個方法並不是同步方法。只有顯式的寫上synchronized關鍵字,纔是同步方法。

所以synchronized不能繼承這句話有歧義,我們只要記住子類如果想要重寫父類的同步方法,synchronized關鍵字一定要顯示寫出,否則無效。

修飾靜態方法

synchronized static void test(){
   //具體代碼
}

修飾類

synchronized (Example2.class) {
    //具體代碼
 }

 

Java對象 Mark Word

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭,實例數據和對齊數據,如下圖:

其中Mark Word值在不同鎖狀態下的展示如下:(重點看線程id,是否爲偏向鎖,鎖標誌位信息)

在64位系統中,Mark Word佔了8個字節,類型指針佔了8個字節,一共是16個字節。Talk is cheap. Show me the code. 咱來看代碼。

  •  我們想要看Java對象的Mark Word,先要加載一個jar包,在pom.xml添加即可。
<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.9</version>
</dependency>
  • 新建一個對象A,擁有初始值爲666的變量x。
public class A {
    private int x=666;
}
  • 新建一個測試類test,這涉及到剛纔加載的jar,我們打印Java對象。

import org.openjdk.jol.info.ClassLayout;

public class test {
    public static void main(String[] args) {
        A a=new A();
        System.out.println( ClassLayout.parseInstance(a).toPrintable());
    }
}
  • 我們發現對象頭(object header)佔了12個字節,爲啥和上面說的16個字節不一樣。

  • 其實上是默認開啓了指針壓縮,我們需要關閉指針壓縮,也就是添加-XX:-UseCompressedOops配置。

  • 再次執行,發現對象頭爲16個字節。

偏向鎖

什麼是偏向鎖

JDK1.6之前鎖爲重量級鎖(待會說,只要知道他和內核交互,消耗資源),1.6之後Java設計人員發現很多情況下並不存在多個線程競爭的關係,所以爲了資源問題引入了無鎖偏向鎖輕量級鎖重量級鎖的概念。先說偏向鎖,他是偏心,偏袒的意思,這個鎖會偏向於第一個獲取他的線程。

偏向鎖演示

  • 創建並啓動一個線程,run方法裏面用了synchronized關鍵字,功能是打印this的Java對象。
public class test {
    public static void main(String[] args) {
         Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this){
                    System.out.println(ClassLayout.parseInstance(this).toPrintable());
                }
            }
        });
        thread.start();
    }
}

標紅的地方爲000,根據之前Mark Word在不同狀態下的標誌,得此爲無鎖狀態。理論上一個線程使用synchronized關鍵字,應爲偏向鎖。

  • 實際上偏向鎖在JDK1.6之後是默認開啓的,但是啓動時間有延遲,所以需要添加參數-XX:BiasedLockingStartupDelay=0,讓其在程序啓動時立刻啓動。

  • 重新運行下代碼,發現標紅地方101,對比Mark Word在不同狀態下的標誌,得此狀態爲偏向鎖。

偏向鎖原理圖解

  • 在線程的run方法中,剛執行到synchronized,會判斷當前對象是否爲偏向鎖和鎖標誌,沒有任何線程執行該對象,我們可以看到是否爲偏向鎖爲0,鎖標誌位01,即無鎖狀態。

  • 線程會將自己的id賦值給markword,即將原來的hashcode值改爲線程id,是否是偏向鎖改爲1,表示線程擁有對象鎖,可以執行下面的業務邏輯。如果synchronized執行完,對象還是偏向鎖狀態;如果線程結束之後,會撤銷偏向鎖,將該對象還原成無鎖狀態。

  • 如果同一個線程中又對該對象進行加鎖操作,我們只要對比對象的線程id是否與線程id相同,如果相同即爲線程鎖重入問題。

優勢

加鎖和解鎖不需要額外的消耗,和執行非同步方法相比只有納秒級的差距。

白話翻譯

線程1鎖定對象this,他發現對象爲無鎖狀態,所以將線程id賦值給對象的Mark Word字段,表示對象爲線程1專用,即使他退出了同步代碼,其他線程也不能使用該對象。

同學A去自習教室C,他發現教室無人,所以在門口寫了個名字,表示當前教室有人在使用,這樣即使他出去吃了飯,其他同學也不能使用這個房間。

輕量鎖

什麼是輕量級鎖

在多線程交替同步代碼塊的情況下,線程間沒有競爭,使用輕量級鎖可以避免重量級鎖引入的性能消耗。

輕量級圖解

  • 在剛纔偏向鎖的基礎上,如果有另外一個線程也想錯峯使用該資源,通過對比線程id是否相同,Java內存會立刻撤銷偏向鎖(需要等待全局安全點),進行鎖升級的操作。

  • 撤銷完輕量級鎖,會在線程1的方法棧中新增一個鎖記錄,對象的Mark Word與鎖記錄交換。

優勢

競爭的線程不會阻塞,提高了程序的響應速度。

白話翻譯

在剛纔偏向鎖的基礎上,另外一個線程也想要獲取資源,所以線程1需要撤銷偏向鎖,升級爲輕量鎖。

同學A在使用自習教室外面寫了自己的名字,所以同學B來也想要使用自習教室,他需要提醒同學A,不能使用重量級鎖,同學A將自習教室門口的名字擦掉,換成了一個書包,裏面是自己的書籍。這樣在同學A不使用自習教室的時候,同學B也能使用自習教室,只需要將自己的書包也掛在外面即可。這樣下次來使用的同學就能知道已經有人佔用了該教室。

重量級鎖

什麼是重量級鎖

當多線程之間發生競爭,Java內存會申請一個Monitor對象來實現。

重量級鎖原理圖解

在剛纔的輕量級鎖的基礎上,線程2也想要申請資源,發現鎖的標誌位爲00,即爲輕量級鎖,所以向內存申請一個Monitor,讓對象的MarkWord指向Monitor地址,並將ower指針指向線程1的地址,線程2放在等待隊列裏面,等線程1指向完畢,釋放鎖資源。

Monitor源碼分析

環境搭建

我們去官網http://openjdk.java.net/找下open源碼,也可以通過其他途徑下載。源碼是C實現的,可以通過DEV C++工具打開,效果如下圖:

構造函數

我們先看下\hotspot\src\share\vm\runtime\ObjectMonitor.hpp,以.hpp結尾的文件是導入的一些包和一些聲明,之後可以被.cpp文件導入。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;//線程重入次數 
    _object       = NULL;//存儲該monitor的對象 
    _owner        = NULL;//標識擁有該monitor的線程 
    _WaitSet      = NULL;//處於wait狀態的線程,會加入到_waitSet 
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//多線程競爭鎖時的單項列表 
    FreeNext      = NULL ;
    _EntryList    = NULL ;//處於等待鎖lock狀態的線程,會被加入到該列表 
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

 

鎖競爭的過程

我們先看下\hotspot\src\share\vm\interpreter\interpreterRuntime.cppIRT_ENTRY_NO_ASYNC即爲鎖競爭過程。


//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
//是否使用偏向鎖,可加參數進行設置  if (UseBiasedLocking) { //如果可以使用偏向鎖,即進入fast_enter
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {//如果不可以使用偏向鎖,即進行slow_enter
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

 

 

slow_enter實際上調用的ObjectMonitor.cpp的enter 方法

void ATTR ObjectMonitor::enter(TRAPS) {
  // The following code is ordered to check the most common cases first
  // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  Thread * const Self = THREAD ;
  void * cur ;

  //通過CAS操作嘗試將monitor的_owner設置爲當前線程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  //如果設置不成功,直接返回
  if (cur == NULL) {
     // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     // CONSIDER: set or assert OwnerIsThread == 1
     return ;
  }
  //如果_owner等於當前線程,重入數_recursions加1,直接返回
  if (cur == Self) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }

  //如果當前線程第一次進入該monitor,設置重入數_recursions爲1,_owner爲當前線程,返回
  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 ;
  }

 //如果未搶到鎖,則進行自旋優化,如果還未獲取鎖,則放入到list裏面
  // We've encountered genuine contention.
  assert (Self->_Stalled == 0, "invariant") ;
  Self->_Stalled = intptr_t(this) ;

  // Try one round of spinning *before* enqueueing Self
  // and before going through the awkward and expensive state
  // transitions.  The following spin is strictly optional ...
  // Note that if we acquire the monitor from an initial spin
  // we forgo posting JVMTI events and firing DTRACE probes.
  if (Knob_SpinEarly && TrySpin (Self) > 0) {
     assert (_owner == Self      , "invariant") ;
     assert (_recursions == 0    , "invariant") ;
     assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
     Self->_Stalled = 0 ;
     return ;
  }

  assert (_owner != Self          , "invariant") ;
  assert (_succ  != Self          , "invariant") ;
  assert (Self->is_Java_thread()  , "invariant") ;
  JavaThread * jt = (JavaThread *) Self ;
  assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
  assert (jt->thread_state() != _thread_blocked   , "invariant") ;
  assert (this->object() != NULL  , "invariant") ;
  assert (_count >= 0, "invariant") ;

  // Prevent deflation at STW-time.  See deflate_idle_monitors() and is_busy().
  // Ensure the object-monitor relationship remains stable while there's contention.
  Atomic::inc_ptr(&_count);

  EventJavaMonitorEnter event;

  { // Change java thread status to indicate blocked on monitor enter.
    JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);

    DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
    if (JvmtiExport::should_post_monitor_contended_enter()) {
      JvmtiExport::post_monitor_contended_enter(jt, this);
    }

    OSThreadContendState osts(Self->osthread());
    ThreadBlockInVM tbivm(jt);

    Self->set_current_pending_monitor(this);

    // TODO-FIXME: change the following for(;;) loop to straight-line code.
    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 (false, Self) ;

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }

  Atomic::dec_ptr(&_count);
  assert (_count >= 0, "invariant") ;
  Self->_Stalled = 0 ;

  // Must either set _recursions = 0 or ASSERT _recursions == 0.
  assert (_recursions == 0     , "invariant") ;
  assert (_owner == Self       , "invariant") ;
  assert (_succ  != Self       , "invariant") ;
  assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;

  // The thread -- now the owner -- is back in vm mode.
  // Report the glorious news via TI,DTrace and jvmstat.
  // The probe effect is non-trivial.  All the reportage occurs
  // while we hold the monitor, increasing the length of the critical
  // section.  Amdahl's parallel speedup law comes vividly into play.
  //
  // Another option might be to aggregate the events (thread local or
  // per-monitor aggregation) and defer reporting until a more opportune
  // time -- such as next time some thread encounters contention but has
  // yet to acquire the lock.  While spinning that thread could
  // spinning we could increment JVMStat counters, etc.

  DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
  if (JvmtiExport::should_post_monitor_contended_entered()) {
    JvmtiExport::post_monitor_contended_entered(jt, this);
  }

  if (event.should_commit()) {
    event.set_klass(((oop)this->object())->klass());
    event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
    event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
    event.commit();
  }

  if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
     ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
  }
} 

 

白話翻譯

同學A在使用自習教室的時候,同學B在同一時刻也想使用自習教室,那就發生了競爭關係。所以同學B在A運行過程中,加入等待隊列。如果此時同學C也要使用該教室,也會加入等待隊列。等同學A使用結束,同學B和C將競爭自習教室。

自旋優化

自旋優化比較簡單,如果將其他線程加入等待隊列,那之後喚醒並運行線程需要消耗資源,所以設計人員讓其空轉一會,看看線程能不能一會結束了,這樣就不要在加入等待隊列。

白話來說,如果同學A在使用自習教室,同學B可以回宿舍,等A使用結束再來,但是B回宿舍再來的過程需要1個小時,而A只要10分鐘就結束了。所以B可以先不回宿舍,而是在門口等個10分鐘,以防止來回時間的浪費。

結語

唉呀媽呀,終於結束了,累死了。終於將synchronized寫完了,如果有不正確的地方,還需要各位指正。如果覺得寫得還行,麻煩幫我點贊,評論哈。

參考資料

Java中System.out.println()爲何會影響內存可見性

別再問什麼是Java內存模型了,看這裏!

JVM---彙編指令集

Java中Synchronized的使用

synchronized同步方法(08)同步不具有繼承性

Thread--synchronized不能被繼承?!?!!!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章