What about the threads that request locks?

Posted by areric on Mon, 01 Nov 2021 09:42:16 +0100

I don't know if you've ever thought about what happened to the threads that applied for locks? Some may apply for a lock and execute the business code immediately. But if a lock is needed by many threads, how are these threads handled?

Today, we walk into synchronized heavyweight locks to see what happens to those threads that do not apply for locks.

ps: if you don't want to see the analysis results, you can pull to the end. There is a summary chart at the end. One picture is worth a thousand words

Previous articles have analyzed the optimization of locks in synchronized, but if there is a lot of competition, they will eventually become heavyweight locks. So let's start by directly analyzing the code of heavyweight locks.

Apply for lock

In the ObjectMonitor::enter function, there are many logic to judge and optimize the execution, but the core is to actually enter the queue through the EnterI function, which will block the current thread

void ObjectMonitor::EnterI(TRAPS) { Thread * const Self = THREAD; // CAS attempts to set the current thread as the thread holding the lock if (tryLock (self) > 0) {assert (_succ! = self, "invariant"); assert (_owner = = self, "invariant"); assert (_responsible! = self, "invariant"); return;} / / call tryLock in spin mode and try again. The operating system thinks there will be some subtle effects if (tryspin (self) > 0) {assert (_owner = = self, "Invariant"); assert (_succ! = self, "invariant"); assert (_responsible! = self, "invariant"); return;}... / / build the current thread as ObjectWaiter objectwaiter node (self); self - > _parkevent - > reset(); node. _prev = (ObjectWaiter *) 0xbad; node.tstate = ObjectWaiter:: ts_cxq; ObjectWaiter * NXT; for (;;) {/ / insert the ObjectWaiter object into the CXQ queue header through CAS. Node. _next = NXT = _cxq; if (atomic:: cmpxchg (& node, &_cxq, NXT) = = NXT) break; / / CAS fails due to CXQ change. Here, tryLock retry if (tryLock (self) > 0) {assert (_succ! = self, "independent"); assert (_owner = = self, "independent"); assert (_responsible! = self, "independent") ; return;}} / / block the current thread for (;) {if (tryLock (self) > 0) break; assert (_owner! = self, "invariant"); / / Park self if (_responsible = = self) {self - > _parkevent - > Park ((jlong) checkinterval); checkinterval * = 8; if (checkinterval > max_check_interval) {checkinterval = max_check_interval;}} else {self - > _parkevent - > park();} ... if (tryLock (self) > 0) break; + + nwakeups; if (tryspin (self) > 0) break;...}... / / self has obtained the lock and needs to remove it from CXQ or EntryList unlinkafteracquire (self, & node)
}
Copy code
  1. Before joining the queue, tryLock will be called to try to set the _owner (thread pointer held by the current ObjectMonitor object lock) field to self (pointing to the currently executing thread) through CAS operation. If the setting is successful, it indicates that the current thread has obtained the lock, otherwise it does not.
int ObjectMonitor::TryLock(Thread * Self) { void * own = _owner; if (own != NULL) return 0; if (Atomic::replace_if_null(Self, &_owner)) { return 1; } return -1;
}
Copy code
  1. If tryLock fails, it will call tryLock again (called tryLock in trySpin) to try to get the lock, because it tells the operating system that I urgently need this resource and hope to distribute it to me as much as possible. However, this affinity is not guaranteed to be guaranteed, but a positive operation.
  2. Wrap the current thread through the ObjectWaiter object and enter it into the head of the CXQ queue
  3. Block the current thread (via pthread_cond_wait)
  4. When the thread wakes up and acquires the lock, call the UnlinkAfterAcquire method to remove the ObjectWaiter from CXQ or EntryList

Core data structure

The ObjectMonitor object stores the queue of synchronized blocked threads and implements different queue scheduling strategies. Therefore, we must first understand some important properties of this object

