解決線程原子性問(wèn)題,最常見(jiàn)的手段就是加鎖,Java提供了兩種加鎖的方式,一個(gè)synchronized隱式鎖,另外一個(gè)是通過(guò)J.U.C框架提供的Lock顯式加鎖。本文主要介紹一個(gè)Synchronized的實(shí)現(xiàn)方式。
synchronized概述
synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問(wèn)資源的同步性,synchronized 翻譯為中文的意思是同步,也稱(chēng)之為”同步鎖“。
synchronized的作用是保證在同一時(shí)刻, 被修飾的代碼塊或方法只會(huì)有一個(gè)線程執(zhí)行,以達(dá)到保證并發(fā)安全的效果。
synchronized的使用方式
基本語(yǔ)法
synchronized有兩個(gè)作用范圍:方法和局部代碼塊,代碼示例如下
public class SynchronizedDemo {
?
?private int v;
?private static int a;
?private final Object lock = new Object();
?
?// 修飾非靜態(tài)方法 對(duì)象鎖
?public synchronized void add(int value) {
? ?v += value; // 臨界區(qū)
}
?
?public void sub(int value) {
? ?// 修飾局部代碼塊 對(duì)象鎖
? ?synchronized (lock) {
? ? ?v -= value; // 臨界區(qū)
? }
}
?
?// 修飾靜態(tài)方法 類(lèi)鎖
?public static synchronized void multi(int value) {
? ?a *= value; // 臨界區(qū)
}
?
?public static void div(int value) {
? ?// 修飾局部代碼塊 類(lèi)鎖
? ?synchronized (SynchronizedDemo.class) {
? ? ?a /= value; // 臨界區(qū)
? }
}
}
復(fù)制代碼
java編譯器會(huì)在synchronized修飾的方法或代碼塊前后自動(dòng)Lock,unlock。
- synchronized修飾代碼塊,鎖定是個(gè)obj對(duì)象,或者是一個(gè)類(lèi),sychronized(this.class)
- synchronized修飾靜態(tài)方法,鎖定是當(dāng)前類(lèi)的class對(duì)象
- synchronized修飾非靜態(tài)方法,鎖定的是當(dāng)前實(shí)例對(duì)象this。
synchronized底層實(shí)現(xiàn)原理
synchronized對(duì)應(yīng)的字節(jié)碼
使用javap -verbose SynchronizedDemo
查看字節(jié)碼文件
public synchronized void add(int);
? descriptor: (I)V
? flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
? Code:
? ? stack=3, locals=2, args_size=2
? ? ? ? 0: aload_0
? ? ? ? 1: dup
? ? ? ? 2: getfield ? ? #4 ? ? ? ? ? ? ? ? // Field v:I
? ? ? ? 5: iload_1
? ? ? ? 6: iadd
? ? ? ? 7: putfield ? ? #4 ? ? ? ? ? ? ? ? // Field v:I
? ? ? 10: return
? ? LineNumberTable:
? ? ? line 10: 0
? ? ? line 11: 10
? ? ? ?
public void sub(int);
? descriptor: (I)V
? flags: (0x0001) ACC_PUBLIC
? Code:
? ? stack=3, locals=4, args_size=2
? ? ? ? 0: aload_0
? ? ? ? 1: getfield ? ? #3 ? ? ? ? ? ? ? ? // Field lock:Ljava/lang/Object;
? ? ? ? 4: dup
? ? ? ? 5: astore_2
? ? ? ? 6: monitorenter
? ? ? ? 7: aload_0
? ? ? ? 8: dup
? ? ? ? 9: getfield ? ? #4 ? ? ? ? ? ? ? ? // Field v:I
? ? ? 12: iload_1
? ? ? 13: isub
? ? ? 14: putfield ? ? #4 ? ? ? ? ? ? ? ? // Field v:I
? ? ? 17: aload_2
? ? ? 18: monitorexit
? ? ? 19: goto ? ? ? ? 27
? ? ? 22: astore_3
? ? ? 23: aload_2
? ? ? 24: monitorexit
? ? ? 25: aload_3
? ? ? 26: athrow
? ? ? 27: return
? ? Exception table:
? ? ? ? from ? to target type
? ? ? ? ? ? 7 ? 19 ? 22 ? any
? ? ? ? ? 22 ? 25 ? 22 ? any
? ? LineNumberTable:
? ? ? line 14: 0
? ? ? line 15: 7
? ? ? line 16: 17
? ? ? line 17: 27
? ? ? ?
public static synchronized void multi(int);
? descriptor: (I)V
? flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
? Code:
? ? stack=2, locals=1, args_size=1
? ? ? ? 0: getstatic ? ? #5 ? ? ? ? ? ? ? ? // Field a:I
? ? ? ? 3: iload_0
? ? ? ? 4: imul
? ? ? ? 5: putstatic ? ? #5 ? ? ? ? ? ? ? ? // Field a:I
? ? ? ? 8: return
? ? LineNumberTable:
? ? ? line 20: 0
? ? ? line 21: 8
?
public static void div(int);
? descriptor: (I)V
? flags: (0x0009) ACC_PUBLIC, ACC_STATIC
? Code:
? ? stack=2, locals=3, args_size=1
? ? ? ? 0: ldc ? ? ? ? ? #6 ? ? ? ? ? ? ? ? // class com/shawn/study/deep/in/java/concurrency/thread/SynchronizedDemo
? ? ? ? 2: dup
? ? ? ? 3: astore_1
? ? ? ? 4: monitorenter
? ? ? ? 5: getstatic ? ? #5 ? ? ? ? ? ? ? ? // Field a:I
? ? ? ? 8: iload_0
? ? ? ? 9: idiv
? ? ? 10: putstatic ? ? #5 ? ? ? ? ? ? ? ? // Field a:I
? ? ? 13: aload_1
? ? ? 14: monitorexit
? ? ? 15: goto ? ? ? ? 23
? ? ? 18: astore_2
? ? ? 19: aload_1
? ? ? 20: monitorexit
? ? ? 21: aload_2
? ? ? 22: athrow
? ? ? 23: return
? ? Exception table:
? ? ? ? from ? to target type
? ? ? ? ? ? 5 ? 15 ? 18 ? any
? ? ? ? ? 18 ? 21 ? 18 ? any
? ? LineNumberTable:
? ? ? line 24: 0
? ? ? line 25: 5
? ? ? line 26: 13
? ? ? line 27: 23 ? ? ? ?
復(fù)制代碼
從上述展示的字節(jié)碼可以看出:
add()函數(shù)對(duì)應(yīng)的字節(jié)碼如下所示。實(shí)際上,編譯器只不過(guò)是在函數(shù)的flags中添加了ACC_SYNCHRONIZED標(biāo)記而已,其他部分跟沒(méi)有添加synchronized的add()函數(shù)的字節(jié)碼相同。
add()函數(shù)對(duì)應(yīng)的字節(jié)碼如下所示。字節(jié)碼通過(guò)monitorenter和monitorexit來(lái)標(biāo)記synchronized的作用范圍。除此之外,對(duì)于以下字節(jié)碼,我們有點(diǎn)需要解釋。其一,以下字節(jié)碼中有兩個(gè)monitorexit,添加第二個(gè)monitorexit的目的是為了在代碼拋出異常時(shí)仍然能解鎖。其二,前面講到,synchronized可以選擇指定使用哪個(gè)對(duì)象的Monitor鎖。具體使用哪個(gè)對(duì)象的Monitor鎖,在字節(jié)碼中,通過(guò)monitorenter前面的幾行字節(jié)碼來(lái)指定。
synchronized關(guān)鍵字底層使用的鎖叫做Monitor鎖。但是,我們無(wú)法直接創(chuàng)建和使用Monitor鎖。Monitor鎖是寄生存在的,每個(gè)對(duì)象都會(huì)擁有一個(gè)Monitor鎖。如果我們想要使用一個(gè)新的Monitor鎖,我們只需要使用一個(gè)新的對(duì)象,并在synchronized關(guān)鍵字后,附帶聲明要使用哪個(gè)對(duì)象的Monitor鎖即可。
-
當(dāng)使用sychronized修飾方法的時(shí)候,編譯器只不過(guò)是在函數(shù)的flags中添加了ACC_SYNCHRONIZED標(biāo)記而已,其他部分跟沒(méi)有添加synchronized的函數(shù)的字節(jié)碼相同。
-
當(dāng)使用synchronized修飾局部代碼塊的時(shí)候,字節(jié)碼通過(guò)monitorenter和monitorexit來(lái)標(biāo)記synchronized的作用范圍。但有兩點(diǎn)需要再解釋一下
- synchronized關(guān)鍵字底層使用的鎖叫做Monitor鎖。但是,我們無(wú)法直接創(chuàng)建和使用Monitor鎖。Monitor鎖是寄生存在的,每個(gè)對(duì)象都會(huì)擁有一個(gè)Monitor鎖,在字節(jié)碼中,通過(guò)monitorenter前面的幾行字節(jié)碼來(lái)指定。
- 以下字節(jié)碼中有兩個(gè)monitorexit,添加第二個(gè)monitorexit的目的是為了在代碼拋出異常時(shí)仍然能解鎖。
monitor鎖實(shí)現(xiàn)原理
synchronized在底層使用不同的鎖來(lái)實(shí)現(xiàn),重量級(jí)鎖,輕量級(jí)鎖,偏向鎖等。
實(shí)際上,synchronized使用的重量級(jí)鎖,就是前面提到的對(duì)象上的Monitor鎖。JVM有不同的實(shí)現(xiàn)版本,因此,Monitor鎖也有不同的實(shí)現(xiàn)方式。在Hotspot JVM實(shí)現(xiàn)中,Monitor鎖對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)為ObjectMonitor類(lèi)。因?yàn)镠otspot JVM是用C++實(shí)現(xiàn)的,所以,ObjectMonitor也是用C++代碼定義的。ObjectMonitor包含的代碼很多,我們只羅列一些與其基本實(shí)現(xiàn)原理相關(guān)的成員變量,如下所示。
class ObjectMonitor {
?void* ?volatile _object; // 該Monitor鎖所屬的對(duì)象
?void* ?volatile _owner; ?// 獲取到該Monitor鎖的線程
?ObjectWaiter* volatile _EntryList; // 存儲(chǔ)等待被喚醒的線程
?ObjectWaiter* volatile _cxq ; // 沒(méi)有獲取到鎖的線程
?ObjectWaiter* volatile _WaitSet; // 存儲(chǔ)調(diào)用了wait()的線程
}
復(fù)制代碼
monitor如何與對(duì)象關(guān)聯(lián)
_object表示該Monitor鎖所屬的對(duì)象,但是如何通過(guò)對(duì)象來(lái)找到對(duì)應(yīng)的Monitor鎖呢?對(duì)象的存儲(chǔ)結(jié)構(gòu)如下:
其中Mark Word是個(gè)可變字段,根據(jù)不同的場(chǎng)景記錄不同的信息,monitor鎖的信息就是記錄在此。
monitor如何實(shí)現(xiàn)加鎖,解鎖
ObjectMonitor Enter方法
互斥鎖的基本功能:
- 多個(gè)線程競(jìng)爭(zhēng)獲取鎖;
- 沒(méi)有獲取到鎖的線程排隊(duì)等待獲取鎖;
- 鎖釋放之后會(huì)通知排隊(duì)等待鎖的線程去競(jìng)爭(zhēng)鎖;
- 沒(méi)有獲取鎖的線程會(huì)阻塞,并且對(duì)應(yīng)的內(nèi)核線程不再分配時(shí)間片;
- 阻塞線程獲取到鎖之后取消阻塞,并且對(duì)應(yīng)的內(nèi)核線程恢復(fù)分配時(shí)間片。
其中加鎖源代碼如下:
void ObjectMonitor::EnterI(TRAPS) {
...
?// Try the lock - TATAS
?if (TryLock (Self) > 0) {
? ?assert(_succ != Self, "invariant");
? ?assert(_owner == Self, "invariant");
? ?assert(_Responsible != Self, "invariant");
? ?return;
}
...
?// We try one round of spinning *before* enqueueing Self.
?//
?// If the _owner is ready but OFFPROC we could use a YieldTo()
?// operation to donate the remainder of this thread's quantum
?// to the owner. This has subtle but beneficial affinity
?// effects.
?
?if (TrySpin (Self) > 0) {
? ?assert(_owner == Self, "invariant");
? ?assert(_succ != Self, "invariant");
? ?assert(_Responsible != Self, "invariant");
? ?return;
}
...
?ObjectWaiter node(Self);
?// Push "Self" onto the front of the _cxq.
?// Once on cxq/EntryList, Self stays on-queue until it acquires the lock.
?// Note that spinning tends to reduce the rate at which threads
?// enqueue and dequeue on EntryList|cxq.
?ObjectWaiter * nxt;
?for (;;) {
? ?node._next = nxt = _cxq;
? ?if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt) break;
?
? ?// Interference - the CAS failed because _cxq changed. Just retry.
? ?// As an optional optimization we retry the lock.
? ?if (TryLock (Self) > 0) {
? ? ?assert(_succ != Self, "invariant");
? ? ?assert(_owner == Self, "invariant");
? ? ?assert(_Responsible != Self, "invariant");
? ? ?return;
? }
}
...
?for (;;) {
? ?if (TryLock(Self) > 0) break;
? ...
? ?if ((SyncFlags & 2) && _Responsible == NULL) {
? ? ?Atomic::replace_if_null(Self, &_Responsible);
? }
? ?// park self
? ?if (_Responsible == Self || (SyncFlags & 1)) {
? ? ?TEVENT(Inflated enter - park TIMED);
? ? ?Self->_ParkEvent->park((jlong) recheckInterval);
? ? ?// Increase the recheckInterval, but clamp the value.
? ? ?recheckInterval *= 8;
? ? ?if (recheckInterval > MAX_RECHECK_INTERVAL) {
? ? ? ?recheckInterval = MAX_RECHECK_INTERVAL;
? ? }
? } else {
? ? ?TEVENT(Inflated enter - park UNTIMED);
? ? ?Self->_ParkEvent->park();
? }
?
? ?if (TryLock(Self) > 0) break;
? ...
}
...
?if (_Responsible == Self) {
? ?_Responsible = NULL;
}
?// 善后處理,比如將當(dāng)前線程從等待隊(duì)列 CXQ 中移除
...
}
?
復(fù)制代碼
多個(gè)線程競(jìng)爭(zhēng)獲取鎖
多個(gè)線程同時(shí)請(qǐng)求獲取Monitor鎖時(shí),JVM會(huì)通過(guò)CAS操作,先檢查_owner
是否是null,如果_owner
是null,再將自己的Thread對(duì)象的地址賦值給_owner
,那么誰(shuí)就獲取到了monitor鎖。
源代碼:
int ObjectMonitor::TryLock (Thread * Self) {
? for (;;) {
? ? ?void * own = _owner ;
? ? ?if (own != NULL) return 0 ;
? ? ?if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
? ? ? ? // Either guarantee _recursions == 0 or set _recursions = 0.
? ? ? ? assert (_recursions == 0, "invariant") ;
? ? ? ? assert (_owner == Self, "invariant") ;
? ? ? ? // CONSIDER: set or assert that OwnerIsThread == 1
? ? ? ? return 1 ;
? ? }
? ? ?// The lock had been free momentarily, but we lost the race to the lock.
? ? ?// Interference -- the CAS failed.
? ? ?// We can either return -1 or retry.
? ? ?// Retry doesn't make as much sense because the lock was just acquired.
? ? ?if (true) return -1 ;
? }
}
復(fù)制代碼
源碼中有段需要注意的是:先檢查再執(zhí)行這類(lèi)復(fù)合操作是非線程安全的,那么就會(huì)存在多個(gè)線程有可能同時(shí)檢查到_owner
為null的情況,然后都去改變_owner
。為了解決這個(gè)問(wèn)題,JVM采用CPU提供的cmpxchg_ptr
指令,通過(guò)給總線加鎖的方式,來(lái)保證了以上CAS操作的線程安全性。
沒(méi)有獲取到鎖的線程排隊(duì)等待獲取鎖
多個(gè)線程競(jìng)爭(zhēng)Monitor鎖,成功獲取到鎖的線程就去執(zhí)行代碼,沒(méi)有獲取到鎖的線程會(huì)放入ObjectMonitor的_cxq單向鏈表中等待鎖
鎖釋放之后會(huì)通知排隊(duì)等待鎖的線程去競(jìng)爭(zhēng)鎖
當(dāng)持有Monitor鎖的線程釋放了鎖之后,JVM會(huì)從_EntryList
中取出一個(gè)線程,再取競(jìng)爭(zhēng)Monitor鎖。
如果_EntryList
中沒(méi)有線程,JVM會(huì)先將_CXQ
中所有線程全部搬移到_EntryList
中,然后再?gòu)?code>_EntryList中取線程。
沒(méi)有獲取鎖的線程會(huì)阻塞,并且對(duì)應(yīng)的內(nèi)核線程不再分配時(shí)間片
一個(gè)java線程會(huì)對(duì)應(yīng)一個(gè)內(nèi)核線程。應(yīng)用程序會(huì)將java線程要執(zhí)行的代碼,交給其對(duì)應(yīng)的內(nèi)核線程來(lái)執(zhí)行。內(nèi)核線程在執(zhí)行過(guò)程中,如果沒(méi)有競(jìng)爭(zhēng)到鎖,則內(nèi)核線程會(huì)調(diào)用park()函數(shù)將自己阻塞,這樣CPU就不再分配時(shí)間片給它。
阻塞線程獲取到鎖之后取消阻塞,并且對(duì)應(yīng)的內(nèi)核線程恢復(fù)分配時(shí)間片
持有鎖的線程在釋放鎖之后,從_EntryList
中取出一個(gè)線程時(shí),就會(huì)調(diào)用unpark()函數(shù),取消對(duì)應(yīng)內(nèi)核線程的阻塞狀態(tài),這樣它才能有機(jī)會(huì)去競(jìng)爭(zhēng)monitor鎖
ObjectMonitor Enter方法總結(jié):
- ObjectMonitor 內(nèi)部通過(guò)一個(gè) CXQ 隊(duì)列保存所有的等待線程
- 在實(shí)際進(jìn)入隊(duì)列之前,會(huì)反復(fù)嘗試 lock,在某些系統(tǒng)上會(huì)存在 CPU 親和力的優(yōu)化
- 入隊(duì)的時(shí)候,通過(guò) ObjectWaiter 對(duì)象將當(dāng)前線程包裹起來(lái),并且入到 CXQ 隊(duì)列的頭部
- 入隊(duì)成功以后,會(huì)根據(jù)當(dāng)前線程是否為第一個(gè)等待線程做不同的處理
- 如果是第一個(gè)等待線程,會(huì)根據(jù)一個(gè)簡(jiǎn)單的「退避算法」來(lái)有條件的 wait
- 如果不是第一個(gè)等待線程,那么會(huì)執(zhí)行無(wú)限期等待
- 線程的 park 在 posix 系統(tǒng)上是通過(guò) pthread_cond_wait() 實(shí)現(xiàn)的
- 當(dāng)一個(gè)線程獲得對(duì)象鎖成功之后,就可以執(zhí)行自定義的同步代碼塊了
ObjectMonitor exit方法
當(dāng)前線程執(zhí)行完代碼塊以后,會(huì)進(jìn)入到ObjectMonitor exit方法,釋放當(dāng)前對(duì)象鎖,方便下一個(gè)線程來(lái)獲取這個(gè)鎖,下面我們逐步分析下 exit 的實(shí)現(xiàn)過(guò)程。
exit函數(shù)方法較長(zhǎng),但是整體上的結(jié)構(gòu)比較清晰。
void ObjectMonitor::exit(bool not_suspended, TRAPS) {
?for (;;) {
? ?//...
? ?ObjectWaiter * w = NULL;
? ?int QMode = Knob_QMode;
? ?if (QMode == 2 && _cxq != NULL) {
? ? ?//
? }
?
? ?if (QMode == 3 && _cxq != NULL) {
? ? ?//
? }
?
? ?if (QMode == 4 && _cxq != NULL) {
? ? ?//
? }
?
? ?// ...
?
? ?if (QMode == 1) {
? ? ?//
? } else {
? ? ?// QMode == 0 or QMode == 2
? }
? ?// ...
}
}
?
復(fù)制代碼
上面的 exit 函數(shù)整體上分為如下幾個(gè)部分:
- 根據(jù) Knob_QMode 的值和 _cxq 是否為空?qǐng)?zhí)行不同策略
- 根據(jù)一定策略喚醒等待隊(duì)列中的下一個(gè)線程
其中Knob_QMode這個(gè)變量主要用來(lái)指定在 exit 的時(shí)候 EntryList 和 CXQ 隊(duì)列之間的喚醒關(guān)系,也就是說(shuō),當(dāng) EntryList 和 CXQ 中都有等待的線程時(shí),因?yàn)?exit 之后只能有一個(gè)線程得到鎖,這個(gè)時(shí)候選擇喚醒哪個(gè)隊(duì)列中的線程是一個(gè)值得考慮的事情。JVM默認(rèn)值為0,我暫時(shí)沒(méi)有找到可以修改Knob_QMode的方法,除了重新編譯JVM源代碼,所以,這里我們暫時(shí)只討論Knob_QMode=0的情況。代碼如下:
void ObjectMonitor::exit(bool not_suspended, TRAPS) {
? ...
?for(;;) {
? ?ObjectWaiter * w = NULL ;
? ?w = _EntryList ;
? ?if (w != NULL) {
? ? ?assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
? ? ?ExitEpilog (Self, w) ;
? ? ?return ;
? }
? ?w = _cxq ;
? ?if (w == NULL) continue ;
? ?for (;;) {
? ? ?assert (w != NULL, "Invariant") ;
? ? ?ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
? ? ?if (u == w) break ;
? ? ?w = u ;
? }
? ?assert (w != NULL ? ? ? ? ? ? , "invariant") ;
? ?assert (_EntryList ?== NULL ? , "invariant") ;
? ?// 抽離出來(lái)的QMode == 0 or QMode == 2情況下代碼;
? ?_EntryList = w ;
? ?ObjectWaiter * q = NULL ;
? ?ObjectWaiter * p ;
? ?// 將單向鏈表構(gòu)造成雙向環(huán)形鏈表;
? ?for (p = w ; p != NULL ; p = p->_next) {
? ? ?guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
? ? ?p->TState = ObjectWaiter::TS_ENTER ;
? ? ?p->_prev = q ;
? ? ?q = p ;
? }
? ?// _succ表示已經(jīng)存在喚醒的線程;
? ?if (_succ != NULL) continue;
? ?w = _EntryList ;
? ?if (w != NULL) {
? ? ?guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
? ? ?ExitEpilog (Self, w) ;
? ? ?return ;
? }
}
}
復(fù)制代碼
- 若EntryList隊(duì)列的頭節(jié)點(diǎn)_EntryList不為null,那么直接喚醒該頭節(jié)點(diǎn)封裝的線程,然后返回;
- 1的條件不滿(mǎn)足,程序繼續(xù)向下執(zhí)行,若cxq隊(duì)列的頭節(jié)點(diǎn)_cxq為null,則跳過(guò)當(dāng)次循環(huán);
- 若程序繼續(xù)向下執(zhí)行說(shuō)明cxq隊(duì)列不為空,EntryList隊(duì)列為空。接下來(lái)是一個(gè)內(nèi)嵌的for循環(huán),目的是取出cxq隊(duì)列中的所有元素,方法是通過(guò)一個(gè)臨時(shí)變量指針獲得構(gòu)成隊(duì)列的整個(gè)鏈表,然后將_cxq指針置為NULL;
- 第二個(gè)內(nèi)嵌for循環(huán)是
QMode == 0
策略的內(nèi)容,目的在于將第三步得到的單向鏈表傾倒(drain)進(jìn)EntryList隊(duì)列,具體方法是將_EntryList指針指向單向鏈表的頭節(jié)點(diǎn),然后通過(guò)for循環(huán)將單向鏈表構(gòu)造成雙向環(huán)形鏈表; - 通過(guò)ExitEpilog函數(shù)釋放monitor鎖并喚醒EntryList隊(duì)列的頭節(jié)點(diǎn);
鎖優(yōu)化
鎖的類(lèi)型
對(duì)于一個(gè)synchronized鎖,如果它只被一個(gè)線程使用,那么,synchronzied鎖底層使用偏向鎖來(lái)實(shí)現(xiàn)。如果它被多個(gè)線程交叉使用(你用完我再用),不存在競(jìng)爭(zhēng)使用的情況,那么,synchronized鎖底層使用輕量級(jí)鎖來(lái)實(shí)現(xiàn)。如果它存在被競(jìng)爭(zhēng)使用的情況,那么,synchronized鎖底層使用重量級(jí)鎖來(lái)實(shí)現(xiàn)。
上面再講到重量級(jí)鎖需要用到對(duì)象頭的Mark Word,實(shí)際上,偏向鎖和輕量級(jí)鎖也要用到Mark Word。
無(wú)鎖在Mark Word中的記錄有unused(25bits)、hashCode(31bits)、cms_free、GC age、偏向1、鎖標(biāo)志位01
偏向鎖
偏向鎖在Mark Word中的記錄有,threadId(51bits)、epoch(2bits)、cms_free(1bit)、GC age(4bits)、偏向1,鎖標(biāo)志位01
如果我們?cè)O(shè)置了JVM參數(shù)-XX:BiasedLockingStartupDelay=0
,那么,Mark Word會(huì)在對(duì)象創(chuàng)建之后,直接進(jìn)入偏向鎖狀態(tài)。
如上圖所示:
- 當(dāng)一個(gè)對(duì)象被創(chuàng)建出來(lái),還沒(méi)有持有偏向鎖,此時(shí)Mark Word字段中的threadID為0,當(dāng)前線程會(huì)使用CPU提供的CAS原子操作來(lái)競(jìng)爭(zhēng)這個(gè)偏向鎖。
- 當(dāng)threadID并未指向當(dāng)前線程,則通過(guò)CAS操作競(jìng)爭(zhēng)鎖。如果競(jìng)爭(zhēng)成功,則將Mark Word中threadID設(shè)置為當(dāng)前線程ID
- 如果CAS獲取偏向鎖失敗,則表示有競(jìng)爭(zhēng)。當(dāng)?shù)竭_(dá)全局安全點(diǎn)(safepoint)時(shí)獲得偏向鎖的線程被掛起,偏向鎖升級(jí)為輕量級(jí)鎖,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼。
- 如果CAS已經(jīng)成功獲取到偏向鎖,那就開(kāi)始執(zhí)行代碼,如果執(zhí)行完代碼,線程也不會(huì)立刻解鎖偏向鎖,也就是不會(huì)更改threadID為0。這是偏向鎖有別于輕量級(jí)鎖和重量級(jí)鎖。這樣做的目的是提高加鎖的效率,當(dāng)同一個(gè)線程再次請(qǐng)求這個(gè)偏向鎖的時(shí)候,線程會(huì)查看Mark Word,發(fā)現(xiàn)還是處于偏向鎖狀態(tài),并且threadID就是自己的threadID,線程不再需要做任何加鎖操作,就可以直接執(zhí)行業(yè)務(wù)代碼。
- 偏向鎖不會(huì)主動(dòng)解鎖,當(dāng)其他線程嘗試獲取偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,JVM需要暫停持有偏向鎖的線程,然后查看它是否還在使用這個(gè)偏向鎖,如果線程不再使用這個(gè)偏向鎖,那么jvm就會(huì)將Mark Word設(shè)置為無(wú)鎖狀態(tài)。如果線程還在使用這個(gè)偏向鎖,那么虛擬機(jī)就將偏向鎖升級(jí)為輕量級(jí)鎖。
jvm需要根據(jù)持有偏向鎖的線程是否正在使用偏向鎖,來(lái)決定將鎖升級(jí)為無(wú)鎖還是偏向鎖,這是一個(gè)CAS的復(fù)合操作,存在線程安全問(wèn)題,但又無(wú)法使用CPU提供的CAS指令來(lái)實(shí)現(xiàn),所以解決方案就是jvm會(huì)復(fù)用垃圾回收器中的STW功能,來(lái)停止持有偏向鎖的線程。
輕量級(jí)鎖和自旋鎖
當(dāng)一個(gè)線程去競(jìng)爭(zhēng)鎖時(shí),它會(huì)先檢查Mark Word的的鎖標(biāo)志位,如果鎖標(biāo)志位是01并且相鄰偏向位為0(無(wú)鎖狀態(tài))或鎖標(biāo)志位是00(輕量級(jí)鎖狀態(tài)),那么,這就說(shuō)明鎖已經(jīng)升級(jí)到了輕量級(jí)鎖。
如果是無(wú)鎖狀態(tài),jvm會(huì)將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝(官方稱(chēng)之為Displaced Mark Word),其目的主要是為了輕量級(jí)鎖解鎖時(shí)快速恢復(fù)到無(wú)鎖狀態(tài)。
拷貝成功后,JVM將使用CAS操作嘗試將Mark Word中的Lock Record指針更新為指向自己的Lock Record。
如果更新成功,那么這個(gè)線程就擁有了該對(duì)象的鎖,并將對(duì)象的Mark Word的標(biāo)志位設(shè)置為“00”,表示此對(duì)象已經(jīng)是輕量級(jí)鎖狀態(tài)。
如果更新失敗,按理來(lái)說(shuō)應(yīng)該要升級(jí)成重量級(jí)鎖,但是JVM對(duì)此做了優(yōu)化,JVM首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說(shuō)明多個(gè)線程競(jìng)爭(zhēng)鎖。若當(dāng)前只有一個(gè)等待線程,則該線程通過(guò)自旋進(jìn)行等待。但是當(dāng)自旋超過(guò)一定的次數(shù)(默認(rèn)是10次,可以使用-XX:PreBlockSpin來(lái)更改),或者一個(gè)線程在持有鎖,一個(gè)在自旋,又有第三個(gè)來(lái)訪時(shí),輕量級(jí)鎖升級(jí)為重量級(jí)鎖。
這里有個(gè)問(wèn)題,自旋多少次合適?如果自旋次數(shù)太少,有可能剛升級(jí)為重量級(jí)鎖,另一個(gè)線程就釋放了輕量級(jí)鎖,這樣就很可惜。如果自旋次數(shù)很多,CPU就會(huì)做了很多無(wú)用功。針對(duì)這個(gè)問(wèn)題,JVM發(fā)明了自適應(yīng)自旋鎖。
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來(lái)開(kāi)啟。JDK 6中變?yōu)槟J(rèn)開(kāi)啟,并且引入了自適應(yīng)的自旋鎖)。如果上次自旋之后成功等到了另一個(gè)線程釋放輕量級(jí)鎖,那么下次自旋的次數(shù)就增加,如果上次自旋沒(méi)有等到等到另一個(gè)線程釋放輕量級(jí)鎖,那么下次自旋的次數(shù)就減少。
如果線程自旋等待輕量級(jí)鎖失敗,只能將輕量級(jí)線程升級(jí)為重量級(jí)線程。跟偏向鎖升級(jí)不同的是,輕量級(jí)鎖升級(jí)不需要STW,因?yàn)樗械腃AS操作都是由硬件提供的原子CAS指令來(lái)完成的。
重量級(jí)鎖
升級(jí)為重量級(jí)鎖時(shí),鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,此時(shí)Mark Word中存儲(chǔ)的是指向重量級(jí)鎖的指針,此時(shí)等待鎖的線程都會(huì)進(jìn)入阻塞狀態(tài)。這個(gè)就是創(chuàng)建Monitor鎖的過(guò)程
總結(jié)
綜上,偏向鎖通過(guò)對(duì)比Mark Word解決加鎖問(wèn)題,避免執(zhí)行CAS操作。而輕量級(jí)鎖是通過(guò)用CAS操作和自旋來(lái)解決加鎖問(wèn)題,避免線程阻塞和喚醒而影響性能。重量級(jí)鎖是將除了擁有鎖的線程以外的線程都阻塞。
鎖消除
JVM在執(zhí)行JIT編譯的時(shí)候,會(huì)根據(jù)對(duì)代碼的逃逸分析,去掉某些沒(méi)有必要的鎖。例如:
public String concat(String s1, String s2){
?StringBuffer buffer = new StringBuffer();
?buffer.append(s1).append(s2);
?return buffer.toString();
}
復(fù)制代碼
StringBuffer中的append函數(shù)使用了Sychronized修飾,加了鎖,但是,buffer是局部變量,不會(huì)被多線程共享,更不會(huì)在多線程環(huán)境下調(diào)用它的append()函數(shù),所以append函數(shù)的鎖可以被優(yōu)化消除。
鎖粗化
JVM在執(zhí)行JIT編譯時(shí),可能會(huì)擴(kuò)大鎖的范圍,對(duì)多個(gè)小范圍代碼的加鎖,合并成一個(gè)對(duì)大范圍代碼加鎖的操作叫做鎖粗化。例如:
private StringBuffer buffer = new StringBuffer();
?
public void reproduce(String s){
?for(int i = 0; i < 10000; i ++){
? ?buffer.append(s);
}
}
復(fù)制代碼
執(zhí)行10000次append,會(huì)加鎖解鎖10000次,通過(guò)鎖粗化,編譯器將append函數(shù)的鎖去掉,移到for循環(huán)外面,這樣只需要加鎖解鎖一次就可以了。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-494795.html
Synchronized的缺點(diǎn)
- 無(wú)法判斷獲取鎖的狀態(tài)。
- 雖然會(huì)自動(dòng)釋放鎖,但如果如果鎖的那個(gè)方法執(zhí)行時(shí)間較長(zhǎng)就會(huì)一直占用著不去釋放,不能讓使用同一把鎖的方法繼續(xù)執(zhí)行,影響程序的運(yùn)行。不能設(shè)置超時(shí)。
- 當(dāng)多個(gè)線程嘗試獲取鎖時(shí),未獲取到鎖的線程會(huì)不斷的嘗試獲取鎖,而不會(huì)發(fā)生中斷,這樣會(huì)造成性能消耗。
- 不能夠?qū)崿F(xiàn)公平鎖
作者:Shawn_Shawn
原文鏈接:https://juejin.cn/post/7195569817940672572
?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-494795.html
到了這里,關(guān)于java線程-synchronized詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!