Java併發編程-synchronized

  這是Java併發編程學習的第一篇,最先在2013年時便勵志要把JAVA的併發編程好好學習一下,那個時候才工做一年。後來因爲各類各樣的緣由,未能學習起來,5年時間過去,技術止步不前,學到的都是業務領域知識,站在我我的發展角度,我但願在技術,主要是JAVA後端技術領域再往前走一步,因此在這裏記錄下我學習的點點滴滴,同時將代碼記錄在Github上。併發編程的文章主要是記錄個人學習過程,應該會有不少錯誤的地方,也會有不少沒有深度的內容,歡迎你們糾正。html

 

一、爲何會用到synchronizedjava

  Java語言的一個高級特性就是支持多線程,線程在操做系統的實現上,能夠當作是輕量級的進程,同一進程中的線程都將共享進程的內存空間,因此Java的多線程在共享JVM的內存空間。JVM的內存空間主要分爲:程序計數器、虛擬機棧、本地方法棧、堆、方法區和運行時常量池。git

  在這些內存空間中,咱們重點關注棧和堆,這裏的棧包括了虛擬機棧和本地方法棧(實際上不少JVM的實現就是二者合二爲一)。在JVM中,每一個線程有本身的棧內存,其餘線程沒法訪問該棧的內存數據,棧中的數據也僅僅限於基本類型和對象引用。在JVM中,全部的線程共享堆內存,而堆上則不保存基本類型和對象引用,只包含對象。除了重點關注的棧和堆,還有一部分數據存放在方法區,好比類的靜態變量,方法區和棧相似,只能存放基本類型和對象引用,不一樣的是方法區是全部線程共享的。github

  如上所述,JVM的堆(對象信息)和方法區(靜態變量)是全部線程共享的,那麼多線程若是訪問同一個對象或者靜態變量時,就須要進行管控,不然可能出現沒法預測的結果。爲了協調多線程對數據的共享訪問,JVM給每一個對象和類都分配了一個鎖,同一時刻,只有一個線程能夠擁有這個對象或者類的鎖。JVM中鎖是經過監視器(Monitors)來實現的,監視器的主要功能就是監視一段代碼,確保在同一時間只有一個線程在執行。每一個監視器都和一個對象關聯,當線程執行到監視器的監視代碼第一條指令時,線程獲取到該對象的鎖定,直到代碼執行完成,執行完成後,線程釋放該對象的鎖。編程

  synchronized就是Java語言中一種內置的Monitor實現,咱們在多線程的實現上就會用到synchronized來對類和對象進行行爲的管控。後端

二、synchronized用法及背後原理數組

  主要提供了2種方式來協調多線程的同步訪問數據:同步方法和同步代碼塊。代碼以下:安全

public class SynchronizedPrincipleTest {
        public synchronized void f1() {
            System.out.println("synchronized void f1()");
        }

        public void f2() {
            synchronized(this) {
                System.out.println("synchronized(this)");
            }
        }

        public static void main(String[] args) {
            SynchronizedPrincipleTest test = new SynchronizedPrincipleTest();
            test.f1();
            test.f2();
        }
}

f1就是同步方法,f2就是同步代碼塊。這兩種實如今背後有什麼差別呢?咱們能夠先javac編譯,而後再經過javap反編譯來看下。網絡

                   圖一數據結構

從圖一能夠看出同步方法JVM是經過ACC_SYNCHRONIZED來實現的,同步代碼塊JVM是經過monitorenter、monitorexit來實現的。在JVM規範中,同步方法經過ACC_SYNCHRONIZED標記對方法隱式加鎖,同步代碼塊則顯示的經過monitorenter和monitorexit進行加鎖,當線程執行到monitorenter時,先得到鎖,而後執行方法,執行到monitorexit再釋放鎖。

3、JVM Monitor背後實現

  查閱網上各類資料及翻閱openJDK代碼。