class ObjectMonitor { // mark word volatile markOop _header; / / pointer to the owning thread or BasicLock void * volatile _owner; / / thread ID of the previous owner of the monitor volatile jlong _previous _owner _tid; / / number of reentries. The first time is 0 volatile intr_t _recursions; / / thread * volatile _succof the next awakened thread; / / the thread is blocked when entering or re entering The list is composed of objectwaiters, which is equivalent to an encapsulation object of a thread. ObjectWaiter * volatile _entrylist; / / the CXQ queue stores the threads that cannot enter because the lock has been blocked by other threads when entering. ObjectWaiter * volatile _cxq; / / it is in the wait state (call wait()) Will be added to the waitSet ObjectWaiter * volatile _WaitSet; / / omit other properties and methods
}
class ObjectWaiter : public StackObj { public: enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ }; // The latter node ObjectWaiter * volatile _next; / / the former node ObjectWaiter * volatile _prev; / / Thread* _thread; / / thread status volatile TStates TState; public: ObjectWaiter(Thread* thread);
};
Copy code

When you see _next and _prev in ObjectWaiter, you will understand that the two-way queue is used to realize the waiting queue, but in fact, the queue entry operation above does not form a two-way list. The two-way list is formed when the exit lock is locked.

wait

Java Object class provides a way of communication between wait and notify threads based on native implementation. In JDK, wait/notify/notifyAll is implemented through native. Of course, in the JVM, its implementation is still in src/hotspot/share/runtime/objectMonitor.cpp.

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { Thread * const Self = THREAD; JavaThread *jt = (JavaThread *)THREAD; ... // If the thread is interrupted, you need to throw an exception if (interruptible & & thread:: is_interrupted (self, true) & &! Has_pending_exception) {thread (vmsymbols:: java_lang_interruptedexception()); return;} JT - > set_current_waiting_monitor (this); / / construct the ObjectWaiter node ObjectWaiter node(Self); node.TState = ObjectWaiter::TS_WAIT;... / / add ObjectWaiter to the tail of WaitSet addwaiter & node; / / release lock exit(true, Self);... / / investigate park(), block current thread if (interruptible & & (thread:: is_interrupted (thread, false) | has_pending_exception)) {/ / intentionally empty} else if (node. _notified = = 0) {if (mill < = 0) { Self->_ParkEvent->park(); } else { ret = Self->_ParkEvent->park(millis); } } ...
}
// Insert node at the end of the bidirectional list _WaitSet
inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) { if (_WaitSet == NULL) { _WaitSet = node; node->_prev = node; node->_next = node; } else { ObjectWaiter* head = _WaitSet; ObjectWaiter* tail = head->_prev; tail->_next = node; head->_prev = node; node->_next = head; node->_prev = tail; }
Copy code

I listed the main method logic of wait above, and will mainly perform the following steps

  1. First, judge whether the current thread is interrupted. If it is interrupted, throw InterruptedException
  2. If it is not interrupted, the ObjectWaiter node will be constructed with the current thread and inserted into the tail of the bidirectional linked list WaitSet
  3. Call exit to release the lock (the logic of releasing the lock will be analyzed later)
  4. Calling Park (actually pthread_cond_wait) blocks the current thread

notify

Similarly, the logic of notify is also in objectmonitor.cpp

void ObjectMonitor::notify(TRAPS) { CHECK_OWNER(); // If waitSet is empty, directly return if (_waitSet = = null) {tevent (empty notify); return;} DTrace_ MONITOR_ PROBE(notify, this, object(), THREAD); //  Wake up a thread INotify(THREAD); OM_PERFDATA_OP(Notifications, inc(1));
}
Copy code

In notify, you will first judge whether the waitSet is empty. If it is empty, it means that no thread is waiting, and it will be returned directly. Otherwise, the INotify method is called.

The notifyAll method is actually a circular call to INotify

void ObjectMonitor::INotify(Thread * Self) { // Before notify, you need to obtain a lock to ensure concurrency security. Thread:: spinacquire (&_waitsetlock, "waitset - notify")// Remove and return the first element in the waitset. For example, 1 < -- > 2 < -- > 3 was returned in the waitset before, and now 1 is returned. Then the waitset becomes 2 < -- > 3 objectwaiter * iterator = dequeuewaiter(); if (iterator != NULL) { // Disposition - what might we do with iterator ? // a. add it directly to the EntryList - either tail (policy == 1) // or head (policy == 0). // b. push it onto the front of the _cxq (policy == 2). // For now we use (b) . / / set the thread state iterator - > tstate = objectwaiter:: ts_enter; iterator - > u notified = 1; iterator - > u notifier_tid = jfr_thread_id (self); objectwaiter * list = _entrylist; if (list! = null) {assert (list - > prev = = null, "independent"); assert (list - > tstate = = objectwaiter:: ts_enter, "independent"); assert (list! = iterator, "independent");} //Prepend to CXQ if (list = = null) {iterator - > _next = iterator - > _prev = null; _entrylist = iterator;} else {iterator - > tstate = objectwaiter:: ts_cxq; for (;) {/ / put the node to wake up in the head of CXQ objectwaiter * front = _cxq; iterator - > _next = front; if (atomic:: cmpxchg (iterator, &_cxq, front) {break;}}} Iterator - > wait_reenter_begin (this);} / / release the waitset lock after notify is executed. Note that this is not to release the lock held by the thread thread:: spinrelease (&_waitsetlock);
}
Copy code

The logic of notify is relatively simple, that is, remove the head node of WaitSet from the queue. If the EntryList is empty, put the queued node into the EntryList. If the EntryList is not empty, insert the node into the head node of CXQ list.

It should be noted that notify does not release the lock, and the logic of releasing the lock is in exit

exit

When a thread obtains the object lock successfully, it can execute the customized synchronization code block. After execution, it will be executed into the exit function of ObjectMonitor to release the current object lock so that the next thread can obtain the lock.

void ObjectMonitor::exit(bool not_suspended, TRAPS) { Thread * const Self = THREAD; if (THREAD != _owner) { // The lock holder is the current thread if (thread - > is_lock_owned ((address) _owner)) {assert (_recurrences = = 0, "invariant"); u owner = thread; _recurrences = 0;} else {assert (false, "non balanced monitor enter / exit! Like JNI locking"); return;}} / / the number of reentries minus 1 if (_recurrences! = 0) {recurrences --; / / this is simple recursive enter return;} For (;) {... W = _EntryList; / / if EntryList is not empty, if (W! = null) {assert (W - > tstate = = objectwaiter:: ts_enter, "invariant"); / / execute unpark to release the lock ExitEpilog(Self, w); return;} W = _cxq;... _EntryList = w; objectwaiter * q = null; objectwaiter * p; / / here _cxqor _entrylistis changed from a one-way linked list to a two-way linked list for (P = w; P! = null; P = P - > u next) {guarantee (P - > tstate = = objectwaiter:: ts_cxq, "invariant"); P - > tstate = objectwaiter:: ts_enter; P - > u prev = q; q = p;} w = _EntryList; if (W! = null) {guarantee (W - > tstate = = objectwaiter:: ts_enter, "invariant"); / / execute unpark to release the lock ExitEpilog(Self, w); return;}...}
}
void ObjectMonitor::ExitEpilog(Thread * Self, ObjectWaiter * Wakee) { // Exit protocol: / / 1. St _succ = wake / / 2. Membar #loadstore |#storestore; / / 2. St _owner = null / / 3. Unpark (wake) _succ = wake - > _thread; parkevent * trigger = wake - > _event; wake = null; / / drop the lock orderaccess:: release_store (&_owner, (void *) null); orderaccess:: fence();... / / release lock trigger - > unpark();
}
Copy code

The logic of exit is relatively simple

  1. If the current thread wants to give up the lock, check whether its re-entry times are 0. If not, subtract 1 from the re-entry times, and then exit directly.
  2. If the EntryList is not empty, the thread in the header element of the EntryList is awakened
  3. Assign the CXQ pointer to EntryList, then turn the CXQ list into a double linked list through the loop, then call ExitEpilog to wake up the header node of the CXQ list (actually through pthread_cond_signal).

After that, EntryList and CXQ are the same, because CXQ is assigned to EntryList.

It should be noted that the awakened thread will continue to execute the EnterI method at the beginning of the article. At this time, the ObjectWaiter will be removed from the EntryList or CXQ.

Actual combat demonstration

The above source code is based on jdk12. The code in jdk8 has other policies about exit and notify (which thread to choose), and only the default policy has been retained since JDK9.

Therefore, the running results of the following Java code are the same whether in jdk8 or jdk12.

Object lock = new Object();
Thread t1 = new Thread(() -> { System.out.println("Thread 1 start!!!!!!"); synchronized (lock) { try { lock.wait(); } catch (Exception e) { } System.out.println("Thread 1 end!!!!!!"); }
});
Thread t2 = new Thread(() -> { System.out.println("Thread 2 start!!!!!!"); synchronized (lock) { try { lock.wait(); } catch (Exception e) { } System.out.println("Thread 2 end!!!!!!"); }
});
Thread t3 = new Thread(() -> { System.out.println("Thread 3 start!!!!!!"); synchronized (lock) { try { lock.wait(); } catch (Exception e) { } System.out.println("Thread 3 end!!!!!!"); }
});
Thread t4 = new Thread(() -> { System.out.println("Thread 4 start!!!!!!"); synchronized (lock) { try { System.in.read(); } catch (Exception e) { } lock.notify(); lock.notify(); lock.notify(); System.out.println("Thread 4 end!!!!!!"); }
});
Thread t5 = new Thread(() -> { System.out.println("Thread 5 start!!!!!!"); synchronized (lock) { System.out.println("Thread 5 end!!!!!!"); }
});
Thread t6 = new Thread(() -> { System.out.println("Thread 6 start!!!!!!"); synchronized (lock) { System.out.println("Thread 6 end!!!!!!"); }
});
Thread t7 = new Thread(() -> { System.out.println("Thread 7 start!!!!!!"); synchronized (lock) { System.out.println("Thread 7 end!!!!!!"); }
});
t1.start();
sleep_1_second();
t2.start();
sleep_1_second();
t3.start();
sleep_1_second();
t4.start();
sleep_1_second();
t5.start();
sleep_1_second();
t6.start();
sleep_1_second();
t7.start();
Copy code

The above code is very simple. Let's analyze it.

Threads 1, 2 and 3 call wait, so it will block. Then the linked list structure of WaitSet is as follows:

Thread 4 acquired the lock and is waiting for an input

Threads 5, 6 and 7 are also waiting for locks, so they will also block. Therefore, the CXQ linked list structure is as follows:

When thread 4 enters anything and the carriage return ends (three of the notify methods have been called, but the lock has not been released)

After thread 4 gives up the lock, because the EntryList is not empty, thread 1 in the EntryList will be awakened first, and then the threads in the CXQ queue will be awakened (you can think that CXQ is EntryList later)

Therefore, the final thread execution order is 4 1 3 2 7 6 5, and our output results can also verify our conclusion

Thread 1 start!!!!!!
Thread 2 start!!!!!!
Thread 3 start!!!!!!
Thread 4 start!!!!!!
Thread 5 start!!!!!!
Thread 6 start!!!!!!
Thread 7 start!!!!!!
think123
Thread 4 end!!!!!!
Thread 1 end!!!!!!
Thread 3 end!!!!!!
Thread 2 end!!!!!!
Thread 7 end!!!!!!
Thread 6 end!!!!!!
Thread 5 end!!!!!!
Copy code

A picture is worth a thousand words

Your attention is my greatest support

Brother, give me a compliment and pay attention by the way

Author: think123 Link: https://juejin.im/post/6894126119743094798 Source: Nuggets The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.

This concludes the current article. Thank you for reading.