当前位置:网站首页>Source code analysis of object wait notify notifyAll
Source code analysis of object wait notify notifyAll
2022-07-26 09:26:00 【new hilbert()】
Java in Object Class Wait Notify NotifyAll Source code is as follows :
/** * Thread waiting * @param var1 millisecond * @param var3 nanosecond */
public final void wait(long var1, int var3) throws InterruptedException {
if (var1 < 0L) {
throw new IllegalArgumentException("timeout value is negative");
} else if (var3 >= 0 && var3 <= 999999) {
// nanosecond >0, Millisecond direct ++
if (var3 > 0) {
++var1;
}
// call native Method
this.wait(var1);
} else {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
}
/** * native Method thread waits */
public final native void wait(long var1) throws InterruptedException;
/** * native Method a single thread wakes up */
public final native void notify();
/** * native Method wakes up all threads in the waiting pool */
public final native void notifyAll();
Conditions before parsing the source code :
Object lock ObjectMonitor There are two kinds of queues: waiting queue and synchronization queue 
wait Method :
Thread waiting , Give up the object lock , Join the wait queue , Then enter park, Wait for other threads to release the lock unpark
synchronized (a) {
a.wait();
}
Equivalent to
moniter.enter // Get object lock
{
1. Determine if the lock exists
2. Judge the interrupt state
3. establish node Join in Waiting in line
4.moniter.exit( According to different strategies , Get the header node thread from the synchronization queue a, Then execute the thread a Of event.unpark Wake up mechanism )
5. This thread executes event.park Wait for another thread to wake up
6. Judge whether the wake-up is interrupted , Do you need to throw an exception
}
moniter.exit // Release the lock , Wake up the next object in the synchronization queue

- CHECK_OWNER Determine if the lock exists , Throw the exception if it doesn't exist . No addition Synchronize Words , Will throw out IllegalMonitorStateException
#define CHECK_OWNER()
do {
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) {
_owner = THREAD ; /* Convert from basiclock addr to Thread addr */
_recursions = 0;
OwnerIsThread = 1 ;
} else {
TEVENT (Throw IMSX) ;
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
}
} while (false)
- call is_interrupted() Determine and clear thread interrupt status , If the interrupt status is true, Throw an interrupt exception and end
// call is_interrupted() Determine and clear thread interrupt status , If the interrupt status is true, Throw an interrupt exception and end
if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
...
TEVENT (Wait - Throw IEX) ;
THROW(vmSymbols::java_lang_InterruptedException());
return ;
}
- Use spin lock to create a node Put in queue
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add")
AddWaiter (&node)
Thread::SpinRelease (&_WaitSetLock)
- Exit monitor exit (Self)
intptr_t save = _recursions; // Record the number of old recursions
_waiters++; // waiters Self increasing
_recursions = 0; // Set up recursion level to be 1
exit (Self) ; // Exit monitor
- utilize parkEvent.park Method block wait signal reminder
if (millis <= 0) {
// call park() Method to block threads
Self->_ParkEvent->park () ;
} else {
// call park() Method blocks the thread during the timeout
ret = Self->_ParkEvent->park (millis) ;
}
- Determine whether to interrupt , By parkEvent.unpark Wake up and judge interrupt Sponsored , still notify Sponsored
if (!WasNotified) {
if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
TEVENT (Wait - throw IEX from epilog) ;
THROW(vmSymbols::java_lang_InterruptedException());
}
}
wait The essence is to call ObjectMonitor Of wait Method
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
Thread * const Self = THREAD ;
assert(Self->is_Java_thread(), "Must be Java thread!");
JavaThread *jt = (JavaThread *)THREAD;
DeferredInitialize () ;
// Throw IMSX or IEX.
CHECK_OWNER();
// call is_interrupted() Determine and clear thread interrupt status , If the interrupt status is true, Throw an interrupt exception and end
if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
//post monitor waited event
// Note that this is the past tense , It's over
if (JvmtiExport::should_post_monitor_waited()) {
// Be careful : Pass the parameters here 'false', This is because the thread is interrupted , The wait will not time out
JvmtiExport::post_monitor_waited(jt, this, false);
}
TEVENT (Wait - Throw IEX) ;
THROW(vmSymbols::java_lang_InterruptedException());
return ;
}
TEVENT (Wait) ;
assert (Self->_Stalled == 0, "invariant") ;
Self->_Stalled = intptr_t(this) ;
jt->set_current_waiting_monitor(this);
// create a node to be put into the queue
// Critically, after we reset() the event but prior to park(), we must check
// for a pending interrupt.
// Create a node Put in queue
// The key is , stay reset() after , But in park() Before , You must check for pending interrupts
ObjectWaiter node(Self);
node.TState = ObjectWaiter::TS_WAIT ;
Self->_ParkEvent->reset() ;
OrderAccess::fence();
// In this case, the waiting queue is a circular two-way linked list , But it can also be a priority queue or any data structure .
//_WaitSetLock Protect the waiting queue .
// Usually , The wait queue can only be used by the monitor *except* Owner access , But in park() It is also possible to return due to interrupt timeout .
// The competition is very small , So use a spin lock instead of a heavyweight blocking lock .
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;
AddWaiter (&node) ;
Thread::SpinRelease (&_WaitSetLock) ;
if ((SyncFlags & 4) == 0) {
_Responsible = NULL ;
}
intptr_t save = _recursions; // Record the number of old recursions
_waiters++; // waiters Self increasing
_recursions = 0; // Set up recursion level to be 1
exit (Self) ; // Exit monitor
guarantee (_owner != Self, "invariant") ;
// Once on top exit() Deleted... In the call ObjectMonitor The ownership of the ,
// Another thread can enter ObjectMonitor, perform notify() and exit() Object monitor .
// If another thread exit() Call to select this thread as a successor , And this thread is publishing MONITOR_CONTENDED_EXIT Occurs when unpark() call ,
// Then we use RawMonitors Operational event risk handling , And use unpark().
// To avoid this problem , We re released the event , Even if the original unpark(),
// It won't do any harm , Because a successor has been chosen for this monitor .
if (node._notified != 0 && _succ == Self) {
node._event->unpark();
}
// The thread is on the WaitSet list - now park() it.
// On MP systems it's conceivable that a brief spin before we park
// could be profitable.
//
// TODO-FIXME: change the following logic to a loop of the form
// while (!timeout && !interrupted && _notified == 0) park()
int ret = OS_OK ;
int WasNotified = 0 ;
{ // State transition wrappers
OSThread* osthread = Self->osthread();
OSThreadWaitState osts(osthread, true);
{
ThreadBlockInVM tbivm(jt);
// Thread is in thread_blocked state and oop access is unsafe.
// Thread is blocked , also oop Access is not secure
jt->set_suspend_equivalent();
if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
// Intentionally empty Empty processing
} else
if (node._notified == 0) {
if (millis <= 0) {
// call park() Method to block threads
Self->_ParkEvent->park () ;
} else {
// call park() Method blocks the thread during the timeout
ret = Self->_ParkEvent->park (millis) ;
}
}
// were we externally suspended while we were waiting?
if (ExitSuspendEquivalent (jt)) {
// TODO-FIXME: add -- if succ == Self then succ = null.
jt->java_suspend_self();
}
} // Exit thread safepoint: transition _thread_blocked -> _thread_in_vm
// When the thread is not waiting on the queue , Use double check locking to avoid getting _WaitSetLock
if (node.TState == ObjectWaiter::TS_WAIT) {
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - unlink") ;
if (node.TState == ObjectWaiter::TS_WAIT) {
DequeueSpecificWaiter (&node) ; // unlink from WaitSet
assert(node._notified == 0, "invariant");
node.TState = ObjectWaiter::TS_RUN ;
}
Thread::SpinRelease (&_WaitSetLock) ;
}
// From the perspective of this thread ,Node's TState It is stable. ,
// No other thread can modify... Asynchronously TState
guarantee (node.TState != ObjectWaiter::TS_WAIT, "invariant") ;
OrderAccess::loadload() ;
if (_succ == Self) _succ = NULL ;
WasNotified = node._notified ;
// Reentry phase -- reacquire the monitor.
// re-enter contended( competition ) monitor after object.wait().
// retain OBJECT_WAIT state until re-enter successfully completes
// Thread state is thread_in_vm and oop access is again safe,
// although the raw address of the object may have changed.
// (Don't cache naked oops over safepoints, of course).
// post monitor waited event.
// Note that this is the past tense , It's over
if (JvmtiExport::should_post_monitor_waited()) {
JvmtiExport::post_monitor_waited(jt, this, ret == OS_TIMEOUT);
}
OrderAccess::fence() ;
assert (Self->_Stalled != 0, "invariant") ;
Self->_Stalled = 0 ;
assert (_owner != Self, "invariant") ;
ObjectWaiter::TStates v = node.TState ;
if (v == ObjectWaiter::TS_RUN) {
enter (Self) ;
} else {
guarantee (v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant") ;
ReenterI (Self, &node) ;
node.wait_reenter_end(this);
}
// Self has reacquired the lock.
// Lifecycle - the node representing Self must not appear on any queues.
// Node is about to go out-of-scope, but even if it were immortal( For a long time ) we wouldn't
// want residual( Residual ) elements associated with this thread left on any lists.
guarantee (node.TState == ObjectWaiter::TS_RUN, "invariant") ;
assert (_owner == Self, "invariant") ;
assert (_succ != Self , "invariant") ;
} // OSThreadWaitState()
jt->set_current_waiting_monitor(NULL);
guarantee (_recursions == 0, "invariant") ;
_recursions = save; // restore the old recursion count
_waiters--; // decrement the number of waiters
// Verify a few postconditions
assert (_owner == Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
if (SyncFlags & 32) {
OrderAccess::fence() ;
}
// Check whether there is any notice notify happen
// from park() Method returned , Determine whether it is because the interrupt returns , Call again
// thread::is_interrupted(Self, true) Determine and clear thread interrupt status
// If the interrupt status is true, Throw an interrupt exception and end .
if (!WasNotified) {
// no, it could be timeout or Thread.interrupt() or both
// check for interrupt event, otherwise it is timeout
if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
TEVENT (Wait - throw IEX from epilog) ;
THROW(vmSymbols::java_lang_InterruptedException());
}
}
// Be careful : False wakeups will be considered timeout ; Monitor notifications take precedence over thread interrupts .
}
notify Method : Get the first node from the waiting queue , Then join the synchronization queue , It has no function of releasing the lock itself , yes Synchroinzed Self provided ( important )
synchronized (a) {
a.notify();
}
amount to
moniter.enter // Get object lock
{
1. Determine if the lock exists
2. Get the first node from the waiting queue
3. According to policy Add policy to cxq perhaps entryList Synchronous queue
}
moniter.exit // Release the lock , Wake up the next object in the synchronization queue