synchronized uses a locking mechanism that is built into the JVM and MONITORENTER / MONITOREXIT bytecode instructions. So the underlying implementation is JVM-specific (that is why it is called intrinsic lock) and AFAIK usually (subject to change) uses a pretty conservative strategy: once lock is "inflated" after threads collision on lock acquiring, synchronized begin to use OS-based locking ("fat locking") instead of fast CAS ("thin locking") and do not "like" to use CAS again soon (even if contention is gone).
…………
PS: you're pretty curious and I highly recommend you to look at HotSpot sources to go deeper (and to find out exact implementations for specific platform version). It may really help. Starting point is somewhere here: http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp

上述表達的大體意思是同步在字節碼層面就是經過monitorenter和monitorexit來實現的,能夠理解爲這種實現是JVM規範,一旦線程在鎖獲取時出現衝突,鎖就會膨脹,這種膨脹是基於系統的實現(胖鎖)來替代CAS實現(瘦鎖)。最後給出JVM底層C++代碼的連接。

查看該類代碼,有以下注釋:

// This is full version of monitor enter and exit. I choose not
// to use enter() and exit() in order to make sure user be ware
// of the performance and semantics difference. They are normally
// used by ObjectLocker etc. The interpreter and compiler use
// assembly copies of these routines. Please keep them synchornized.

知道這個類是全部monitor enter and exit的實現,其中方法jni_enter和jni_exit就是heavy weight monitor的實現。再看這個jni_enter方法的實現,調用了inflate方法,返回ObjectMonitor指針。

再看類ObjectMonitor,給出了具體enter和exit方法的實現。

// The ObjectMonitor class is used to implement JavaMonitors which have
// transformed from the lightweight structure of the thread stack to a
// heavy weight lock due to contention

與wiki.openjdk描述一致:

Synchronization affects multiple parts of the JVM: The structure of the object header is defined in the classes oopDesc and markOopDesc, the code for thin locks is integrated in the interpreter and compilers, and the class ObjectMonitor represents inflated locks.

4、鎖優化

  從上述中的註釋咱們能夠看出synchronized是一種heavy weight lock,Brian Goetz在IBM developerworks的論文《Java theory and practice:More flexible, scalable locking in JDK 5.0》也比較了synchronized和ReentrantLock二者的性能,因此在JDK1.6以後對鎖作了不少優化,主要有:自旋鎖和自適應自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖

4.1、自旋鎖和自適應自旋

  線程執行到synchronized同步方法或者代碼塊時,若是另外的線程已經獲取到該對象的鎖,該線程就只能等待,被操做系統掛起,直到另外的線程處理完成,該線程才能恢復執行。線程的掛起和恢復都須要從應用的用戶態切換到操做系統的內核態才能完成,這種操做給系統也帶來了性能上很大的影響。同時虛擬機的研發團隊注意到在不少應用上,共享數據上的鎖只會持續很短的時間,爲了這點時間去掛起和恢復線程是不值得的。若是物理機上有多個CPU,那麼就能夠同時讓多個線程並行執行,就能夠在JVM的層面上讓請求鎖的線程「稍等一下」,但不放棄CPU的執行時間,看下持有鎖的線程是否會很快就釋放鎖。爲了讓線程等待,就讓線程去執行一個忙循環(自旋),這種技術就是所謂的自旋鎖。舉個例子,我以爲有點像,工做中我正在回覆郵件,這個動做其實很快就能作完,這個時候另一我的給我打電話,我接通了,可是我告訴他等我一下,我回復完這封郵件,我們再交流。這個過程,回覆郵件佔用了我這個資源,另一我的要和我通話,若是徹底阻塞,我就不接電話直接完成回覆郵件再接通電話,可是其實回覆郵件只要一下子時間,因此我接通了電話,而後對方一直在線上等佔用着個人時間(他本身也一直在等我,暫時不作別的事情,忙循環),等我回復郵件完成,立馬切換過來電話交流。在這個例子裏面,其實咱們也能夠看出若是對方一直等待,若是我郵件遲遲未回覆完成,對方也是一直在耗着等待且不能作其餘的工做,這也是性能的浪費,這個就是自旋鎖的缺點,因此自旋不能沒有限制,要能作到「智能」的判斷,這個就是自適應自旋的概念,自適應自旋時間不固定,是由前一次鎖的自旋時間和鎖的擁有者的狀態決定的,若是以前自旋成功獲取過鎖,則這次自旋等待的時間能夠長一點,不然省略自旋,避免資源浪費。一樣,拿這個例子來講,若是這次對方在電話那頭就等了我一小段時間,我就和對方溝通了,那麼下次碰到一樣的狀況時,對方會繼續在電話耐心的等待,不然對方就直接掛電話了(由於喪失了「信任」)。

