Java并發(fā)編程之Synchronized關(guān)鍵字
并發(fā)編程的重點也是難點是數(shù)據(jù)同步、線程安全、鎖。要編寫線程安全的代碼,其核心在于對共享和可變的狀態(tài)的訪問進行管理。
共享意味著變量可以由多個線程訪問,而可變則意味著變量的值在其生命周期內(nèi)可以發(fā)生變化。
當多個線程訪問某個狀態(tài)變量且其中有一個線程執(zhí)行寫入操作時,必須采用同步機制來協(xié)同這些線程對變量的訪問。
Java中的主要同步機制是關(guān)鍵字synchronized,它提供了一種獨占的加鎖方式。
勾勾從一下幾個方面來學習synchronized:

關(guān)鍵字synchronized的特性
synchronized關(guān)鍵字可以實現(xiàn)一個簡單的策略來防止線程干擾和內(nèi)存一致性錯誤,如果一個對象對多個線程是可見的,那么該對象的所有讀和寫都需通過同步的方式。
synchronized的特性:
不可中斷:synchronized關(guān)鍵字提供了獨占的加鎖方式,一旦一個線程持有了鎖對象,其他線程將進入阻塞狀態(tài)或者等待狀態(tài),直到前一個線程釋放鎖,中間過程不可中斷。
原子性: synchronized關(guān)鍵字的不可中斷性保證了它的原子性。
可見性:synchronized關(guān)鍵字包含了兩個JVM指令:monitor enter和monitor exit,它能夠保證在任何時候任何線程執(zhí)行到monitor enter時都必須從主內(nèi)存中獲取數(shù)據(jù),而不是從線程工作內(nèi)存獲取數(shù)據(jù),在monitor exit之后,工作內(nèi)存被更新后的值必須存入主內(nèi)存,從而保證了數(shù)據(jù)可見性。
有序性:synchronized關(guān)鍵字修改的同步方法是串行執(zhí)行的,但其所修飾的代碼塊中的指令順序還是會發(fā)生改變的,這種改變遵守java happens-before規(guī)則。
可重入性:如果一個擁有鎖持有權(quán)的線程再次獲取鎖,則monitor的計數(shù)器會累加1,當線程釋放鎖的時候也會減1,直到計數(shù)器為0表示線程釋放了鎖的持有權(quán),在計數(shù)器不為0之前,其他線程都處于阻塞狀態(tài)。
關(guān)鍵字synchronized的用法
synchronized關(guān)鍵字鎖的是對象,修飾的可以是代碼塊和方法,但是不能修飾class對象以及變量。
代碼塊,鎖對象即是object
- private final Object obj = new Object();
- public void sync(){
- synchronized (obj){
- }
- }
方法,鎖對象即是this
- public synchronized void syncMethod(){
- }
靜態(tài)方法,鎖對象既是class
- public synchronized static void syncStaticMethod(){
- }
勾勾在開發(fā)中最常用的是用synchronized關(guān)鍵字修飾對象,可以控制鎖的粒度,所以針對最常用的場景勾勾去了解了它的字節(jié)碼文件,先來看看勾勾的測試用例:
- public class TestSynchronized {
- private int index;
- private final static int MAX = 100;
- public void sync(){
- synchronized (new Object()){
- while (index < MAX){
- index ++;
- }
- }
- }
- }
運行命令 “javac -encoding UTF-8 TestSynchronized.java”編輯成class文件,然后
運行命令“javap -c TestSynchronized.class”得到字節(jié)碼文件:
- public com.example.demo.articles.thread.TestSynchronized();
- Code:
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: return
- public void sync();
- Code:
- 0: new #2 // class java/lang/Object
- 3: dup
- 4: invokespecial #1 // Method java/lang/Object."<init>":()V
- 7: dup
- 8: astore_1
- 9: monitorenter //進入同步代碼塊
- 10: aload_0 //加載數(shù)據(jù)
- 11: getfield #3 // Field index:I
- 14: bipush 100
- 16: if_icmpge 32
- 19: aload_0
- 20: dup
- 21: getfield #3 // Field index:I
- 24: iconst_1
- 25: iadd // 加1操作
- 26: putfield #3 // Field index:I
- 29: goto 10 //跳轉(zhuǎn)至10行
- 32: aload_1
- 33: monitorexit // 退出同步代碼塊
- 34: goto 42 //跳轉(zhuǎn)至42行
- 37: astore_2 // 刷新數(shù)據(jù)
- 38: aload_1
- 39: monitorexit
- 40: aload_2
- 41: athrow
- 42: return
- Exception table:
- from to target type
- 10 34 37 any
- 37 40 37 any
monitorenter和monitorexit是成對出現(xiàn)的,有時候你看到的是一個monitorenter對應多個monitorexit,但是能肯定的一定點是每一個monitorexit之前必有一個monitorenter。
從字節(jié)碼文件中可以看到monitorenter之后執(zhí)行了aload操作,monitorexit之后執(zhí)行了astore操作。
TIPS:在使用synchronized關(guān)鍵字時注意事項
- 鎖的對象不能為空;
- 鎖的范圍不宜太大;
- 不要試圖使用不同的monitor來鎖同一個方法;
- 避免多個鎖交叉等待導致死鎖;
鎖膨脹
在jdk1.6之前,線程在獲取鎖時,如果鎖對象已經(jīng)被其他線程持有,此線程將掛起進入阻塞狀態(tài),喚醒阻塞線程的過程涉及到了用戶態(tài)和內(nèi)核態(tài)的切換,性能損耗比較大。
synchronized作為親兒子,混的太差肯定不行,在jdk1.6對其進行了優(yōu)化,將鎖狀態(tài)分為了無鎖狀態(tài),偏向鎖,輕量級鎖,重量級鎖。
鎖的升級過程既是:

