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

關(guān)鍵字synchronized的特性
synchronized關(guān)鍵字可以實(shí)現(xiàn)一個(gè)簡單的策略來防止線程干擾和內(nèi)存一致性錯(cuò)誤,如果一個(gè)對象對多個(gè)線程是可見的,那么該對象的所有讀和寫都需通過同步的方式。
synchronized的特性:
不可中斷:synchronized關(guān)鍵字提供了獨(dú)占的加鎖方式,一旦一個(gè)線程持有了鎖對象,其他線程將進(jìn)入阻塞狀態(tài)或者等待狀態(tài),直到前一個(gè)線程釋放鎖,中間過程不可中斷。
原子性: synchronized關(guān)鍵字的不可中斷性保證了它的原子性。
可見性:synchronized關(guān)鍵字包含了兩個(gè)JVM指令:monitor enter和monitor exit,它能夠保證在任何時(shí)候任何線程執(zhí)行到monitor enter時(shí)都必須從主內(nèi)存中獲取數(shù)據(jù),而不是從線程工作內(nèi)存獲取數(shù)據(jù),在monitor exit之后,工作內(nèi)存被更新后的值必須存入主內(nèi)存,從而保證了數(shù)據(jù)可見性。
有序性:synchronized關(guān)鍵字修改的同步方法是串行執(zhí)行的,但其所修飾的代碼塊中的指令順序還是會(huì)發(fā)生改變的,這種改變遵守java happens-before規(guī)則。
可重入性:如果一個(gè)擁有鎖持有權(quán)的線程再次獲取鎖,則monitor的計(jì)數(shù)器會(huì)累加1,當(dāng)線程釋放鎖的時(shí)候也會(huì)減1,直到計(jì)數(shù)器為0表示線程釋放了鎖的持有權(quán),在計(jì)數(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 ++;
- }
- }
- }
- }
運(yùn)行命令 “javac -encoding UTF-8 TestSynchronized.java”編輯成class文件,然后
運(yùn)行命令“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 //進(jìn)入同步代碼塊
- 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)的,有時(shí)候你看到的是一個(gè)monitorenter對應(yīng)多個(gè)monitorexit,但是能肯定的一定點(diǎn)是每一個(gè)monitorexit之前必有一個(gè)monitorenter。
從字節(jié)碼文件中可以看到monitorenter之后執(zhí)行了aload操作,monitorexit之后執(zhí)行了astore操作。
TIPS:在使用synchronized關(guān)鍵字時(shí)注意事項(xiàng)
- 鎖的對象不能為空;
- 鎖的范圍不宜太大;
- 不要試圖使用不同的monitor來鎖同一個(gè)方法;
- 避免多個(gè)鎖交叉等待導(dǎo)致死鎖;
鎖膨脹
在jdk1.6之前,線程在獲取鎖時(shí),如果鎖對象已經(jīng)被其他線程持有,此線程將掛起進(jìn)入阻塞狀態(tài),喚醒阻塞線程的過程涉及到了用戶態(tài)和內(nèi)核態(tài)的切換,性能損耗比較大。
synchronized作為親兒子,混的太差肯定不行,在jdk1.6對其進(jìn)行了優(yōu)化,將鎖狀態(tài)分為了無鎖狀態(tài),偏向鎖,輕量級(jí)鎖,重量級(jí)鎖。
鎖的升級(jí)過程既是:

在了解鎖的升級(jí)過程之前,勾勾重點(diǎn)理解了monitor和對象頭。
在第一次研究鎖膨脹的時(shí)候因?yàn)闆]有花時(shí)間去理解這兩個(gè)概念,勾勾對鎖升級(jí)的記憶只持續(xù)了3天,最后勾勾又用了兩天的時(shí)間去學(xué)習(xí)對象頭和monitor,才算是真正的理解鎖的膨脹原理。所以大家在學(xué)習(xí)一個(gè)知識(shí)的時(shí)候,不要靠背去記憶一個(gè)知識(shí)點(diǎn),一定要知其然。
每一個(gè)對象都與一個(gè)monitor相關(guān)聯(lián),monitor對象與實(shí)例對象一同創(chuàng)建并銷毀,monitor是C++支持的一個(gè)監(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指向了哪個(gè)線程表示此線程持有了鎖對象。
waitSet:進(jìn)入阻塞等待的線程隊(duì)列。當(dāng)線程調(diào)用wait方法之后,就會(huì)進(jìn)入waitset隊(duì)列,可以等待其他線程喚醒。
entryList:當(dāng)多個(gè)線程進(jìn)入同步代碼塊之后,處于阻塞狀態(tài)的線程就會(huì)被放入entryList中。
那什么是對象頭呢,它與synchronized又有什么關(guān)系呢?
在JVM中,對象在內(nèi)存中分為3塊區(qū)域:
- 對象頭Mark Word(標(biāo)記字段):用于存儲(chǔ)對象的hashcode,分代年齡,鎖標(biāo)志位,是否可偏向標(biāo)志,在運(yùn)行期間,其存儲(chǔ)的數(shù)據(jù)會(huì)發(fā)生變化。Klass Point(類型指針):該指針指向它的類元數(shù)據(jù),JVM通過這個(gè)指針確定對象是哪個(gè)類的實(shí)例。該指針的位長度為JVM的一個(gè)字大小,即32位的JVM為32位,64位的JVM為64位。
- 實(shí)例數(shù)據(jù)用于存放類的數(shù)據(jù)信息
- 填充數(shù)據(jù)虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍,當(dāng)不滿足時(shí)需對其填充。
我們先通過一張圖了解下在鎖升級(jí)的過程中對象頭的變化:

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

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

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