4.2、鎖消除

  鎖消除指的是JVM在JIT運行時,對一些代碼要求同步而實際該段代碼在數據共享上不可能出現競爭的鎖而進行消除操做。好比代碼(EascapeTest類)以下:

private static String concatString(String s1,String s2) {
     return s1 + s2;
}

public static void main(String[] args) {
     EascapeTest eascapeTest = new EascapeTest();
     eascapeTest.concatString("a","b");
}

  方法concatString,是咱們在實際開發中常常會用到的一個字符串拼接的實現,從源代碼層面上看是沒有任何同步操做的。但實際JVM在運行這個方法時會優化爲StringBuilder的append()操做,這個咱們能夠經過javap反編譯來驗證,見圖二。

                                圖二

JVM採用StringBuilder來實現,是經過同步方法來實現的,可是concatString方法中StringBuilder的對象的做用域是被限制在這個方法內部,只要作到EascapeTest被安全的發佈,那麼concatString方法中StringBuilder的全部對象都不會發生逸出,也就是線程安全的,對應的鎖能夠被消除,從而提高性能。

4.3、鎖粗化

  鎖粗化是合併使用相同鎖定對象的相鄰同步塊的過程。看以下代碼:

public void addStooges(Vector v) {
     v.add("Moe");
     v.add("Larry");
     v.add("Curly");
}

addStooges方法中的一系列操做都是在不斷的對同一個對象進行反覆的加鎖和解鎖,即便沒有線程競爭,如此頻繁的同步操做也是很損耗性能的,JVM若是探測到這樣的操做,就會對同步範圍進行粗化,把鎖放在第一個操做加上,而後在最後一個操做中釋放,這樣就只加了一次鎖可是達到了一樣的效果。

4.4、輕量級鎖和偏向鎖

  學習輕量級鎖和偏向鎖以前,我們得先來學習下Java對象模型和對象頭,有了這個基礎纔好來理解這兩個鎖。

4.4.1、Java對象模型及對象頭

  Java虛擬機有不少對應的實現版本,本小節的內容基於HotSpot虛擬機來學習下Java對象模型和對象頭。HotSpot的底層是用C++實現的,這個能夠經過下載OpenJDK源代碼來看便可確認。衆所周知,C++和Java都是面向對象的語言,那麼Java的對象在虛擬機的表示,最簡單的一種實現就是在C++層面上實現一個與之對應的類,然而HotSpot並無這麼實現,而是專門設計一套OOP-Klass二分模型。

OOP:ordinary object pointer,普通對象指針,用來描述對象實例信息。

Klass:Java類的C++對等體,用來描述Java類。

之因此會這麼設計,其中一個理由就是設計者不想讓每個對象都有一個C++虛函數指針(取自klass.hpp註釋)。

// One reason for the oop/klass dichotomy in the implementation is
// that we don't want a C++ vtbl pointer in every object.  ……….

對於OOP對象來講,主要職能是表示對象的實例信息,不必持有任何虛函數;而在描述Java類的Klass對象中含有VTBL(繼承自klass_vtbl),那麼Klass就能夠根據Java對象的實際類型進行C++的分發,這樣OOP對象只須要經過相應的Klass即可以找到全部的虛函數,就避免了給每個對象都分配一個C++的虛函數指針。