在了解鎖的升級過程之前,勾勾重點理解了monitor和對象頭。
在第一次研究鎖膨脹的時候因為沒有花時間去理解這兩個概念,勾勾對鎖升級的記憶只持續(xù)了3天,最后勾勾又用了兩天的時間去學習對象頭和monitor,才算是真正的理解鎖的膨脹原理。所以大家在學習一個知識的時候,不要靠背去記憶一個知識點,一定要知其然。
每一個對象都與一個monitor相關(guān)聯(lián),monitor對象與實例對象一同創(chuàng)建并銷毀,monitor是C++支持的一個監(jiān)視器。鎖對象的爭奪既是爭奪monitor的持有權(quán)。
勾勾在OpenJdk源碼中找到了ObjectMonitor的源碼:
- // initialize the monitor, exception the semaphore, all other fields
- // are simple integers or pointers
- 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 ;
- }
- protected: // protected for jvmtiRawMonitor
- void * volatile _owner; // pointer to owning thread OR BasicLock
- volatile intptr_t _recursions; // recursion count, 0 for first entry
- private:
- int OwnerIsThread ; // _owner is (Thread *) vs SP/BasicLock
- ObjectWaiter * volatile _cxq ; // LL of recently-arrived threads blocked on entry.
- // The list is actually composed of WaitNodes, acting
- // as proxies for Threads.
- protected:
- ObjectWaiter * volatile _EntryList ; // Threads blocked on entry or reentry.
- private:
- Thread * volatile _succ ; // Heir presumptive thread - used for futile wakeup throttling
- Thread * volatile _Responsible ;
- int _PromptDrain ; // rqst to drain cxq into EntryList ASAP
- }
owner:指向線程的指針。即鎖對象關(guān)聯(lián)的monitor中的owner指向了哪個線程表示此線程持有了鎖對象。
waitSet:進入阻塞等待的線程隊列。當線程調(diào)用wait方法之后,就會進入waitset隊列,可以等待其他線程喚醒。
entryList:當多個線程進入同步代碼塊之后,處于阻塞狀態(tài)的線程就會被放入entryList中。
那什么是對象頭呢,它與synchronized又有什么關(guān)系呢?
在JVM中,對象在內(nèi)存中分為3塊區(qū)域:
- 對象頭Mark Word(標記字段):用于存儲對象的hashcode,分代年齡,鎖標志位,是否可偏向標志,在運行期間,其存儲的數(shù)據(jù)會發(fā)生變化。Klass Point(類型指針):該指針指向它的類元數(shù)據(jù),JVM通過這個指針確定對象是哪個類的實例。該指針的位長度為JVM的一個字大小,即32位的JVM為32位,64位的JVM為64位。
- 實例數(shù)據(jù)用于存放類的數(shù)據(jù)信息
- 填充數(shù)據(jù)虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍,當不滿足時需對其填充。
我們先通過一張圖了解下在鎖升級的過程中對象頭的變化:

