思考問題
- 首先請您思考下面的問題:
- Synchronized鎖同步機(jī)制性能不好嘛?
- 一個(gè)對象天生對應(yīng)一個(gè)monitor鎖嗎褪测?
- 為什么說synchronized是非公平鎖滞磺?
synchronized字節(jié)碼
- 使用java反編譯,javap -c -p -v class文件
- 使用jclasslib插件,更加方便快捷
public synchronized int getAge(){ return 18 ; } //synchronized使用在實(shí)例方法上標(biāo)記為ACC_SYNCHRONIZED,如果是類方法ACC_STATIC public synchronized int getAge(); descriptor: ()I flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=1, locals=1, args_size=1 0: bipush 18 2: ireturn public String setName(){ synchronized (this){ return "shiq"; } } public java.lang.String setName(); Code: 0: aload_0 //一個(gè)局部變量下標(biāo)0加載到操作棧即將this加載到操作數(shù)棧 1: dup //復(fù)制 2: astore_1 //數(shù)值從操作數(shù)棧存儲到局部變量表 3: monitorenter //對操作數(shù)棧頂元素加鎖即this 4: ldc #2 // String shiq 6: aload_1 7: monitorexit 8: areturn 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow Exception table: //方法異常表虐沥,保證能夠釋放鎖 from to target type 4 8 9 any 9 12 9 any //如果將上方替換成反編譯后 synchronized (SynchronizedTest.class) 0: ldc #2 // class SynchronizedTest 2: dup 3: astore_1 4: monitorenter //0行表示將一個(gè)常量池中位置為2的常量加載到操作數(shù)棧,查詢常量池 #2 = Class #24 // SynchronizedTest
- 以上我們可以看到synchronized修飾this表示對象本身加鎖,修飾class表示對類加鎖
對象內(nèi)存構(gòu)成
- 一個(gè)對象的內(nèi)存構(gòu)成有對象頭欲险,實(shí)例數(shù)據(jù)镐依,對齊填充
oop層級
- oopDesc是對象類的頂層基類,每個(gè)Java Object 在 JVM 內(nèi)部都有一個(gè) native 的 C++ 對象 oop/oopDesc 與之對應(yīng)天试。源碼位置 openjdk\hotspot\src\share\vm\oops\oop.hpp
- 每個(gè)對象頭由兩部分組成槐壳,klass pointer和Mark Word和Array length(只有是數(shù)組才會有)源碼在vm/oops/oop.hpp中定義的OopDes
- _mark表示對象標(biāo)記、屬于markOop類型喜每,也就是接下來要講解的Mark World务唐,它記錄了對象和鎖有關(guān)的信息
- Klass Pointer對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定對象是哪個(gè)類的實(shí)例带兜。即下方的_metadata表示類元信息枫笛,類元信息存儲的是對象指向它的類元數(shù)據(jù)(Klass)的首地址,其中Klass表示普通指針刚照、 _compressed_klass表示壓縮類指針
- 實(shí)例數(shù)據(jù):按對象頭為基址做相對偏移后操作 obj_field_addr(offset偏移量)
- 對齊填充 :java默認(rèn)的8字節(jié)對齊規(guī)則刑巧,如果占位不足要補(bǔ)齊
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark; ////markOop:Mark Word標(biāo)記字段
union _metadata {
Klass* _klass; ////對象類型元數(shù)據(jù)的指針
narrowKlass _compressed_klass;
} _metadata;
public:
markOop mark() const { return _mark; } //返回Mark Word數(shù)值
markOop* mark_addr() const { return (markOop*) &_mark; } //返回一個(gè)地址值為_mark
public:
// Need this as public for garbage collection.
template <class T> T* obj_field_addr(int offset) const;
}
oopDes結(jié)構(gòu)圖:
對象頭中Mark World
- markOopDesc 位于openjdk\hotspot\src\share\vm\oops\markOop.hpp 繼承oopDesc
//mark Word 在markOopDesc中有注釋
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
/**
通過以上可知是個(gè)32bit數(shù)值,不太可能是指針
*/
//觀察其中的方法 无畔,是否是重量級鎖monitor_value = 2 => 鎖標(biāo)記為 10 啊楚, 如果value()為重量級鎖標(biāo)記則***10 & 10 != 0成立
//因此value()函數(shù)代表的即為當(dāng)前obj的mark Word數(shù)值
bool has_monitor() const {
return ((value() & monitor_value) != 0);
}
private:
// Conversion
uintptr_t value() const { return (uintptr_t) this; }
//是否設(shè)置了偏向標(biāo)志 biased_lock_mask_in_place = 3
bool has_bias_pattern() const {
return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern);
}
-
mark word 圖解
- 以上分析也可以很容易理解了markOopDesc中函數(shù)monitor: 返回一個(gè)ObjectMonitor指針對象 這個(gè)ObjectMonitor 其實(shí)就是對象監(jiān)視器
ObjectMonitor* monitor() const {
assert(has_monitor(), "check"); //斷定為重量級鎖
// Use xor instead of &~ to provide one extra tag-bit check.
return (ObjectMonitor*) (value() ^ monitor_value); //將Mark Word值轉(zhuǎn)化成指針,指向的就是obj的ObjectMonitor鎖對象
}
- 用于返回一個(gè)ObjectMonitor鎖對象浑彰,方法在vm/runtime/synchronizer.cpp中
鎖入口
- 無論是synchronized代碼塊和synchronized方法恭理,其底層獲取鎖的邏輯都是一樣的。我們以代碼塊的monitorentor和monitorexit為例
- monitorenter指令在HotSpot的中有兩處地方對monitorenter指令進(jìn)行解析:一個(gè)是在bytecodeInterpreter.cpp 郭变,另一個(gè)是在templateTable_x86_64.cpp
- 前者是JVM字節(jié)碼解釋器,有C++書寫颜价,優(yōu)點(diǎn)是實(shí)現(xiàn)相對簡單且容易理解,缺點(diǎn)是執(zhí)行慢诉濒;
- 后者是模板解釋器:對每個(gè)指令都寫了一段對應(yīng)的匯編代碼拍嵌,啟動時(shí)將每個(gè)指令與對應(yīng)匯編代碼入口綁定,效率極高但理解不易循诉;
偏向鎖
- 首先理解兩個(gè)類BasicObjectLock,BasicLock BasicObjectLock將特定的Java對象與BasicLock關(guān)聯(lián)撇他。
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
BasicLock _lock; //BasicLock對象
oop _obj; //與上方_lock關(guān)聯(lián)的java對象
public:
oop obj() const { return _obj; }
void set_obj(oop obj) { _obj = obj; }
BasicLock* lock() { return &_lock; }
class BasicLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
volatile markOop _displaced_header; //mark word字段
public:
markOop displaced_header() const { return _displaced_header; }
void set_displaced_header(markOop header) { _displaced_header = header; }
void print_on(outputStream* st) const;
// move a basic lock (used during deoptimization
void move_to(oop obj, BasicLock* dest);
static int displaced_header_offset_in_bytes(){ return offset_of(BasicLock, _displaced_header); }
};
- BasicLock中的markOop其實(shí)就是偏向鎖中保存當(dāng)前mark word字段使用的
偏向鎖加鎖
- 偏向鎖入口函數(shù)_monitorenter字節(jié)碼
CASE(_monitorenter): {
//當(dāng)前鎖對象
oop lockee = STACK_OBJECT(-1);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break;
most_recent++;
}
if (entry != NULL) { //找到entry即可用的最高位的Lock Record
entry->set_obj(lockee); //Lock Record的obj指向鎖對象
int success = false; //標(biāo)記位: 用于確定是否需要升級鎖為輕量級鎖
uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
markOop mark = lockee->mark();
intptr_t hash = (intptr_t) markOopDesc::no_hash;
/**
1. 可以使用偏向鎖
*/
if (mark->has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread();
anticipated_bias_locking_value =
(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);
/**
* 1.2 : 當(dāng)前偏向線程是自己茄猫,什么都不做,設(shè)置success = true困肩,防止進(jìn)入下方鎖升級
*/
if (anticipated_bias_locking_value == 0) {
// already biased towards this thread, nothing to do
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
}
//批量重偏向 klass()->prototype_header不支持偏向鎖
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
// try revoke bias
markOop header = lockee->klass()->prototype_header();
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
}
/**
* 1.3 當(dāng)前偏向鎖的epoch失效划纽,則重新偏向
*/
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// 1.3.1 構(gòu)造一個(gè)偏向當(dāng)前線程切epoch為class最新的mark word
markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
if (hash != markOopDesc::no_hash) {
new_header = new_header->copy_set_hash(hash);
}
// 1.3.2 CAS 替換 mark word
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
}
else {
/**
* /替換ThreadID失敗,有兩種可能:
1. 撤銷原有偏向ID锌畸,重新設(shè)置
2. 有競爭勇劣,升級鎖,當(dāng)下方success=true 所以在monitorenter中升級的
*/
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
else {
/**
* 1.1 當(dāng)前無鎖或 1.3 當(dāng)前偏向其他線程
*/
markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
(uintptr_t)markOopDesc::age_mask_in_place |
epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
/**
* 1. 構(gòu)造一個(gè)偏向當(dāng)前線程的mark Word
2. 設(shè)置首位的Lock Record lock即BasicLock為線程ID默認(rèn) 0xdeaddead
3. 使用CAS替換操作
*/
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
// debugging hint
DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
/**
* 替換ThreadID失敗,有兩種可能:
1. 撤銷原有偏向ID比默,重新設(shè)置
2. 有競爭幻捏,升級鎖,當(dāng)下方success=true 所以在monitorenter中升級的
*/
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
}
-
偏向鎖示意圖:
-
整個(gè)過程示意圖:
偏向鎖撤銷
- 在獲取偏向鎖的過程因?yàn)椴粷M足條件導(dǎo)致要將鎖對象改為非偏向鎖狀態(tài)
- 調(diào)取過程在 InterpreterRuntime::monitorenter -> 開啟JVM偏向鎖 ObjectSynchronizer::fast_enter -> java線程 BiasedLocking::revoke_and_rebias
static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread, JavaThread** biased_locker) {
markOop mark = obj->mark();
//如果不是偏向模式命咐,返回
if (!mark->has_bias_pattern()) {
return BiasedLocking::NOT_BIASED;
}
//創(chuàng)建兩個(gè)mark word篡九,一個(gè)是匿名偏向模式(101),一個(gè)是無鎖模式(001)
uint age = mark->age();
markOop biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
...
JavaThread* biased_thread = mark->biased_locker();
if (biased_thread == NULL) {
// 匿名偏向醋奠。當(dāng)調(diào)用鎖對象的hashcode()方法可能會導(dǎo)致走到這個(gè)邏輯
// 如果不允許重偏向榛臼,則將對象的mark word設(shè)置為無鎖模式
if (!allow_rebias) {
obj->set_mark(unbiased_prototype);
}
return BiasedLocking::BIAS_REVOKED;
}
//0. 判斷偏向線程是否還存活
bool thread_is_alive = false;
//0.1 如果當(dāng)前線程就是偏向線程
if (requesting_thread == biased_thread) {
thread_is_alive = true;
} else {
// 0.2 遍歷當(dāng)前jvm的所有線程,如果能找到窜司,則說明偏向的線程還存活
for (JavaThread* cur_thread = Threads::first(); cur_thread != NULL; cur_thread = cur_thread->next()) {
if (cur_thread == biased_thread) {
thread_is_alive = true; //偏向線程還在
break;
}
}
}
/**
* 1. 如果偏向線程不存在了
*/
if (!thread_is_alive) {
// 1.1 允許重偏向則將對象mark word設(shè)置為匿名偏向狀態(tài)101沛善,否則設(shè)置為無鎖狀態(tài)001
if (allow_rebias) {
obj->set_mark(biased_prototype);
} else {
obj->set_mark(unbiased_prototype);
}
return BiasedLocking::BIAS_REVOKED;
}
/**
* 2. 如果偏向線程存在,則遍歷該線程棧中所有的Lock Record
*/
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread); //獲取線程中的棧 All Lock Record
BasicLock* highest_lock = NULL;
for (int i = 0; i < cached_monitor_info->length(); i++) { //從上向下遍歷
MonitorInfo* mon_info = cached_monitor_info->at(i);
/**
* 2.1 判斷當(dāng)前Lock Record 的 owner是否指向鎖對象塞祈,如果指向說明當(dāng)前線程在偏向金刁,這個(gè)時(shí)候需要升級為輕量級鎖啦
*/
if (TraceBiasedLocking && Verbose) {
tty->print_cr(" mon_info->owner (" PTR_FORMAT ") == obj (" PTR_FORMAT ")",
p2i((void *) mon_info->owner()),
p2i((void *) obj));
}
/** 2.2 低位lock 直接修改偏向線程棧中的Lock Record。為了處理鎖重入的case织咧,在這里將Lock Record的Displaced Mark Word設(shè)置為null胀葱,
* 第一個(gè)Lock Record會在下面的代碼中再處理
*/
markOop mark = markOopDesc::encode((BasicLock*) NULL);
highest_lock = mon_info->lock(); //
highest_lock->set_displaced_header(mark);
} else {
...
}
}
/**
* 2.3 對高位進(jìn)行處理,此時(shí)Displaced Mark Word即lock不能在設(shè)置成null
*/
if (highest_lock != NULL) {
// 2.3.1 修改第一個(gè)Lock Record為unbiased_prototype無鎖狀態(tài)笙蒙,
highest_lock->set_displaced_header(unbiased_prototype);
//2.3.2 將鎖對象obj obj的mark word設(shè)置為指向該Lock Record的指針
obj->release_set_mark(markOopDesc::encode(highest_lock));
assert(!obj->mark()->has_bias_pattern(), "illegal mark state: stack lock used bias bit");
if (TraceBiasedLocking && (Verbose || !is_bulk)) {
tty->print_cr(" Revoked bias of currently-locked object");
}
} else {
/**
* 3. 走到這里說明偏向線程已經(jīng)不在同步塊中了
*/
if (allow_rebias) {
//3.1 設(shè)置為匿名偏向狀態(tài)
obj->set_mark(biased_prototype);
} else {
// 3.2 設(shè)置成無鎖狀態(tài)
obj->set_mark(unbiased_prototype);
}
}
- 查看偏向的線程是否存活抵屿,如果已經(jīng)不存活了,則直接撤銷偏向鎖捅位。JVM維護(hù)了一個(gè)集合存放所有存活的線程轧葛,通過遍歷該集合判斷某個(gè)線程是否存活。
- 偏向的線程是否還在同步塊中艇搀,如果不在了尿扯,則撤銷偏向鎖。我們回顧一下偏向鎖的加鎖流程:每次進(jìn)入同步塊(即執(zhí)行monitorenter)的時(shí)候都會以從高往低的順序在棧中找到第一個(gè)可用的Lock Record焰雕,將其obj字段指向鎖對象衷笋。每次解鎖(即執(zhí)行monitorexit)的時(shí)候都會將最低的一個(gè)相關(guān)Lock Record移除掉。所以可以通過遍歷線程棧中的Lock Record來判斷線程是否還在同步塊中矩屁。
- 將偏向線程所有相關(guān)Lock Record的Displaced Mark Word設(shè)置為null辟宗,然后將最高位的Lock Record的Displaced Mark Word 設(shè)置為無鎖狀態(tài),最高位的Lock Record也就是第一次獲得鎖時(shí)的Lock Record(這里的第一次是指重入獲取鎖時(shí)的第一次)吝秕,然后將對象頭指向最高位的Lock Record泊脐,這里不需要用CAS指令,因?yàn)槭窃趕afepoint烁峭。 執(zhí)行完后容客,就升級成了輕量級鎖秕铛。原偏向線程的所有Lock Record都已經(jīng)變成輕量級鎖的狀態(tài)。
偏向鎖的釋放
- 釋放過程相對比較簡單
CASE(_monitorexit): {
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
// derefing's lockee ought to provoke implicit null check
// find our monitor slot
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
// 從低往高遍歷棧的Lock Record
while (most_recent != limit ) {
// 如果Lock Record關(guān)聯(lián)的是該鎖對象
if ((most_recent)->obj() == lockee) {
BasicLock* lock = most_recent->lock();
markOop header = lock->displaced_header();
// 釋放Lock Record
most_recent->set_obj(NULL);
// 如果是偏向模式缩挑,僅僅釋放Lock Record就好了但两。否則要走輕量級鎖or重量級鎖的釋放流程
if (!lockee->mark()->has_bias_pattern()) {
bool call_vm = UseHeavyMonitors;
// header!=NULL說明不是重入,則需要將Displaced Mark Word CAS到對象頭的Mark Word
if (header != NULL || call_vm) {
if (call_vm || Atomic::cmpxchg_ptr(header, lockee->mark_addr(), lock) != lock) {
// CAS失敗或者是重量級鎖則會走到這里调煎,先將obj還原镜遣,然后調(diào)用monitorexit方法
most_recent->set_obj(lockee);
CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
}
}
}
//執(zhí)行下一條命令
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
}
//處理下一條Lock Record
most_recent++;
}
// Need to throw illegal monitor state exception
CALL_VM(InterpreterRuntime::throw_illegal_monitor_state_exception(THREAD), handle_exception);
ShouldNotReachHere();
}
- 偏向鎖只有遇到其他線程嘗試競爭偏向鎖時(shí),持有偏向鎖的線程才會釋放鎖士袄,線程不會主動去釋放偏向鎖悲关。
- 偏向鎖的撤銷,需要等待全局安全點(diǎn)娄柳,它會首先暫停擁有偏向鎖的線程寓辱,判斷鎖對象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級鎖(標(biāo)志位為“00”)的狀態(tài)
- -XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 查看停頓時(shí)間詳細(xì)信息
- JDK1.6以后默認(rèn)開啟偏向鎖UseBiasedLocking赤拒,對于高并發(fā)提升效率-XX:-UseBiasedLocking
輕量級鎖
輕量級鎖加鎖
- 入口函數(shù)同上
CASE(_monitorenter): { oop lockee = STACK_OBJECT(-1); ... if (entry != NULL) { ... // 上面省略的代碼中如果CAS操作失敗也會調(diào)用到InterpreterRuntime::monitorenter if (!success) { // 構(gòu)建一個(gè)無鎖狀態(tài)的Displaced Mark Word markOop displaced = lockee->mark()->set_unlocked(); // 設(shè)置到Lock Record中去 entry->lock()->set_displaced_header(displaced); bool call_vm = UseHeavyMonitors; if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) { // 如果CAS替換不成功秫筏,代表鎖對象不是無鎖狀態(tài),這時(shí)候判斷下是不是鎖重入 // Is it simple recursive case? if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { entry->lock()->set_displaced_header(NULL); } else { // CAS操作失敗則調(diào)用monitorenter CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } } } UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1); } else { istate->set_msg(more_monitors); UPDATE_PC_AND_RETURN(0); // Re-execute } }
- 構(gòu)建一個(gè)無鎖的mark Work 設(shè)置到Lock Record 中l(wèi)ock中去
- CAS替換鎖對象的Mark word為指向1中的Lock Record挎挖,成功輕量級鎖完成
- 如果失敗这敬,兩種情況
- 是否是鎖重入,如果是則設(shè)置lock為null
- 否則蕉朵,調(diào)取monitorenter 自旋鎖或者升級鎖
-
過程示意圖
輕量級解鎖
- 解鎖過程由下往上查找當(dāng)前線程的Lock Record 中owner為鎖對象后判斷BasicLock
- 如果BasicLock == null ,表示一個(gè)鎖重入崔涂,當(dāng)前線程還在同步塊中,不做操作
- 如果BasicLock有值始衅,即為鎖對象指向的Display Mark Word 冷蚂,則表示這時(shí)候需要真正釋放鎖,將DisPlay Mark Word替換回鎖對象的Mark Word中去汛闸,如果失敗蝙茶,則調(diào)用InterpreterRuntime::monitorexit
- 總結(jié):輕量級鎖解鎖時(shí),把復(fù)制的對象頭替換回去(cas)如果替換成功(就是要把無所的狀態(tài)放回去給對象頭诸老,之后鎖繼續(xù)被拿還是輕量級鎖隆夯,但是如果鎖已經(jīng)是重量級鎖了,那么就失敗别伏,之后鎖就是重量級的鎖了)吮廉,鎖結(jié)束,之后別的線程來拿還是輕量級鎖畸肆,如果失敗,說明已有競爭宙址,釋放鎖轴脐,此時(shí)把對象頭設(shè)為重量級鎖,并notify 喚醒其他等待線程。
重量級鎖
重量級鎖加鎖
- 入口函數(shù)大咱,當(dāng)存在競爭時(shí)恬涧,調(diào)用ObjectSynchronizer::slow_enter() 最后一行
//需要膨脹為重量級鎖,膨脹前碴巾,設(shè)置Displaced Mark Word為一個(gè)特殊值溯捆,代表該鎖正在用一個(gè)重量級鎖的monitor
lock->set_displaced_header(markOopDesc::unused_mark());
//先調(diào)用inflate膨脹為重量級鎖,該方法返回一個(gè)ObjectMonitor對象厦瓢,然后調(diào)用其enter方法
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
- 鎖膨脹提揍,即獲取一個(gè)ObjectMonitor對象inflate()
//結(jié)構(gòu)體如下objectMonitor.hpp中定義
ObjectMonitor::ObjectMonitor() {
_header = NULL; //對象頭的Mark Word
_count = 0;
_waiters = 0,
_recursions = 0; //線程的重入次數(shù)
_object = NULL;
_owner = NULL; //標(biāo)識擁有該monitor的線程
_WaitSet = NULL; //等待線程組成的雙向循環(huán)鏈表,_WaitSet是第一個(gè)節(jié)點(diǎn)
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多線程競爭鎖進(jìn)入時(shí)的單向鏈表
FreeNext = NULL ;
_EntryList = NULL ; //_owner從該雙向循環(huán)鏈表中喚醒線程結(jié)點(diǎn)煮仇,_EntryList是第一個(gè)節(jié)點(diǎn)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
inflate膨脹
- inflate膨脹有四種情況:
- Inflated(重量級鎖狀態(tài)) - 直接返回
- INFLATING(膨脹中) - 忙等待直到膨脹完成
- Stack-locked(輕量級鎖狀態(tài)) - 膨脹
- Neutral(無鎖狀態(tài)) - 膨脹
- Inflated 重量級鎖狀態(tài): 直接返回
/**
* 1. 已經(jīng)是重量級鎖了劳跃,直接返回obj ->mark word指向的monitor對象
*/
if (mark->has_monitor()) {
ObjectMonitor * inf = mark->monitor() ;
return inf ;
}
- INFLATING 其他線程正在膨脹中,則當(dāng)前線程等待
/**
* 2. 有其他線程正在膨脹中浙垫,這個(gè)時(shí)候忙等待后continue
*/
if (mark == markOopDesc::INFLATING()) {
TEVENT (Inflate: spin while INFLATING) ;
//3.1 在該方法中會進(jìn)行spin/yield/park等操作完成自旋動作
ReadStableMark(object) ;
continue ;
}
- Stack-locked(輕量級鎖狀態(tài)升級) - 膨脹
/**
* 3. 輕量級鎖升級
*/
if (mark->has_locker()) {
// 3.1 omAlloc從當(dāng)前線程的Self->omFreeList的 ObjectMonitor數(shù)組中獲取刨仑,成功加入全局中管理,否則從全局中獲取
ObjectMonitor * m = omAlloc (Self) ;
// 初始化ObjectMonitor對象
m->Recycle();
m->_Responsible = NULL ;
m->OwnerIsThread = 0 ;
m->_recursions = 0 ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class
// 將鎖對象的mark word設(shè)置為INFLATING (0)狀態(tài)
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
if (cmp != mark) {
omRelease (Self, m, true) ;
continue ; // Interference -- just retry
}
// 棧中的displaced mark word
markOop dmw = mark->displaced_mark_helper() ;
// monitor中的header設(shè)置為Displaced mark Word
m->set_header(dmw) ;
//將原有的Lock Record設(shè)置給monitor的owner
m->set_owner(mark->locker());
//monitor的obj設(shè)置鎖對象
m->set_object(object);
//將鎖對象頭設(shè)置為重量級鎖狀態(tài)
object->release_set_mark(markOopDesc::encode(m));
...
return m ;
}
- 無鎖狀態(tài)
/**
* 4. 無鎖狀態(tài)
*/
//獲取一個(gè)ObjectMonitor對象
ObjectMonitor * m = omAlloc (Self) ;
// 初始化操作
m->Recycle();
m->set_header(mark); //設(shè)置header為mark Word
m->set_owner(NULL); //Lock Record不存在
m->set_object(object);
m->OwnerIsThread = 1 ;
m->_recursions = 0 ;
m->_Responsible = NULL ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class
//用CAS替換對象頭的mark word為重量級鎖狀態(tài)
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 ;
}
...
return m ;
}
獲取鎖->enter(THREAD)
- 在ObjectMonitor::enter()四種情況
- 無鎖直接可以獲取到
- 重入情況
- 輕量級升級情況
- 重量級鎖競爭情況
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ;
void * cur ;
/**
* 1. owner為null代表無鎖狀態(tài)夹姥,如果能CAS設(shè)置成功杉武,則當(dāng)前線程直接獲得鎖
*/
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
...
return ;
}
/**
* 2. 如果是重入的情況
*/
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
/**
* 3. 當(dāng)前線程是之前持有輕量級鎖的線程。由輕量級鎖膨脹且第一次調(diào)用enter方
* 法辙售,那cur是指向Lock Record的指針
*/
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
// 重入計(jì)數(shù)重置為1
_recursions = 1 ;
// 設(shè)置owner字段為當(dāng)前線程(之前owner是指向Lock Record的指針)
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
...
// 在調(diào)用系統(tǒng)的同步操作之前轻抱,先嘗試自旋獲得鎖
if (Knob_SpinEarly && TrySpin (Self) > 0) {
...
//自旋的過程中獲得了鎖,則直接返回
Self->_Stalled = 0 ;
return ;
}
...
{
...
/**
* 4. 重量級鎖競爭情況
*/
for (;;) {
jt->set_suspend_equivalent();
// 在該方法中調(diào)用系統(tǒng)同步操作圾亏,真正的競爭鎖
EnterI (THREAD) ;
...
}
Self->set_current_pending_monitor(NULL);
}
...
}
重量級鎖競爭
-
首先分析objectMonitor中的元素: cxq(下圖中的ContentionList)十拣,EntryList ,WaitSet志鹃,owner夭问, 前三個(gè)是由ObjectWaiter的鏈表結(jié)構(gòu), owner是當(dāng)前持有鎖的線程
- ObjectWaiter結(jié)構(gòu)
class ObjectWaiter : public StackObj {
public:
enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
enum Sorted { PREPEND, APPEND, SORTED } ;
ObjectWaiter * volatile _next;
ObjectWaiter * volatile _prev;
Thread* _thread;
jlong _notifier_tid;
ParkEvent * _event;
volatile int _notified ;
volatile TStates TState ;
Sorted _Sorted ; // List placement disposition
bool _active ; // Contention monitoring is enabled
public:
ObjectWaiter(Thread* thread); //對應(yīng)獲取鎖的線程
void wait_reenter_begin(ObjectMonitor *mon);
void wait_reenter_end(ObjectMonitor *mon);
};
- 重量級加鎖過程
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
...
// 嘗試獲得鎖
if (TryLock (Self) > 0) {
...
return ;
}
DeferredInitialize () ;
// 自旋
if (TrySpin (Self) > 0) {
...
return ;
}
...
// 將線程封裝成node節(jié)點(diǎn)ObjectWaiter中
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;
/**
* 1. 將node節(jié)點(diǎn)插入到_cxq隊(duì)列的頭部曹铃,cxq是一個(gè)單向鏈表
*/
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
// CAS失敗的話 再嘗試獲得鎖缰趋,這樣可以降低插入到_cxq隊(duì)列的頻率
if (TryLock (Self) > 0) {
...
return ;
}
}
// SyncFlags默認(rèn)為0,如果沒有其他等待的線程,則將_Responsible設(shè)置為自己
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
TEVENT (Inflated enter - Contention) ;
int nWakeups = 0 ;
int RecheckInterval = 1 ;
for (;;) {
if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;
...
// park self
if (_Responsible == Self || (SyncFlags & 1)) {
// 當(dāng)前線程是_Responsible時(shí)涂滴,調(diào)用的是帶時(shí)間參數(shù)的park
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
//否則直接調(diào)用park掛起當(dāng)前線程
TEVENT (Inflated enter - park UNTIMED) ;
Self->_ParkEvent->park() ;
}
if (TryLock(Self) > 0) break ;
...
if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;
...
/**
* 2. 在當(dāng)前線程釋放鎖時(shí)奶卓,_succ會被設(shè)置為EntryList或_cxq中的一個(gè)線程:表示假定繼承人
* _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ; 在ExitEpilog中設(shè)置
* ExitEpilog(w) 調(diào)用w = _cxq 或者 w = _EntryList
*/
if (_succ == Self) _succ = NULL ;
// Invariant: after clearing _succ a thread *must* retry _owner before parking.
OrderAccess::fence() ;
}
// 走到這里說明已經(jīng)獲得鎖了
assert (_owner == Self , "invariant") ;
assert (object() != NULL , "invariant") ;
// 將當(dāng)前線程的node從cxq或EntryList中移除
UnlinkAfterAcquire (Self, &node) ;
if (_succ == Self) _succ = NULL ;
if (_Responsible == Self) {
_Responsible = NULL ;
OrderAccess::fence();
}
...
return ;
}
- 當(dāng)一個(gè)線程嘗試獲得鎖時(shí),如果該鎖已經(jīng)被占用灰粮,則會將該線程封裝成一個(gè)ObjectWaiter對象插入到cxq的隊(duì)列的隊(duì)首,然后調(diào)用park函數(shù)掛起當(dāng)前線程忍坷。
- 當(dāng)線程釋放鎖時(shí)粘舟,會從cxq或EntryList中挑選一個(gè)線程喚醒熔脂,被選中的線程叫做Heir presumptive即假定繼承人(Ready Thread),假定繼承人被喚醒后會嘗試獲得鎖柑肴,但synchronized是非公平的霞揉,所以假定繼承人不一定能獲得鎖;
- 如果線程獲得鎖后調(diào)用Object#wait方法,則會將線程加入到WaitSet中
- 當(dāng)被Object#notify喚醒后晰骑,會將線程從WaitSet移動到cxq或EntryList中去适秩。
- 調(diào)用一個(gè)鎖對象的wait或notify方法時(shí),如當(dāng)前鎖的狀態(tài)是偏向鎖或輕量級鎖則會先膨脹成重量級鎖硕舆。
重量級鎖解鎖
- 解鎖包括: 1. 當(dāng)前是可重入鎖 2. 正常釋放鎖
- 可重入鎖
// 重入計(jì)數(shù)器還不為0秽荞,則計(jì)數(shù)器-1后返回
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
- 正常釋放鎖,可能需要喚醒其他線程
if (Knob_ExitPolicy == 0) { //Knob_ExitPolicy默認(rèn)為0
/**
* 1. synchronized非公平鎖原因: 當(dāng)前線程先釋放鎖岗宣,這時(shí)如果有其他線程進(jìn)入同步塊則能獲得鎖蚂会,不管剛剛上面的
假定繼承人及_succ是否存在,因此它是非公平鎖
*/
OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock
//內(nèi)存屏障耗式,是否需要喚醒繼任者
OrderAccess::storeload() ; //See if we need to wake a successor
/**
* 2. 如果沒有等待線程_EntryList和_cxq均為空胁住,或者_(dá)succ假定繼承人存在,這個(gè)時(shí)候是沒有競爭的刊咳,可以運(yùn)行彪见,不需要喚醒,直接返回就好了
*/
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT (Inflated exit - simple egress) ;
return ;
}
TEVENT (Inflated exit - complex egress) ;
/**
* 3. 走到這里表示娱挨,當(dāng)前_EntryList 余指,_cxq不為空,且沒有假定繼承人跷坝,需要喚醒操作了
* 而要執(zhí)行之后的操作就需要重新獲得鎖酵镜,即設(shè)置_owner為當(dāng)前線程
*/
if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
return ;
}
...
/**
* 4. 根據(jù)不同的QMode執(zhí)行不同的喚醒策略,默認(rèn)為0
*/
ObjectWaiter * w = NULL ;
// code 4:根據(jù)QMode的不同會有不同的喚醒策略柴钻,默認(rèn)為0
int QMode = Knob_QMode ;
if (QMode == 2 && _cxq != NULL) {
/**
* QMode == 2 : cxq中的線程有更高優(yōu)先級淮韭,直接喚醒cxq的隊(duì)首線程,返回了贴届,最終喚醒的就是這個(gè)隊(duì)首線程_succ == w
*/
w = _cxq ;
assert (w != NULL, "invariant") ;
assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
ExitEpilog (Self, w) ;
return ;
}
f (QMode == 3 && _cxq != NULL) {
/**
* QMode == 3: 將cxq中的元素插入到EntryList的末尾,繼續(xù)向下執(zhí)行程序
*/
...
}
if (QMode == 4 && _cxq != NULL) {
/**
* QMode == 4: 將cxq插入到EntryList的隊(duì)首
*/
...
}
/**
* 默認(rèn)QMode == 0 : 什么也不做
*/
/**
* 3. 如果_EntryList不為null ,取隊(duì)首w 靠粪,喚醒ObjectWaiter對象的線程,然后立即返回
*/
w = _EntryList ;
if (w != NULL) {
assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}
/**
* 4. 如果EntryList的首元素為空毫蚓,就將cxq的所有元素放入到EntryList中
* 然后再從EntryList中取出來隊(duì)首元素執(zhí)行ExitEpilog方法占键,然后立即返回
*
* 使用w保存_cxq隊(duì)列后,將_cxq設(shè)置null
*/
w = _cxq ;
if (w == NULL) continue ;
// Drain _cxq into EntryList - bulk transfer.
// First, detach _cxq.
// The following loop is tantamount to: w = swap (&cxq, NULL)
for (;;) {
assert (w != NULL, "Invariant") ;
//下方用到w替代_cxq
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
if (QMode == 1) {
// QMode == 1 : 將cxq中的元素轉(zhuǎn)移到EntryList元潘,并反轉(zhuǎn)順序
...
} else {
// QMode == 0 or QMode == 2‘
// 將cxq中的元素轉(zhuǎn)移到EntryList
_EntryList = w ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
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 ;
}
//如果當(dāng)前有個(gè)新增的假定繼承人畔乙,所以不需要當(dāng)前線程去喚醒,以減少上下文切換的比率
if (_succ != NULL) continue;
/**
* 4.1 將上方有cxq轉(zhuǎn)移到_EntryList首位喚醒即可
*/
w = _EntryList ;
if (w != NULL) {
guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}
//ExitEpilog用于喚醒線程操作
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
assert (_owner == Self, "invariant") ;
/**
* 假定繼承人的設(shè)置翩概,查看調(diào)用方法在解鎖exit中設(shè)置w = _EntryList
* 只有QMode == 2 時(shí) w = _cxq 中取
*/
_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
ParkEvent * Trigger = Wakee->_event ;
Wakee = NULL ;
//如果有其他線程進(jìn)來啸澡,將會直接運(yùn)行袖订,不會喚醒操作了
OrderAccess::release_store_ptr (&_owner, NULL) ;
OrderAccess::fence() ;
if (SafepointSynchronize::do_call_back()) {
TEVENT (unpark before SAFEPOINT) ;
}
DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
Trigger->unpark() ; //喚醒線程
// Maintain stats and report events to JVMTI
if (ObjectMonitor::_sync_Parks != NULL) {
ObjectMonitor::_sync_Parks->inc() ;
}
}
- 釋放鎖,這個(gè)時(shí)刻其他的線程能獲取到鎖,synchronized是非公平鎖的原因
- 如果當(dāng)前沒有等待的線程或succ 嗅虏!= null 有一個(gè)假定繼承人存在(可以運(yùn)行它) ,則直接返回就好了上沐,因?yàn)椴恍枰獑拘哑渌€程皮服。
- 當(dāng)前線程重新獲得鎖,因?yàn)橹笠僮鱟xq和EntryList隊(duì)列以及喚醒線程
- 根據(jù)QMode的不同参咙,會執(zhí)行不同的喚醒策略: 默認(rèn)為0
- QMode = 2且cxq非空:取cxq隊(duì)列隊(duì)首的ObjectWaiter對象龄广,調(diào)用ExitEpilog方法,該方法會喚醒ObjectWaiter對象的線程蕴侧,然后立即返回择同,后面的代碼不會執(zhí)行了;
- QMode = 3且cxq非空:把cxq隊(duì)列插入到EntryList的尾部净宵;
- QMode = 4且cxq非空:把cxq隊(duì)列插入到EntryList的頭部敲才;
- QMode = 0:默認(rèn),暫時(shí)什么都不做择葡,繼續(xù)往下看紧武;
- 如果EntryList的首元素非空,就取出來調(diào)用ExitEpilog方法敏储,該方法會喚醒ObjectWaiter對象的線程阻星,然后立即返回;
- 如果EntryList的首元素為空已添,就將cxq的所有元素放入到EntryList中妥箕,然后再從EntryList中取出來隊(duì)首元素執(zhí)行ExitEpilog方法,然后立即返回更舞;
wait ,notify喚醒操作
- wait將當(dāng)前Thread構(gòu)造ObjectWaiter后通過AddWaiter加入_waitSet隊(duì)列
inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
// 從waitSet頭部添加
if (_WaitSet == NULL) {
_WaitSet = node;
node->_prev = node;
node->_next = node;
} else {
ObjectWaiter* head = _WaitSet ;
ObjectWaiter* tail = head->_prev;
assert(tail->_next == head, "invariant check");
tail->_next = node;
head->_prev = node;
node->_next = head;
node->_prev = tail;
}
}
- notify喚醒wait
//獲取等待隊(duì)列中一個(gè)ObjectWaiter
ObjectWaiter * iterator = DequeueWaiter() ; //也是從頭部取值呀
inline ObjectWaiter* ObjectMonitor::DequeueWaiter() {
// dequeue the very first waiter
ObjectWaiter* waiter = _WaitSet;
if (waiter) {
DequeueSpecificWaiter(waiter);
}
return waiter;
}
...
int Policy = Knob_MoveNotifyee ; //默認(rèn)為2
ObjectWaiter * List = _EntryList ;
if (Policy == 2) { // prepend to cxq
// 如果_EntryList為null,將喚醒的加入_EntryList很大幾率運(yùn)行
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else { //否則加入_cxq隊(duì)首畦幢,等待_EntryList為null后加入運(yùn)行,QMode =默認(rèn)0時(shí)
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Front = _cxq ;
iterator->_next = Front ;
if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
break ;
}
}
}
}
- 加入等待隊(duì)列從隊(duì)首添加疏哗,取出也是從隊(duì)首呛讲,notify只喚醒一個(gè)添加到_cxq或者_(dá)EntryList中,notifyAll()邏輯一致返奉,只是使用for循環(huán)獲取waitSet中所有數(shù)據(jù)添加而已
思考
- 對于下方代碼贝搁,請您分析其執(zhí)行順序:
public class SyncDemo {
public static void main(String[] args) throws InterruptedException {
SyncDemo syncDemo = new SyncDemo();
syncDemo.startThreadA();
Thread.sleep(100);
syncDemo.startThreadB();
Thread.sleep(100);
syncDemo.startThreadC();
}
final Object lock = new Object();
public void startThreadA() {
new Thread(() -> {
synchronized (lock) {
System.out.println("ThreadA get lock");
try {
Thread.sleep(500);
} catch (Exception e) {}
System.out.println("ThreadA release lock");
}
}, "thread-A").start();
}
public void startThreadB() {
new Thread(() -> {
synchronized (lock) {
System.out.println("ThreadB get lock");
}
}, "thread-B").start();
}
public void startThreadC() {
new Thread(() -> {
synchronized (lock) {
System.out.println("ThreadC get lock");
}
}, "thread-C").start();
}
}
//最終打印結(jié)果一定是 A -> C -> B
下載鏈接
- jdk8u版本下載 點(diǎn)擊左側(cè)zip
- 鎖狀態(tài) https://blog.csdn.net/u014590757/article/details/79717549