Klass向JVM提供了2個功能:

實現語言層面的Java類;

實現Java對象的分發功能;

這2個功能在一個C++類中就能實現,前者在基類Klass中已經實現,然後者就由Klass的子類提供虛函數實現(取自klass.hpp註釋)。

// A Klass provides:
//  1: language level class object (method dictionary etc.)
//  2: provide vm dispatch behavior for the object
// Both functions are combined into one C++ class.

OOP框架和Klass框架的關係能夠在oopsHierarchy.hpp文件中體現,JDK1.7和JDK1.8因爲內存空間的變化,因此oopsHierarchy.hpp的實現也不同,這裏以OpenJDK1.7來描述OOP-Klass。

typedef class oopDesc*                            oop;//oops基類
typedef class   instanceOopDesc*            instanceOop; //Java類實例
typedef class   methodOopDesc*                    methodOop; //Java方法
typedef class   constMethodOopDesc*            constMethodOop; //Java方法不變信息
typedef class   methodDataOopDesc*            methodDataOop; //性能信息數據結構
typedef class   arrayOopDesc*                    arrayOop; //數組oops基類
typedef class     objArrayOopDesc*            objArrayOop; //數組oops對象
typedef class     typeArrayOopDesc*            typeArrayOop;
typedef class   constantPoolOopDesc*            constantPoolOop;
typedef class   constantPoolCacheOopDesc*   constantPoolCacheOop;
typedef class   klassOopDesc*                    klassOop; //與Java類對等的C++類
typedef class   markOopDesc*                    markOop; //Java對象頭
typedef class   compiledICHolderOopDesc*    compiledICHolderOop;

在Java程序運行的過程當中,每建立一個Java對象,在JVM內部就會相應的建立一個OOP對象來表示該Java對象。OOP對象的基類就是oopDesc,它的代碼實現以下:

 volatile markOop  _mark;
  union _metadata {
    wideKlassOop    _klass;
    narrowOop       _compressed_klass;
  } _metadata;

在虛擬機內部,經過instanceOopDesc來表示一個Java對象。對象在內部中的佈局能夠分爲兩個連續的部分:instanceOopDesc和實例數據。instanceOopDesc又被稱爲對象頭,繼承自oopDesc,看instanceOop.hpp的實現,未新增新的數據結構,和oopDesc同樣,包含以下2部分信息:

_mark:markOop類型,存儲對象運行時記錄信息,主要有HashCode、分代年齡、鎖狀態標記、線程持有的鎖、偏向線程ID等,佔用內存和虛擬機位長一致,若是是32位虛擬機則爲32位,在64位虛擬機則爲64位;

_metadata:聯合體,指向描述類型的Klass對象的指針,由於Klass對象包含了實例對象所屬類型的元數據,故被稱爲元數據指針。虛擬機運行時將頻繁使用這個指針定位到方法區的類信息。

到此基本描述了Java的對象頭,可是這只是一部分,還有一部分是Klass,合起來纔是完整的對象模型。那麼Klass在對象模型中是如何體現的呢?實際上,HotSpot是這樣處理的,經過爲每個已加載的Java類建立一個instanceKlass對象,用來在JVM層表示Java類。來看看instanceKlass的數據結構。

// Method array.方法列表
objArrayOop     _methods;
// Int array containing the original order of method in the class file (for
// JVMTI).方法順序
typeArrayOop    _method_ordering;
// Interface (klassOops) this class declares locally to implement.實現接口
objArrayOop     _local_interfaces;
// Interface (klassOops) this class implements transitively.繼承接口
objArrayOop     _transitive_interfaces;
…………
typeArrayOop    _fields;
// Constant pool for this class.
constantPoolOop _constants;
// Class loader used to load this class, NULL if VM loader used.
oop             _class_loader;
// Protection domain.
oop             _protection_domain;

能夠看到一個類該有的內容,instanceKlass基本都有了。