接下來我們分析鎖升級的過程:
第一個分支鎖標志為01:
當線程運行到同步代碼塊時,首先會判斷鎖標志位,如果鎖標志位為01,則繼續(xù)判斷偏向標志。
如果偏向標志為0,則表示鎖對象未被其他線程持有,可以獲取鎖。此時當前線程通過CAS的方法修改線程ID,如果修改成功,此時鎖升級為偏向鎖。
如果偏向標志為1,則表示鎖對象已經(jīng)被占有。
進一步判斷線程id是否相等,相等則表示當前線程持有的鎖對象,可以重入。
如果線程id不相等,則表示鎖被其他線程占有。
需進一步判斷持有偏向鎖的線程的活動狀態(tài),如果原持有偏向鎖線程已經(jīng)不活動或者已經(jīng)退出同步代碼塊,則表示原持有偏向鎖的線程可以釋放偏向鎖。釋放后偏向鎖回到無鎖狀態(tài),線程再次嘗試獲取鎖。主要是因為偏向鎖不會主動釋放,只有其他線程競爭偏向鎖的時候才會釋放。
如果原持有偏向鎖的線程沒有退出同步代碼塊,則鎖升級為輕量級鎖。
偏向鎖的流程圖如下:

第二個分支鎖標志為00:
在第一個分支中我們了解到在如果偏向鎖已經(jīng)被其他線程占有,則鎖會被升級為輕量級鎖。
此時原持有偏向鎖的線程的棧幀中分配鎖記錄Lock Record,將對象頭中的Mark Word信息拷貝到鎖記錄中,Mark Word的指針指向了原持有偏向鎖線程中的鎖記錄,此時原持有偏向鎖的線程獲取輕量級鎖,繼續(xù)執(zhí)行同步塊代碼。
如果線程在運行同步塊時發(fā)現(xiàn)鎖的標志位為00,則在當前線程的棧幀中分配鎖記錄,拷貝對象頭中的Mark Word到鎖記錄中。通過CAS操作將Mark Word中的指針指向自己的鎖記錄,如果成功,則當前線程獲取輕量鎖。
如果修改失敗,則進入自旋,不斷通過CAS的方式修改Mark Word中的指針指向自己的鎖記錄。
當自旋超過一定次數(shù)(默認10次),則升級為重量鎖。
輕量鎖的鎖是主動釋放的,持有輕量鎖的線程在執(zhí)行完同步代碼塊后,會先判斷Mark Word中的指針是否依然指向自己,且自己鎖記錄中的Mark Word信息與鎖對象的Mark Word信息一致,如果都一致,則釋放鎖成功。
如果不一致,則鎖有可能已經(jīng)被升級為重量鎖。
輕量級流程圖如下圖:

第三個分支鎖標志位為10:
鎖標志為10時,此時鎖已經(jīng)為重量鎖,線程會先判斷monitor中的owner指針指向是否為自己,是則獲取重量鎖,不是則會掛起。
整個鎖升級過程中的流程圖如下,如果看懂了一定要自己畫一遍。
總結(jié):
synchronized關(guān)鍵字是一種獨占的加鎖方式,不可中斷,保證了原子性,可見性,和有序性。
synchronized關(guān)鍵字可用于修飾方法和代碼塊,不能用于修飾變量和類。
多線程在執(zhí)行同步代碼塊時獲取鎖的過程在不同的鎖狀態(tài)下不一樣,偏向鎖是修改Mark Word中的線程ID,輕量鎖是修改Mark Word的指針指向自己的鎖記錄,重量鎖是修改monitor中的指針指向自己。
今天就學到這里了!收工!
并發(fā)編程、JVM、數(shù)據(jù)結(jié)構(gòu)基礎知識更新完了,后續(xù)還會慢慢補充!