警告⚠️:本文耗時很長,先做好心理準備,建議PC端瀏覽器瀏覽效果更佳。
-
當沒有競爭出現時,預設會使用偏向鎖。JVM 會利用 CAS 操作(compare and swap),在物件頭上的 Mark Word 部分設定執行緒 ID,以表示這個物件偏向於當前執行緒,所以並不涉及真正的互斥鎖。這樣做的假設是基於在很多應用場景中,大部分物件生命週期中最多會被一個執行緒鎖定,使用偏向鎖可以降低無競爭開銷。
-
如果有另外的執行緒試圖鎖定某個已經被偏向過的物件,JVM 就需要撤銷(revoke)偏向鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功,就使用輕量級鎖;否則,進一步升級為重量級鎖
public class TestDemo { } public class DemoExample1 { static TestDemo testDemo; public static void main(String[] args) throws Exception { testDemo= new TestDemo(); synchronized (testDemo){ System.out.println("lock ing"); testDemo.hashCode(); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }
javap -c DemoExample1.class
- 當執行 monitorenter 時,如果目標鎖物件的計數器為 0,那麼說明它沒有被其他執行緒所持有。在這個情況下,Java 虛擬機器會將該鎖物件的持有執行緒設定為當前執行緒,並且將其計數器加 1。
- 在目標鎖物件的計數器不為 0 的情況下,如果鎖物件的持有執行緒是當前執行緒,那麼 Java 虛擬機可以將其計數器加 1,否則需要等待,直至持有執行緒釋放該鎖。當執行 monitorexit 時,Java 虛擬機器則需將鎖物件的計數器減 1。當計數器減為 0 時,那便代表該鎖已經被釋放掉了。
- 之所以採用這種計數器的方式,是為了允許同一個執行緒重複獲取同一把鎖。舉個例子,如果一個 Java 類中擁有多個 synchronized 方法,那麼這些方法之間的相互呼叫,不管是直接的還是間接的,都會涉及對同一把鎖的重複加鎖操作。因此,我們需要設計這麼一個可重入的特性,來避免程式設計裡的隱式約束。
public class DemoExample3 {
public int sharedState;
public void nonSafeAction() {
while (sharedState < 100000) {
int former = sharedState++;
int latter = sharedState;
if (former != latter - 1) {
System.out.println("Observed data race, former is " +
former + ", " + "latter is " + latter);
}
}
}
public static void main(String[] args) throws InterruptedException {
final DemoExample3 demoExample3 = new DemoExample3();
Thread thread1 = new Thread() {
@Override
public void run() {
demoExample3.nonSafeAction();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
demoExample3.nonSafeAction();
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
public class DemoExample3 { public int sharedState; public void nonSafeAction() { while (sharedState < 100000) { synchronized (this) { int former = sharedState++; int latter = sharedState; if (former != latter - 1) { System.out.println("Observed data race, former is " + former + ", " + "latter is " + latter); } } } } public static void main(String[] args) throws InterruptedException { final DemoExample3 demoExample3 = new DemoExample3(); Thread thread1 = new Thread() { @Override public void run() { demoExample3.nonSafeAction(); } }; Thread thread2 = new Thread() { @Override public void run() { demoExample3.nonSafeAction(); } };
thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }
這次看下加上synchronized關鍵字的列印出來的結果:
// Handles the uncommon case in locking, i.e., contention or an inflated lock. JRT_BLOCK_ENTRY(void, SharedRuntime::complete_monitor_locking_C(oopDesc* _obj, BasicLock* lock, JavaThread* thread)) // Disable ObjectSynchronizer::quick_enter() in default config // on AARCH64 and ARM until JDK-8153107 is resolved. if (ARM_ONLY((SyncFlags & 256) != 0 &&) AARCH64_ONLY((SyncFlags & 256) != 0 &&) !SafepointSynchronize::is_synchronizing()) { // Only try quick_enter() if we're not trying to reach a safepoint // so that the calling thread reaches the safepoint more quickly. if (ObjectSynchronizer::quick_enter(_obj, thread, lock)) return; } // NO_ASYNC required because an async exception on the state transition destructor // would leave you with the lock held and it would never be released. // The normal monitorenter NullPointerException is thrown without acquiring a lock // and the model is that an exception implies the method failed. JRT_BLOCK_NO_ASYNC oop obj(_obj); if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); } Handle h_obj(THREAD, obj); //在 JVM 啟動時,我們可以指定是否開啟偏向鎖 if (UseBiasedLocking) { // Retry fast entry if bias is revoked to avoid unnecessary inflation //fast_enter 是我們熟悉的完整鎖獲取路徑 ObjectSynchronizer::fast_enter(h_obj, lock, true, CHECK); } else { //slow_enter 則是繞過偏向鎖,直接進入輕量級鎖獲取邏輯 ObjectSynchronizer::slow_enter(h_obj, lock, CHECK); } assert(!HAS_PENDING_EXCEPTION, "Should have no exception here"); JRT_BLOCK_END JRT_END
// ----------------------------------------------------------------------------- // Fast Monitor Enter/Exit // This the fast monitor enter. The interpreter and compiler use // some assembly copies of this code. Make sure update those code // if the following function is changed. The implementation is // extremely sensitive to race condition. Be careful. void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { if (UseBiasedLocking) { if (!SafepointSynchronize::is_at_safepoint()) { //biasedLocking定義了偏向鎖相關操作,revoke_and_rebias revokeatsafepoint 則定義了當檢測到安全點時的處理 BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD); if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) { return; } } else { assert(!attempt_rebias, "can not rebias toward VM thread"); BiasedLocking::revoke_at_safepoint(obj); } assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } //如果獲取偏向鎖失敗,則進入 slow_enter,鎖升級 slow_enter(obj, lock, THREAD); } // ----------------------------------------------------------------------------- // Interpreter/Compiler Slow Case // This routine is used to handle interpreter/compiler slow case // We don't need to use fast path here, because it must have been // failed in the interpreter/compiler code. void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); if (mark->is_neutral()) { // Anticipate successful CAS -- the ST of the displaced mark must // be visible <= the ST performed by the CAS. // 將目前的 Mark Word 複製到 Displaced Header 上 lock->set_displaced_header(mark); // 利用 CAS 設定物件的 Mark Wo if (mark == obj()->cas_set_mark((markOop) lock, mark)) { return; } // Fall through to inflate() … // 檢查存在競爭 } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock”); // 清除 lock->set_displaced_header(NULL); return; } // The object header will never be displaced to this lock, // so it does not matter what the value is, except that it // must be non-zero to avoid looking like a re-entrant lock, // and must not look locked either. // 重置 Displaced Header lock->set_displaced_header(markOopDesc::unused_mark()); //鎖膨脹 ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD); } // This routine is used to handle interpreter/compiler slow case // We don't need to use fast path here, because it must have // failed in the interpreter/compiler code. Simply use the heavy // weight monitor should be ok, unless someone find otherwise. void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) { fast_exit(object, lock, THREAD); } //鎖膨脹 ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) { // Inflate mutates the heap ... // Relaxing assertion for bug 6320749. assert (Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "invariant") ; for (;;) {//自旋 const markOop mark = object->mark() ; assert (!mark->has_bias_pattern(), "invariant") ; // The mark can be in one of the following states: // * Inflated - just return // * Stack-locked - coerce it to inflated // * INFLATING - busy wait for conversion to complete // * Neutral - aggressively inflate the object. // * BIASED - Illegal. We should never see this // CASE: inflated已膨脹,即重量級鎖 if (mark->has_monitor()) {//判斷當前是否為重量級鎖 ObjectMonitor * inf = mark->monitor() ;//獲取指向ObjectMonitor的指標 assert (inf->header()->is_neutral(), "invariant"); assert (inf->object() == object, "invariant") ; assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid"); return inf ; } // CASE: inflation in progress - inflating over a stack-lock.膨脹等待(其他執行緒正在從輕量級鎖轉為膨脹鎖) // Some other thread is converting from stack-locked to inflated. // Only that thread can complete inflation -- other threads must wait. // The INFLATING value is transient. // Currently, we spin/yield/park and poll the markword, waiting for inflation to finish. // We could always eliminate polling by parking the thread on some auxiliary list. if (mark == markOopDesc::INFLATING()) { TEVENT (Inflate: spin while INFLATING) ; ReadStableMark(object) ; continue ; } // CASE: stack-locked棧鎖(輕量級鎖) // Could be stack-locked either by this thread or by some other thread. // // Note that we allocate the objectmonitor speculatively, _before_ attempting // to install INFLATING into the mark word. We originally installed INFLATING, // allocated the objectmonitor, and then finally STed the address of the // objectmonitor into the mark. This was correct, but artificially lengthened // the interval in which INFLATED appeared in the mark, thus increasing // the odds of inflation contention. // // We now use per-thread private objectmonitor free lists. // These list are reprovisioned from the global free list outside the // critical INFLATING...ST interval. A thread can transfer // multiple objectmonitors en-mass from the global free list to its local free list. // This reduces coherency traffic and lock contention on the global free list. // Using such local free lists, it doesn't matter if the omAlloc() call appears // before or after the CAS(INFLATING) operation. // See the comments in omAlloc(). if (mark->has_locker()) { ObjectMonitor * m = omAlloc (Self) ;//獲取一個可用的ObjectMonitor // Optimistically prepare the objectmonitor - anticipate successful CAS // We do this before the CAS in order to minimize the length of time // in which INFLATING appears in the mark. m->Recycle(); m->_Responsible = NULL ; m->OwnerIsThread = 0 ; m->_recursions = 0 ; m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ; if (cmp != mark) {//CAS失敗//CAS失敗,說明衝突了,自旋等待//CAS失敗,說明衝突了,自旋等待//CAS失敗,說明衝突了,自旋等待 omRelease (Self, m, true) ;//釋放監視器鎖 continue ; // Interference -- just retry } // We've successfully installed INFLATING (0) into the mark-word. // This is the only case where 0 will appear in a mark-work. // Only the singular thread that successfully swings the mark-word // to 0 can perform (or more precisely, complete) inflation. // // Why do we CAS a 0 into the mark-word instead of just CASing the // mark-word from the stack-locked value directly to the new inflated state? // Consider what happens when a thread unlocks a stack-locked object. // It attempts to use CAS to swing the displaced header value from the // on-stack basiclock back into the object header. Recall also that the // header value (hashcode, etc) can reside in (a) the object header, or // (b) a displaced header associated with the stack-lock, or (c) a displaced // header in an objectMonitor. The inflate() routine must copy the header // value from the basiclock on the owner's stack to the objectMonitor, all // the while preserving the hashCode stability invariants. If the owner // decides to release the lock while the value is 0, the unlock will fail // and control will eventually pass from slow_exit() to inflate. The owner // will then spin, waiting for the 0 value to disappear. Put another way, // the 0 causes the owner to stall if the owner happens to try to // drop the lock (restoring the header from the basiclock to the object) // while inflation is in-progress. This protocol avoids races that might // would otherwise permit hashCode values to change or "flicker" for an object. // Critically, while object->mark is 0 mark->displaced_mark_helper() is stable. // 0 serves as a "BUSY" inflate-in-progress indicator // fetch the displaced mark from the owner's stack. // The owner can't die or unwind past the lock while our INFLATING // object is in the mark. Furthermore the owner can't complete // an unlock on the object, either. markOop dmw = mark->displaced_mark_helper() ; assert (dmw->is_neutral(), "invariant") ; //CAS成功,設定ObjectMonitor的_header、_owner和_object等 // Setup monitor fields to proper values -- prepare the monitor m->set_header(dmw) ; // Optimization: if the mark->locker stack address is associated // with this thread we could simply set m->_owner = Self and // m->OwnerIsThread = 1. Note that a thread can inflate an object // that it has stack-locked -- as might happen in wait() -- directly // with CAS. That is, we can avoid the xchg-NULL .... ST idiom. m->set_owner(mark->locker()); m->set_object(object); // TODO-FIXME: assert BasicLock->dhw != 0. // Must preserve store ordering. The monitor state must // be stable at the time of publishing the monitor address. guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ; object->release_set_mark(markOopDesc::encode(m)); // Hopefully the performance counters are allocated on distinct cache lines // to avoid false sharing on MP systems ... if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; TEVENT(Inflate: overwrite stacklock) ; if (TraceMonitorInflation) { if (object->is_instance()) { ResourceMark rm; tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s", (void *) object, (intptr_t) object->mark(), object->klass()->external_name()); } } return m ; } // CASE: neutral 無鎖 // TODO-FIXME: for entry we currently inflate and then try to CAS _owner. // If we know we're inflating for entry it's better to inflate by swinging a // pre-locked objectMonitor pointer into the object header. A successful // CAS inflates the object *and* confers ownership to the inflating thread. // In the current implementation we use a 2-step mechanism where we CAS() // to inflate and then CAS() again to try to swing _owner from NULL to Self. // An inflateTry() method that we could call from fast_enter() and slow_enter() // would be useful. assert (mark->is_neutral(), "invariant"); ObjectMonitor * m = omAlloc (Self) ; // prepare m for installation - set monitor to initial state m->Recycle(); m->set_header(mark); m->set_owner(NULL); m->set_object(object); m->OwnerIsThread = 1 ; m->_recursions = 0 ; m->_Responsible = NULL ; m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) { m->set_object (NULL) ; m->set_owner (NULL) ; m->OwnerIsThread = 0 ; m->Recycle() ; omRelease (Self, m, true) ; m = NULL ; continue ; // interference - the markword changed - just retry. // The state-transitions are one-way, so there's no chance of // live-lock -- "Inflated" is an absorbing state. } // Hopefully the performance counters are allocated on distinct // cache lines to avoid false sharing on MP systems ... if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ; TEVENT(Inflate: overwrite neutral) ; if (TraceMonitorInflation) { if (object->is_instance()) { ResourceMark rm; tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s", (void *) object, (intptr_t) object->mark(), object->klass()->external_name()); } } return m ; } }
1、整個膨脹過程在自旋下完成;
2、mark->has_monitor()方法判斷當前是否為重量級鎖,即Mark Word的鎖標識位為 10,如果當前狀態為重量級鎖,執行步驟(3),否則執行步驟(4);
3、mark->monitor()方法獲取指向ObjectMonitor的指標,並返回,說明膨脹過程已經完成;
4、如果當前鎖處於膨脹中,說明該鎖正在被其它執行緒執行膨脹操作,則當前執行緒就進行自旋等待鎖膨脹完成,這裡需要注意一點,雖然是自旋操作,但不會一直佔用cpu資源,每隔一段時間會通過os::NakedYield方法放棄cpu資源,或通過park方法掛起;如果其他執行緒完成鎖的膨脹操作,則退出自旋並返回;
5、如果當前是輕量級鎖狀態,即鎖標識位為 00,膨脹過程如下:
- 通過omAlloc方法,獲取一個可用的ObjectMonitor monitor,並重置monitor資料;
- 通過CAS嘗試將Mark Word設定為markOopDesc:INFLATING,標識當前鎖正在膨脹中,如果CAS失敗,說明同一時刻其它執行緒已經將Mark Word設定為markOopDesc:INFLATING,當前執行緒進行自旋等待膨脹完成;
- 如果CAS成功,設定monitor的各個欄位:_header、_owner和_object等,並返回;
6、如果是無鎖,重置監視器值;
-
偏向鎖是指一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖,降低獲取鎖的代價。
-
在大多數情況下,鎖總是由同一執行緒多次獲得,不存在多執行緒競爭,所以出現了偏向鎖。其目標就是在只有一個執行緒執行同步程式碼塊時能夠提高效能。
-
當一個執行緒訪問同步程式碼塊並獲取鎖時,會在Mark Word裡儲存鎖偏向的執行緒ID。線上程進入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word裡是否儲存著指向當前執行緒的偏向鎖。引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令即可。
-
偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態。撤銷偏向鎖後恢復到無鎖(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。
-
偏向鎖在JDK 6及以後的JVM裡是預設啟用的。可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程式預設會進入輕量級鎖狀態。
//建立一個啥都沒有的類:
public class TestDemo {}
public class DemoExample {
static TestDemo testDemo;
public static void main(String[] args) throws Exception {
//此處睡眠50000ms,取消jvm預設偏向鎖延遲4000ms
Thread.sleep(5000);
testDemo= new TestDemo();
//hash計算?
//testDemo.hashCode();
System.out.println("befor lock");
//無鎖:偏向鎖?
System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
synchronized (testDemo){
System.out.println("lock ing");
System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
}
System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
}
}
-
具體來說,線上程進行加鎖時,如果該鎖物件支援偏向鎖,那麼 Java 虛擬機器會通過 CAS操作,將當前執行緒的地址記錄在鎖物件的標記欄位之中,並且將標記欄位的最後三位設定為:1 01;
-
在接下來的執行過程中,每當有執行緒請求這把鎖,Java 虛擬機器只需判斷鎖物件標記欄位中:最後三位是否為: 1 01,是否包含當前執行緒的地址,以及 epoch 值是否和鎖物件的類的epoch 值相同。如果都滿足,那麼當前執行緒持有該偏向鎖,可以直接返回;
-
我們先從偏向鎖的撤銷講起。當請求加鎖的執行緒和鎖物件標記欄位保持的執行緒地址不匹配時(而且 epoch 值相等,如若不等,那麼當前執行緒可以將該鎖重偏向至自己),Java 虛擬機器需要撤銷該偏向鎖。這個撤銷過程非常麻煩,它要求持有偏向鎖的執行緒到達安全點,再將偏向鎖替換成輕量級鎖;
-
如果某一類鎖物件的總撤銷數超過了一個閾值(對應 jvm引數 -XX:BiasedLockingBulkRebiasThreshold,預設為 20),那麼 Java 虛擬機器會宣佈這個類的偏向鎖失效;(這裡說的就是批量重偏向)
product(intx, BiasedLockingBulkRebiasThreshold, 20, \ "Threshold of number of revocations per type to try to " \ "rebias all objects in the heap of that type") \ range(0, max_intx) \ constraint(BiasedLockingBulkRebiasThresholdFunc,AfterErgo) \
-
具體的做法便是在每個類中維護一個 epoch 值,你可以理解為第幾代偏向鎖。當設定偏向鎖時,Java 虛擬機器需要將該 epoch 值複製到鎖物件的標記欄位中;
-
在宣佈某個類的偏向鎖失效時,Java 虛擬機器實則將該類的 epoch 值加 1,表示之前那一代的偏向鎖已經失效。而新設定的偏向鎖則需要複製新的 epoch 值;
-
為了保證當前持有偏向鎖並且已加鎖的執行緒不至於因此丟鎖,Java 虛擬機器需要遍歷所有線程的 Java 棧,找出該類已加鎖的例項,並且將它們標記欄位中的 epoch 值加 1。該操作需要所有執行緒處於安全點狀態;
-
如果總撤銷數超過另一個閾值(對應 jvm 引數 -XX:BiasedLockingBulkRevokeThreshold,預設值為 40),那麼 Java 虛擬機器會認為這個類已經不再適合偏向鎖。此時,Java 虛擬機器會撤銷該類例項的偏向鎖,並且在之後的加鎖過程中直接為該類例項設定輕量級鎖(這裡說的就是偏向批量撤銷)
product(intx, BiasedLockingBulkRevokeThreshold, 40, \ "Threshold of number of revocations per type to permanently " \ "revoke biases of all objects in the heap of that type") \ range(0, max_intx) \ constraint(BiasedLockingBulkRevokeThresholdFunc,AfterErgo)
public class TestDemo { } public class DemoExample4 { public static void main(String[] args) throws InterruptedException { test1(); } public class DemoExample5 { public static void main(String[] args) throws InterruptedException { test1(); } /** * 僅證明批量重偏向 * @throws InterruptedException */ public static void test1() throws InterruptedException { List<TestDemo> list = new ArrayList<>(); for (int i = 0; i < 100; i++) { list.add(new TestDemo()); } Thread t1 = new Thread(()->{ System.out.println("加鎖前 get(0) 應該是無鎖可偏向 "+ ClassLayout.parseInstance(list.get(0)).toPrintable()); for (TestDemo a:list ) { synchronized (a){ System.out.print("加鎖 >"); } } System.out.println(); System.out.println("加鎖後 get(0) 應該是偏向鎖"+ClassLayout.parseInstance(list.get(0)).toPrintable()); try { TimeUnit.SECONDS.sleep(1000);//這裡不讓執行緒死,防止執行緒ID複用 } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); TimeUnit.SECONDS.sleep(5); Thread t2 = new Thread(()->{ for (int i = 0; i < 40; i++) { TestDemo a = list.get(i); synchronized (a){ System.out.print("加鎖 >"); } if (i==18){ System.out.println(); System.out.println("加鎖後 get(18) 應該是無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i==19){ //開始重偏向 System.out.println(); System.out.println("加鎖後 get(19) 應該是偏向鎖 "+ClassLayout.parseInstance(list.get(i)).toPrintable()); System.out.println("加鎖後 get(0) 應該是無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(0)).toPrintable()); System.out.println("加鎖後 get(99) 應該是偏向鎖 偏向t1 "+ClassLayout.parseInstance(list.get(99)).toPrintable()); } if (i==20){ System.out.println(); System.out.println("加鎖後 get(20) 應該是偏向鎖 "+ClassLayout.parseInstance(list.get(i)).toPrintable()); } } }); t2.start(); } }
public class TestDemo { } public class DemoExample7 { public static void main(String[] args) throws Exception { List<TestDemo> list = new ArrayList<>(); //初始化資料 for (int i = 0; i < 100; i++) { list.add(new TestDemo()); } Thread t1 = new Thread() { String name = "1"; public void run() { System.out.printf(name); for (TestDemo a : list) { synchronized (a) { if (a == list.get(10)) { System.out.println("t1 預期是偏向鎖" + 10 + ClassLayout.parseInstance(a).toPrintable()); } } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); Thread.sleep(5000); System.out.println("main 預期是偏向鎖" + 10 + ClassLayout.parseInstance(list.get(10)).toPrintable()); Thread t2 = new Thread() { String name = "2"; public void run() { System.out.printf(name); for (int i = 0; i < 100; i++) { TestDemo a = list.get(i); // hack 為了在批量重偏向發生後再次加鎖,前面使用了輕量級鎖的物件 if (i == 20) { a = list.get(9); } synchronized (a) { if (i == 10) { //已經經過偏向鎖撤銷,並使用輕量級鎖的物件,釋放後 狀態依為001 無鎖狀態 System.out.println("t2 i=10 get(1)預期是無鎖" + ClassLayout.parseInstance(list.get(1)).toPrintable()); //因為和t1交替使用物件a 沒有發生競爭,但偏向鎖已偏向,另外不滿足重偏向條件,所以使用輕量級鎖 System.out.println("t2 i=10 get(i) 預期輕量級鎖 " + i + ClassLayout.parseInstance(a).toPrintable()); } if (i == 19) { //已經經過偏向鎖撤銷,並使用輕量級鎖的物件,在批量重偏向發生後。不會影響現有的狀態 狀態依然為001 System.out.println("t2 i=19 get(10)預期是無鎖" + 10 + ClassLayout.parseInstance(list.get(10)).toPrintable()); //滿足重偏向條件後,已偏向的物件可以重新使用偏向鎖 將執行緒id指向當前執行緒,101 System.out.println("t2 i=19 get(i) 滿足重偏向條件20 預期偏向鎖 " + i + ClassLayout.parseInstance(a).toPrintable()); //滿足重偏向條件後,已偏向還為需要加鎖的物件依然偏向執行緒1 因為偏向鎖的撤銷是發生在下次加鎖的時候。這裡沒有執行到同步此物件,所以依然偏向t1 System.out.println("t2 i=19 get(i) 滿足重偏向條件20 但後面的物件沒有被加鎖,所以依舊偏向t1 " + i + ClassLayout.parseInstance(list.get(40)).toPrintable()); } if (i == 20) { //滿足重偏向條件後,再次加鎖之前使用了輕量級鎖的物件,依然輕量級鎖,證明重偏向這個狀態只針對偏向鎖。已經發生鎖升級的,不會退回到偏向鎖 System.out.println("t2 i=20 滿足偏向條件之後,之前被設定為無鎖狀態的物件,不可偏向,這裡使用的是輕量級鎖 get(9)預期是輕量級鎖 " + ClassLayout.parseInstance(a).toPrintable()); } } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t2.start(); Thread.sleep(5000); } }
public class TestDemo { } public class DemoExample6 { public static void main(String[] args) throws InterruptedException { test2(); } /** * 證明偏量偏向撤銷 * @throws InterruptedException */ public static void test2() throws InterruptedException { List<TestDemo> list = new ArrayList<TestDemo>(); for (int i = 0; i < 100; i++) { list.add(new TestDemo()); } Thread t1 = new Thread(()->{ System.out.println("加鎖前 get(0) 應該是無鎖可偏向 "+ClassLayout.parseInstance(list.get(0)).toPrintable()); for (TestDemo a:list ) { synchronized (a){ System.out.print("加鎖 >"); } } System.out.println(); System.out.println("加鎖後 get(0) 應該是偏向鎖"+ClassLayout.parseInstance(list.get(0)).toPrintable()); try { TimeUnit.SECONDS.sleep(1000);//這裡不讓執行緒死,防止執行緒ID複用 } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); TimeUnit.SECONDS.sleep(5); Thread t2 = new Thread(()->{ for (int i = 0; i < 100; i++) { TestDemo a = list.get(i); synchronized (a){ System.out.println(Thread.currentThread().getId()+"加鎖 >"); } try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (i==9){//這裡剛好是第19個上鎖的(同樣是第19個偏向鎖升級的) System.out.println(); System.out.println("加鎖後 get(9) 應該是無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i==10){//這裡剛好是第21個上鎖的 System.out.println(); System.out.println("加鎖後 get(10) 應該是偏向鎖 偏向t2 "+ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i==50){//50開始升級為輕量級鎖(同樣是第21個偏向鎖升級的) System.out.println(); System.out.println("加鎖後 get(50) 無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i==59){//60(同樣是第39個偏向鎖升級的) System.out.println(); System.out.println("加鎖後 get(59) 無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(i)).toPrintable()); } if (i==69){//69(同樣是第59個偏向鎖升級的) System.out.println(); System.out.println("加鎖後 get(69) 無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(i)).toPrintable()); TestDemo a1 = new TestDemo(); synchronized (a1){ System.out.println("偏向撤銷發生後的該類新建的物件都不會再偏向任何執行緒 "+ClassLayout.parseInstance(a1).toPrintable()); } } } }); Thread t3 = new Thread(()->{ for (int i = 99; i >= 0; i--) { TestDemo a = list.get(i); synchronized (a){ System.out.println(Thread.currentThread().getId()+"加鎖 >"); } try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } /** * 重點:重偏向撤銷 */ if (i==40){//40升級為輕量級鎖(同樣是第40個偏向鎖升級的,這時候發生偏向撤銷) System.out.println(); System.out.println("加鎖後 get("+i+") 應該是無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(0)).toPrintable()); TestDemo a1 = new TestDemo(); synchronized (a1){ System.out.println("偏向撤銷發生後的該類新建的物件都不會再偏向任何執行緒 "+ClassLayout.parseInstance(a1).toPrintable()); } } if (i==30){//39升級為輕量級鎖(同樣是第42個偏向鎖升級的) System.out.println(); System.out.println("加鎖後 get("+i+") 應該是無鎖(輕量級鎖釋放) "+ClassLayout.parseInstance(list.get(0)).toPrintable()); TestDemo a1 = new TestDemo(); synchronized (a1){ System.out.println("偏向撤銷發生後的該類新建的物件都不會再偏向任何執行緒 "+ClassLayout.parseInstance(a1).toPrintable()); } } } }); t2.start(); TimeUnit.MILLISECONDS.sleep(50); t3.start(); } }
public class TestDemo { } public class DemoExample8 { public static void main(String[] args) throws Exception { List<TestDemo> list = new ArrayList<>(); List<TestDemo> list2 = new ArrayList<>(); List<TestDemo> list3 = new ArrayList<>(); for (int i = 0; i < 100; i++) { list.add(new TestDemo()); list2.add(new TestDemo()); list3.add(new TestDemo()); } //偏向鎖 System.out.println("初始狀態" + 10 + ClassLayout.parseClass(TestDemo.class).toPrintable()); Thread t1 = new Thread() { String name = "1"; public void run() { System.out.printf(name); for (TestDemo a : list) { synchronized (a) { if (a == list.get(10)) { //偏向鎖 System.out.println("t1 預期是偏向鎖" + 10 + ClassLayout.parseInstance(a).toPrintable()); } } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); Thread.sleep(5000); //偏向鎖 System.out.println("main 預期是偏向鎖" + 10 + ClassLayout.parseInstance(list.get(10)).toPrintable()); Thread t2 = new Thread() { String name = "2"; public void run() { System.out.printf(name); for (int i = 0; i < 100; i++) { TestDemo a = list.get(i); synchronized (a) { if (a == list.get(10)) { System.out.println("t2 i=10 get(1)預期是無鎖" + ClassLayout.parseInstance(list.get(1)).toPrintable());//偏向鎖 System.out.println("t2 i=10 get(10) 預期輕量級鎖 " + i + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 } if (a == list.get(19)) { System.out.println("t2 i=19 get(10)預期是無鎖" + 10 + ClassLayout.parseInstance(list.get(10)).toPrintable());//偏向鎖 System.out.println("t2 i=19 get(19) 滿足重偏向條件20 預期偏向鎖 " + i + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 System.out.println("類的物件累計撤銷達到20"); } } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t2.start(); Thread.sleep(5000); Thread t3 = new Thread() { String name = "3"; public void run() { System.out.printf(name); for (TestDemo a : list2) { synchronized (a) { if (a == list2.get(10)) { System.out.println("t3 預期是偏向鎖" + 10 + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 } } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t3.start(); Thread.sleep(5000); Thread t4 = new Thread() { String name = "4"; public void run() { System.out.printf(name); for (int i = 0; i < 100; i++) { TestDemo a = list2.get(i); synchronized (a) { if (a == list2.get(10)) { System.out.println("t4 i=10 get(1)預期是無鎖" + ClassLayout.parseInstance(list2.get(1)).toPrintable());//偏向鎖 System.out.println("t4 i=10 get(10) 當前不滿足重偏向條件 20 預期輕量級鎖 " + i + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 } if (a == list2.get(19)) { System.out.println("t4 i=19 get(10)預期是無鎖" + 10 + ClassLayout.parseInstance(list2.get(10)).toPrintable());//偏向鎖 System.out.println("t4 i=19 get(19) 當前滿足重偏向條件 20 但A類的物件累計撤銷達到40 預期輕量級鎖 " + i + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 System.out.println("類的物件累計撤銷達到40"); } if (a == list2.get(20)) { System.out.println("t4 i=20 get(20) 當前滿足重偏向條件 20 預期輕量級鎖 " + i + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 } } } } }; t4.start(); Thread.sleep(5000); System.out.println("main 預期是偏向鎖" + 10 + ClassLayout.parseInstance(list3.get(0)).toPrintable());//偏向鎖 Thread t5 = new Thread() { String name = "5"; public void run() { System.out.printf(name); for (TestDemo a : list3) { synchronized (a) { if (a == list3.get(10)) { System.out.println("t5 預期是輕量級鎖,類的物件累計撤銷達到40 不可以用偏向鎖了" + 10 + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 } } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t5.start(); Thread.sleep(5000); System.out.println("main 預期是偏向鎖" + 10 + ClassLayout.parseInstance(list.get(10)).toPrintable());//偏向鎖 Thread t6 = new Thread() { String name = "6"; public void run() { System.out.printf(name); for (int i = 0; i < 100; i++) { TestDemo a = list3.get(i); synchronized (a) { if (a == list3.get(10)) { System.out.println("t6 i=10 get(1)預期是無鎖" + ClassLayout.parseInstance(list3.get(1)).toPrintable());//偏向鎖 System.out.println("t6 i=10 get(10) 預期輕量級鎖 " + i + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 } if (a == list3.get(19)) { System.out.println("t6 i=19 get(10)預期是無鎖" + 10 + ClassLayout.parseInstance(list3.get(10)).toPrintable());//偏向鎖 System.out.println("t6 i=19 get(19) 滿足重偏向條件20 但類的物件累計撤銷達到40 不可以用偏向鎖了 " + i + ClassLayout.parseInstance(a).toPrintable());//偏向鎖 } } } try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t6.start(); Thread.sleep(5000); System.out.println("由於撤銷鎖次數達到預設的 BiasedLockingBulkRevokeThreshold=40 這裡例項化的物件 是無鎖狀態" + ClassLayout.parseInstance(new TestDemo()).toPrintable());//偏向鎖 System.out.println("撤銷偏向後狀態" + 10 + ClassLayout.parseInstance(new TestDemo()).toPrintable());//偏向鎖 } }
撤銷偏向後狀態10com.boke.TestDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) bf c3 00 f8 (10111111 11000011 00000000 11111000) (-134167617)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 當鎖是偏向鎖的時候,被另外的執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高效能。
- 在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,然後拷貝物件頭中的Mark Word複製到鎖記錄中。
- 拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,並將Lock Record裡的owner指標指向物件的Mark Word。
- 如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位設定為“00”,表示此物件處於輕量級鎖定狀態。
- 如果輕量級鎖的更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明多個執行緒競爭鎖。
- 若當前只有一個等待執行緒,則該執行緒通過自旋進行等待。但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。
- 多個執行緒在不同的時間段請求同一把鎖,也就是說沒有鎖競爭。針對這種情形,Java 虛擬機器採用了輕量級鎖,來避免重量級鎖的阻塞以及喚醒
- 在沒有鎖競爭的前提下,減少傳統鎖使用OS互斥量產生的效能損耗
- 在競爭激烈時,輕量級鎖會多做很多額外操作,導致效能下降
- 可以認為兩個執行緒交替執行的情況下請求同一把鎖
public class TestDemo { } public class DemoExample9 { public static void main(String[] args) throws Exception { TestDemo testDemo = new TestDemo(); //子執行緒 Thread t1 = new Thread(){ @Override public void run() { synchronized (testDemo){ System.out.println("t1 lock ing"); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }; t1.join(); //主執行緒 synchronized (testDemo){ System.out.println("main lock ing"); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }
- 多個執行緒競爭同一個鎖的時候,虛擬機器會阻塞加鎖失敗的執行緒,並且在目標鎖被釋放的時候,喚醒這些執行緒;
- Java 執行緒的阻塞以及喚醒,都是依靠作業系統來完成的:os pthread_mutex_lock() ;
- 升級為重量級鎖時,鎖標誌的狀態值變為“10”,此時Mark Word中儲存的是指向重量級鎖的指標,此時等待鎖的執行緒都會進入阻塞狀態
分析一個由輕量級鎖膨脹成重量級鎖的案例:
public class TestDemo { } public class DemoExample9 { public static void main(String[] args) throws Exception { TestDemo testDemo = new TestDemo(); Thread t1 = new Thread(){ @Override public void run() { synchronized (testDemo){ System.out.println("t1 lock ing"); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }; t1.start(); synchronized (testDemo){ System.out.println("main lock ing"); System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); } } }
執行結果:
-
當進行加鎖操作時,Java 虛擬機器會判斷是否已經是重量級鎖。如果不是,它會在當前執行緒的當前棧楨中劃出一塊空間,作為該鎖的鎖記錄,並且將鎖物件的標記欄位複製到該鎖記錄中。
-
然後,Java 虛擬機器會嘗試用 CAS(compare-and-swap)操作替換鎖物件的標記欄位。這裡解釋一下,CAS 是一個原子操作,它會比較目標地址的值是否和期望值相等,如果相等,則替換為一個新的值。
-
假設當前鎖物件的標記欄位為 X…XYZ,Java 虛擬機器會比較該欄位是否為 X…X01。如果是,則替換為剛才分配的鎖記錄的地址。由於記憶體對齊的緣故,它的最後兩位為 00。此時,該執行緒已成功獲得這把鎖,可以繼續執行了。
-
如果不是 X…X01,那麼有兩種可能。第一,該執行緒重複獲取同一把鎖。此時,Java 虛擬機器會將鎖記錄清零,以代表該鎖被重複獲取。第二,其他執行緒持有該鎖。此時,Java 虛擬機器會將這把鎖膨脹為重量級鎖,並且阻塞當前執行緒。
-
當進行解鎖操作時,如果當前鎖記錄(你可以將一個執行緒的所有鎖記錄想象成一個棧結構,每次加鎖壓入一條鎖記錄,解鎖彈出一條鎖記錄,當前鎖記錄指的便是棧頂的鎖記錄)的值為 0,則代表重複進入同一把鎖,直接返回即可。
-
否則,Java 虛擬機器會嘗試用 CAS 操作,比較鎖物件的標記欄位的值是否為當前鎖記錄的地址。如果是,則替換為鎖記錄中的值,也就是鎖物件原本的標記欄位。此時,該執行緒已經成
-
功釋放這把鎖。
- 如果不是,則意味著這把鎖已經被膨脹為重量級鎖。此時,Java 虛擬機器會進入重量級鎖的釋放過程,喚醒因競爭該鎖而被阻塞了的執行緒
-
偏向鎖只會在第一次請求時採用 CAS 操作,在鎖物件的標記欄位中記錄下當前執行緒的地址。在之後的執行過程中,持有該偏向鎖的執行緒的加鎖操作將直接返回。它針對的是鎖僅會被同一執行緒持有的情況。
-
輕量級鎖採用 CAS 操作,將鎖物件的標記欄位替換為一個指標,指向當前執行緒棧上的一塊空間,儲存著鎖物件原本的標記欄位。它針對的是多個執行緒在不同時間段申請同一把鎖的情況。
-
重量級鎖會阻塞、喚醒請求加鎖的執行緒。它針對的是多個執行緒同時競爭同一把鎖的情況。Java 虛擬機器採取了自適應自旋,來避免執行緒在面對非常小的 synchronized 程式碼塊時,仍會被阻塞、喚醒的情況。