綜上,Java對象在JVM中的表示是這樣的,對象的實例(instanceOopDesc)存儲在堆上,對象的元數據(instanceKlass)存儲在方法區,對象的引用存儲在棧上。以下圖:

            圖三 取自參考資料

4.4.2、輕量級鎖

  輕量級鎖並非用來替代重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小重量級鎖使用操做系統互斥量產生的性能消耗。上面咱們已經介紹了Java對象頭(instanceOopDesc),數據結構以下:

enum { age_bits                 = 4,
         lock_bits                = 2,
         biased_lock_bits         = 1,
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2
  };

此結構和網絡數據包的報文頭結構很是像。

                    圖四 取自參考資料

簡單介紹完對象頭的構造後,回到輕量級鎖的的執行過程上。在代碼進入同步塊的時候,若是此時同步對象未被鎖定(Unlocked,01),JVM會在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(Displace Mark Word)。而後,JVM將使用CAS操做嘗試將該對象的Mark Word指向Lock Record的指針,若是這個操做成功,則線程就擁有了該對象的鎖,而且對象的Mark Word狀態變成(Light-weight locked,00),表示輕量級鎖定。若是這個操做失敗的話,JVM會先檢查對象的Mark Word是否是已經指向當前棧幀,若是是則直接進入同步代碼塊執行,不然說明對象已經被其餘線程搶佔了。若是同時有兩個線程以上爭用同一個鎖,那輕量級鎖再也不有效,要膨脹爲重量級鎖,鎖標記狀態更新爲「10」(Heavy-weight locked),後面等待鎖的線程直接進入阻塞狀態。

輕量級鎖的解鎖過程,也是同樣的,經過CAS來實現,若是對象的Mark Word仍然指向線程的鎖記錄,那麼就用CAS操做把對象當前的Mark Word和線程複製的Displace Mark Word替換回來。

4.4.3、偏向鎖

  若是說輕量級鎖是在無競爭的狀況下使用CAS操做消除同步使用的互斥量,那偏向鎖就是在無競爭的狀況下將整個同步都消除掉,連CAS都不操做了。偏向的意思就是這個對象的鎖會偏向於第一個獲取到它的線程,若是再接下來的過程當中,該鎖沒有被其餘線程獲取,則持有偏向鎖的線程將永遠不須要同步。

是否開啓偏向鎖模式,看的是參數UseBiasedLocking,這個在synchronizer.cpp文件中也能夠看到。

if (UseBiasedLocking) {
    BiasedLocking::revoke_and_rebias(obj, false, THREAD);
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }

若是啓用了偏向鎖,那麼當鎖對象第一次被線程獲取的時候,JVM會把對象頭的標誌位置爲「01」,Biased/Biasable,同時使用CAS操做把得到鎖的線程ID記錄在對象的Mark Word之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入到這個鎖的相關同步塊時JVM均不用再次進行同步操做。當另外有線程去嘗試獲取這個鎖時,偏向模式宣佈結束,恢復到Unlocked或者是Light-weight locked,後續的同步操做就和上述輕量級鎖那樣執行。

                 

提綱:先描述了爲何會用到synchronized,再學習了同步的用法及背後的實現,最後到JVM中ObjectMonitor,這裏沒有分析ObjectMonitor具體是怎麼作的,最後學了一下鎖的優化技術。

 

參考資料:

https://github.com/lingjiango/ConcurrentProgramPractice

https://www.javaworld.com/article/2076971/java-concurrency/how-the-java-virtual-machine-performs-thread-synchronization.html

https://wiki.openjdk.java.net/display/HotSpot/Synchronization

https://stackoverflow.com/questions/26357186/what-is-in-java-object-header

https://stackoverflow.com/questions/36371149/reentrantlock-vs-synchronized-on-cpu-level

https://www.ibm.com/developerworks/java/library/j-jtp10264/

https://stackoverflow.com/questions/47605/string-concatenation-concat-vs-operator

<<HotSpot實戰>>

相關文章
相關標籤/搜索