- CHECK_OWNER Determine if the lock exists , Throw the exception if it doesn't exist . No addition Synchronize Words , Will throw out IllegalMonitorStateException
#define CHECK_OWNER()
do {
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) {
_owner = THREAD ; /* Convert from basiclock addr to Thread addr */
_recursions = 0;
OwnerIsThread = 1 ;
} else {
TEVENT (Throw IMSX) ;
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
}
} while (false)
- Take the first node from the waiting queue
ObjectWaiter * iterator = DequeueWaiter() ;
- According to the different policy, Add the node waiting for the column to the synchronization queue
if (Policy == 0) { // prepend( prepend ) to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
List->_prev = iterator ;
iterator->_next = List ;
iterator->_prev = NULL ;
_EntryList = iterator ;
}
}......
Notify The essence is to call ObjectMonitor Of notify Method
void ObjectMonitor::notify(TRAPS) {
CHECK_OWNER();
if (_WaitSet == NULL) {
TEVENT (Empty-Notify) ;
return ;
}
DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);
int Policy = Knob_MoveNotifyee ;
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
ObjectWaiter * iterator = DequeueWaiter() ;
if (iterator != NULL) {
TEVENT (Notify1 - Transfer) ;
guarantee (iterator->TState == ObjectWaiter::TS_WAIT, "invariant") ;
guarantee (iterator->_notified == 0, "invariant") ;
if (Policy != 4) {
iterator->TState = ObjectWaiter::TS_ENTER ;
}
iterator->_notified = 1 ;
ObjectWaiter * List = _EntryList ;
if (List != NULL) {
assert (List->_prev == NULL, "invariant") ;
assert (List->TState == ObjectWaiter::TS_ENTER, "invariant") ;
assert (List != iterator, "invariant") ;
}
if (Policy == 0) { // prepend( prepend ) to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
List->_prev = iterator ;
iterator->_next = List ;
iterator->_prev = NULL ;
_EntryList = iterator ;
}
} else
if (Policy == 1) { // append( Really add ) to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
// consider : Get... Now EntryList Of tail You need to traverse the entire list
// take tail The access is converted to CDLL Instead of using the current DLL, So that the access time is fixed .
ObjectWaiter * Tail ;
for (Tail = List ; Tail->_next != NULL ; Tail = Tail->_next) ;
assert (Tail != NULL && Tail->_next == NULL, "invariant") ;
Tail->_next = iterator ;
iterator->_prev = Tail ;
iterator->_next = NULL ;
}
} else
if (Policy == 2) { // prepend to cxq
// prepend( prepend ) to cxq
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Front = _cxq ;
iterator->_next = Front ;
if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
break ;
}
}
}
} else
if (Policy == 3) { // append( Really add ) to cxq
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Tail ;
Tail = _cxq ;
if (Tail == NULL) {
iterator->_next = NULL ;
if (Atomic::cmpxchg_ptr (iterator, &_cxq, NULL) == NULL) {
break ;
}
} else {
while (Tail->_next != NULL) Tail = Tail->_next ;
Tail->_next = iterator ;
iterator->_prev = Tail ;
iterator->_next = NULL ;
break ;
}
}
} else {
ParkEvent * ev = iterator->_event ;
iterator->TState = ObjectWaiter::TS_RUN ;
OrderAccess::fence() ;
ev->unpark() ;
}
if (Policy < 4) {
iterator->wait_reenter_begin(this);
}
// _WaitSetLock protects the wait queue, not the EntryList. We could
// move the add-to-EntryList operation, above, outside the critical section
// protected by _WaitSetLock. In practice that's not useful. With the
// exception of wait() timeouts and interrupts the monitor owner
// is the only thread that grabs _WaitSetLock. There's almost no contention
// on _WaitSetLock so it's not profitable to reduce the length of the
// critical section.
}
Thread::SpinRelease (&_WaitSetLock) ;
if (iterator != NULL && ObjectMonitor::_sync_Notifications != NULL) {
ObjectMonitor::_sync_Notifications->inc() ;
}
}
notifyAll Method : Follow Notify The method is similar to , Just use for loop Will wait for all nodes of the queue , Join the synchronization queue , It has no function of releasing the lock itself , yes Synchroinzed Self provided
void ObjectMonitor::notifyAll(TRAPS) {
CHECK_OWNER();
ObjectWaiter* iterator;
if (_WaitSet == NULL) {
TEVENT (Empty-NotifyAll) ;
return ;
}
DTRACE_MONITOR_PROBE(notifyAll, this, object(), THREAD);
int Policy = Knob_MoveNotifyee ;
int Tally = 0 ;
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notifyall") ;
for (;;) {
iterator = DequeueWaiter () ;
if (iterator == NULL) break ;
TEVENT (NotifyAll - Transfer1) ;
++Tally ;
// Disposition - what might we do with iterator ?
// a. add it directly to the EntryList - either tail or head.
// b. push it onto the front of the _cxq.
// For now we use (a).
guarantee (iterator->TState == ObjectWaiter::TS_WAIT, "invariant") ;
guarantee (iterator->_notified == 0, "invariant") ;
iterator->_notified = 1 ;
if (Policy != 4) {
iterator->TState = ObjectWaiter::TS_ENTER ;
}
ObjectWaiter * List = _EntryList ;
if (List != NULL) {
assert (List->_prev == NULL, "invariant") ;
assert (List->TState == ObjectWaiter::TS_ENTER, "invariant") ;
assert (List != iterator, "invariant") ;
}
if (Policy == 0) { // prepend to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
List->_prev = iterator ;
iterator->_next = List ;
iterator->_prev = NULL ;
_EntryList = iterator ;
}
} else
if (Policy == 1) { // append to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
// CONSIDER: finding the tail currently requires a linear-time walk of
// the EntryList. We can make tail access constant-time by converting to
// a CDLL instead of using our current DLL.
ObjectWaiter * Tail ;
for (Tail = List ; Tail->_next != NULL ; Tail = Tail->_next) ;
assert (Tail != NULL && Tail->_next == NULL, "invariant") ;
Tail->_next = iterator ;
iterator->_prev = Tail ;
iterator->_next = NULL ;
}
} else
if (Policy == 2) { // prepend to cxq
// prepend to cxq
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Front = _cxq ;
iterator->_next = Front ;
if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
break ;
}
}
} else
if (Policy == 3) { // append to cxq
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Tail ;
Tail = _cxq ;
if (Tail == NULL) {
iterator->_next = NULL ;
if (Atomic::cmpxchg_ptr (iterator, &_cxq, NULL) == NULL) {
break ;
}
} else {
while (Tail->_next != NULL) Tail = Tail->_next ;
Tail->_next = iterator ;
iterator->_prev = Tail ;
iterator->_next = NULL ;
break ;
}
}
} else {
ParkEvent * ev = iterator->_event ;
iterator->TState = ObjectWaiter::TS_RUN ;
OrderAccess::fence() ;
ev->unpark() ;
}
if (Policy < 4) {
iterator->wait_reenter_begin(this);
}
// _WaitSetLock protects the wait queue, not the EntryList. We could
// move the add-to-EntryList operation, above, outside the critical section
// protected by _WaitSetLock. In practice that's not useful. With the
// exception of wait() timeouts and interrupts the monitor owner
// is the only thread that grabs _WaitSetLock. There's almost no contention
// on _WaitSetLock so it's not profitable to reduce the length of the
// critical section.
}
Thread::SpinRelease (&_WaitSetLock) ;
if (Tally != 0 && ObjectMonitor::_sync_Notifications != NULL) {
ObjectMonitor::_sync_Notifications->inc(Tally) ;
}
}
problem 1:wait In the previous link ParkEvent.park Blocking waiting to wake up , however notify The essence is to add the nodes in the waiting queue to the synchronization queue nodes , But there are many nodes in the synchronization queue , Who will use it , Where was it called ParkEvent.unpark The wake-up thread continues to go down ?
problem 2:wait Method just exits the object lock . How does it give object locks to other threads , Because the transfer of this object lock only occurs in wait and notify In these two threads , There is no third party to coordinate , How object locks flow .
In fact, the essence is a problem : How object locks are transferred ?
Key points :wait The method itself called once ObjectMonitor.exit Method ,Synchronized The keyword itself has once ObjectMonitor.exit Method .
void ATTR ObjectMonitor::exit(TRAPS) {
......
// according to QMode Policy from synchronization queue Take out node
if (QMode == 2 && _cxq != NULL) {
// QMode == 2 : cxq has precedence over EntryList.
// Try to directly wake a successor from the cxq.
// If successful, the successor will need to unlink itself from cxq.
w = _cxq ;
assert (w != NULL, "invariant") ;
assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
ExitEpilog (Self, w) ;
return ;
}
if (QMode == 3 && _cxq != NULL) {
// Aggressively drain cxq into EntryList at the first opportunity.
// This policy ensure that recently-run threads live at the head of EntryList.
// Drain _cxq into EntryList - bulk transfer.
// First, detach _cxq.
// The following loop is tantamount to: w = swap (&cxq, NULL)
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
assert (w != NULL , "invariant") ;
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 ;
}
// Append the RATs to the EntryList
// TODO: organize EntryList as a CDLL so we can locate the tail in constant-time.
ObjectWaiter * Tail ;
for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ;
if (Tail == NULL) {
_EntryList = w ;
} else {
Tail->_next = w ;
w->_prev = Tail ;
}
// Fall thru into code that tries to wake a successor from EntryList
}
......
w = _EntryList ;
if (w != NULL) {
guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}
}
}
The key point is to get the corresponding node and execute ExitEpilog Method , Wake up this is wait The node of
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
{
assert (_owner == Self, "invariant") ;
ParkEvent * Trigger = Wakee->_event ;
.... // Here is the corresponding wait Method used ParkEvent.park
Trigger->unpark() ; //unpark Wake up the wait Threads
.....
if (ObjectMonitor::_sync_Parks != NULL) {
ObjectMonitor::_sync_Parks->inc() ;
}
}
wait Method process



Notify Method process


边栏推荐
猜你喜欢
随机推荐
大二上第一周学习笔记
tabbarController的使用
OpenCV 表格识别之表格提取(二)
【Mysql】Mysql锁详解(三)
Elastic APM installation and use
Selection and practice of distributed tracking system
登录模块用例编写
注册模块用例编写
Force button list question
TabbarController的封装
Your login IP is not within the login mask configured by the administrator
Personality test system V1.0
opencv图像处理
Vertical search
839. Simulation reactor
838. Heap sorting
Object type collections are de duplicated according to the value of an attribute
malloc分配空间失败,并且不返回null
el-table的formatter属性的用法
附加到进程之后,断点显示“当前不会命中断点 还没有为该文档加载任何符号”






![[MySQL] detailed explanation of MySQL lock (III)](/img/3c/c6b5aa5693e6e7b5730a286d988c82.png)

