There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti

2025/07/1002:02:42 technology 1172

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

is written in front of

  • There are all kinds of trivial matters everywhere you go. In MySQL, we have already talked about all kinds of trivial matters. -MySQL lock mechanism && transactions, let's take a look at the lock upgrade process in Java, and the comparison between various locks, pessimistic and optimistic, roughened and eliminated~

Four locks Markword

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

priority

  • bias lock-lightweight lock-(spin first, no expansion) heavyweight lock (no spin directly blocking)

lightweight lock

is just a lock object in the stack , not a heavyweight

such as monitor. The use scenario of lightweight lock is: if an object has multiple threads to lock it, and has a staggered lock time (that is, no one can compete, so there will be no blockage ), then lightweight lock can be used for optimization. The lightweight lock is transparent to the user , that is, the syntax is still synchronized. Suppose there are two methods for synchronization blocks, and lock

statictml8 final Object obj = new Object();public static void method1() {synchronized( obj ) {// Synchronized block Amethod2();}}public static void method2() {synchronized( obj ) {// Synchronized block B}}Copy code 
  1. Every time it points to a synchronized code block, it will create an lock record (Lock Record) object . Each thread will include a lock record structure. The lock record can store the Mark Word of the object (used to change the lock record encoding of the object) and the object reference reference (indicates which object to point to)

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. Let the Object reference in the lock record point to the object, and try to replace the Mark Word of the Object object with CAS (compare and sweep), and change the mark of the object Word is updated to a pointer to Lock Record, and the value of Mark Word is stored in the lock record (equivalent to pointing the owner pointer in the Lock Record to the Mark Word of the object.)

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. If the cas is replaced successfully, then the object header of the object stores the address and status of the lock record 01, as shown below

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. If cas fails , there are two situations. If other threads of already hold the lightweight lock of the Object, it means there is competition and will enter the lock expansion stage. If your own thread has already executed synchronized to lock, then add another Lock Record as the reentry count

and in this new Lock Record, the MarkWord of the object is null (equivalent to being snatched by the previous one)

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. When the thread exits the synchronized code block, if the value is null The lock record of indicates reentry. At this time, the lock record is reset, which means the reentry count is reduced by

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. When the thread exits the synchronized code block, if the acquired lock record value is not null, then **Use cas to restore the Mark Word value to the object ** If it succeeds, the unlocking is successful. failed. This means that the lightweight lock has undergone lock expansion or has been upgraded to a heavyweight lock, and enters the heavyweight lock unlocking process

summary

  • Locking and unlocking are both used to exchange Locks with CAS Record

lock expansion

If the cas operation cannot be successful during the process of trying to add a lightweight lock, there is a situation where other threads have already added a lightweight lock to this object, so it is necessary to perform lock expansion and turn the lightweight lock into a heavyweight lock.

  1. When Thread-1 performs lightweight lock, Thread-0 has added a lightweight lock to the object

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. At this time, Thread-1 adds lightweight lock failed to , enter the lock expansion process, that is, apply for Monitor lock for object , let the Object point to the heavyweight lock address, and then enter the Monitor EntryList itself and becomes BLOCKED blocking state

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. When Thread-0 When launching synchronized synchronization blocks, use cas to restore the value of Mark Word to the object header. If it fails, the unlocking process of heavyweight lock will be entered, that is, find the Monitor object according to the address of the monitor, set the Owner to null, and wake up the Thread-1 thread in EntryList

Total process

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

Spin optimization

In order to make the current thread "wait for a moment", we need to let the current thread spin. If the thread that locks the synchronization resource before the spin has released the lock, then the current thread can directly acquire the synchronization resource without blocking, thereby avoiding the overhead of switching threads. This is spin lock . When the heavyweight lock competes for , you can also use spin to optimize. If the current thread spins successfully (that is, the thread holding the lock releases the lock when is spinning), then the current thread can obtain the lock without context switching

  1. spin retry successfully

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

  1. spin retry failed, spinning a certain number of times but still not waiting for the thread holding the lock

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

spin will take up cpu time, single core CPU Spin is a waste, and multi-core CPU spin can play its advantages. After Java 6, spin lock is adaptive. For example, if the object has just succeeded, then it is believed that the possibility of spin success will be high, so it will spin a few more times; otherwise, there will be less spin or even no spin. In short, it is more intelligent. Java 7 After that, it cannot be controlled whether the spin function is enabled

adaptive spin lock

spin lock was introduced in JDK1.4.2, and it is enabled using -XX:+UseSpinning. JDK 6 has become enabled by default, and an adaptive spin lock (adaptive spin lock) has been introduced.

adaptation means that the spin time (number of times) is no longer fixed, but is determined by the spin time on the same lock the previous time and the state of the lock owner . If on the same lock object, the spin wait has just successfully obtained the lock and the thread holding the lock is running, the virtual machine will think that the spin is also likely to succeed again, and it will allow the spin wait for a relatively longer period of time. If spins are rarely successfully obtained for a lock, then the spin process may be omitted when trying to acquire this lock in the future and block the thread directly to avoid wasting processor resources.

is in spin lock There are three other common lock forms: TicketLock, CLHlock and MCSlock

bias lock

In lightweight lock, we can find that if the same thread reenters the same 2 objects, it also needs to perform CAS operations, which is a bit time-consuming, so Java6 began to introduce bias locks, and only the Mark of the object was used for the first time. The Word header is set to the lock thread ID. After , when the lock thread re-enters the lock thread and finds that the thread ID is its own, then there is no need to perform CAS to lock and unlock

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

biased state

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

object creation process

  1. If the biased lock is enabled ( is enabled by default ), then after the object is created, Mark Word The last three digits have 101, and this is its Thread, epoch, and age, and set these values ​​when locking.
  2. biased lock is delayed by default and will not take effect immediately when the program starts. If you want to avoid delay, you can add virtual machine parameters to disable delay: -XX:BiasedLockingStartupDelay=0 to disable delay
  3. Note: After the object in biased lock is unlocked, the thread id is still stored in the object header

plus virtual machine parameters -XX:BiasedLockingStartupDelay=0 for testing

public static void main(String[] args) throws InterruptedException {Test1 t = new Test1();// test.parseObjectHeader(getObjectHeader(t));// synchronized after locking (t){test.parseObjectHeader(getObjectHeader(t));}// test.parseObjectHeader(getObjectHeader(t));}// test.parseObjectHeader(getObjectHeader(t));} // The output result is as follows, the status codes of the three outputs are 101biasedLockFlag (1bit): 1LockFlag (2bit): 01biasedLockFlag (1bit): 1LockFlag (2bit): 01biasedLockFlag (2bit): 01biasedLockFlag (1bit): 1LockFlag (2bit): 01 Copy code 

Disable bias lock

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

Test disabled: If the bias lock is not enabled, the value of the last three digits after the object is created is 001. At this time, its hashcode and age are 0. Hashcode is assigned only when it is used for hashcode for the first time. When running the above test code, add VM parameters -XX:-UseBiasedLocking to disable bias lock (use lightweight lock is preferred if bias lock is disabled), exit synchronized state and change back to 001

  1. Test code: Virtual machine parameters -XX:-UseBiasedLocking
  2. output results are as follows, the initial state is 001, then add lightweight locks to 00, and finally restore to 001
 biasedLockFlag (1bit): 0LockFlag (2bit): 01LockFlag (2bit): 00biasedLockFlag (1bit): 0LockFlag (2bit): 01 Copy code 

Undo bias lock - hashcode method

test hashCode: When the hashcode method of the object is called, the bias lock of the object will be undocumented. has no position to store **hashcode** value , and the lightweight lock is lockRecord, and the heavyweight lock is monitor

  1. test code is as follows, using the virtual machine parameter -XX:BiasedLockingStartupDelay=0 to ensure that our program first uses bias lock! However, the result shows that the program still uses a lightweight lock.
public static void main(String[] args) throws InterruptedException {Test1 t = new Test1();//Revoke bias lock t.hashCode();test.parseObjectHeader(getObjectHeader(t));synchronized (t){test.parseObjectHeader(getObjectHeader(t));}test.parseObjectHeader(getObjectHeader(t));} Output result biasedLockFlag (1bit): 0LockFlag (2bit): 01LockFlag (2bit): 00biasedLockFlag (1bit): 0LockFlag (2bit): 01 Copy code 

Undo biased lock-other threads use object

Here we demonstrate the process of deflected lock revocation into lightweight locks. Then the use conditions of lightweight locks must be met, that is, no thread competes for locks on the same object. We use wait and notify to assist in implementing

  1. code. Virtual machine parameters -XX:BiasedLockingStartupDelay=0 to ensure that our program first uses biased locks!
  2. output result. The first one used a biased lock, but when the second thread tried to acquire the object lock, it found that the object was originally biased to thread one, so the biased lock would fail. The lightweight lock
biasedLockFlag (1bit): 1LockFlag (2bit): 01biasedLockFlag (1bit): 1LockFlag (2bit): 01biasedLockFlag (1bit): 1LockFlag (2bit): 01biasedLockFlag (1bit): 1LockFlag (2bit): 01biasedLockFlag (1bit): 1LockFlag (2bit): 01biasedLockFlag (1bit): 01biasedLockFlag (2bit): 01LockFlag (2bit): 01LockFlag (2bit): 01LockFlag (2bit): 00biasedLockFlag (1bit): 0LockFlag (2bit): 01 Copy code 

Undefine bias lock - Calling wait/notify

will turn the object's lock into a heavyweight lock, because wait/notify method only supports heavyweight locks

batch heavy bias

If the object is accessed by multiple threads, but there is no competition, the object that is biased to thread one has the chance to redirect thread two, that is, you don't need to upgrade to a lightweight lock, but this is inconsistent with the experiment we did before. In fact, to achieve redirection, there must be conditions: if more than 20 objects are biased to the same thread, such as thread one revoke, then the 20th and later objects can turn the action of undoing the bias to thread one into turning the 20th and later objects towards thread two.

optimistic lock vs pessimistic lock

optimistic lock (no lock)

CAS

Advantages

  • will not block, all threads are in a competition state, suitable for situations where threads are small

Disadvantages

  • When there are many threads In the future, we will constantly spin and waste CPU resources

read more optimistic locks (less conflicts)

write more pessimistic locks (more conflicts)

From the above introduction of the two locks, we know that the two locks have their own advantages and disadvantages. One cannot be considered better than the other. For example, optimistic locks are suitable for situations where there are fewer writes (more read scenarios), that is, when conflicts really rarely occur, this can save the overhead of the lock and increase the entire throughput of the system. However, if you write more, conflicts will usually occur frequently, which will lead to the upper-level application retry continuously, which will reduce performance. Therefore, it is generally more appropriate to use pessimistic locking of in the scenario of writing more.

fair lock vs non-fair lock

fair lock

The advantage of fair lock is that the thread waiting for the lock will not starve to death. The disadvantage is that the overall throughput efficiency is lower than the unfair lock. All threads in the waiting queue except the first thread will block. The overhead of the CPU to wake up the blocking thread is greater than that of the unfair lock.

Unfair lock

The advantage of unfair lock is that can reduce the overhead of invoking threads (for example, when new thread D comes in, just before thread A releases the lock, then D can directly acquire the lock without entering the blocking queue). , the overall has a high throughput efficiency of , because the thread has a chance to directly acquire the lock without blocking, and the CPU does not have to wake up all threads. The disadvantage is that threads in the waiting queue may starve to death, or wait for a long time to obtain the lock.

implementation

ReentrantLock provides a fair and unfair lock implementation. · Fair lock: ReentrantLockpairLock =new ReentrantLock(true). · Unfair lock: ReentrantLockpairLock =new ReentrantLock(false).

  • If the constructor does not pass parameters, the default is non-fair lock .

source code comparison

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

Through the comparison of source code in the figure above, we can clearly see that the only difference between the lock() method of fair lock and the non-fair lock is that fair lock has an additional restriction when obtaining the synchronous state: hasQueuedPredecessors().

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

Enter hasQueuedPredecessors() and you can see that this method mainly does one thing: mainly determines whether the current thread is in the first in the synchronization queue. Return true if yes, otherwise return false.

In summary, fair lock means that synchronizes the queue to realize multiple threads to acquire locks in the order in which the lock is applied, thereby achieving the fair feature. When adding an unfair lock, you do not consider waiting in line and try to acquire the lock directly, so you can apply for the lock first.

reentrant lock vs non-reentrant lock

reentrant lock may cause deadlock problems

First of all, ReentrantLock and NonReentrantLock both inherit the parent class AQS. Its parent class AQS maintains a synchronous state status to count the reentrant number , and the initial value of status is 0.

When a thread tries to acquire the lock, it can reenter the lock and try to obtain and update the status value first. If status == 0 means that no other thread is executing the synchronization code, set status to 1 and the current thread starts executing. If status != 0, it determines whether the current thread is the thread that acquired the lock. If so, status+1 is executed, and the current thread can acquire the lock again. non-reentable lock is that directly obtains and tries to update the current status value . If status != 0, it will cause its acquisition lock to fail and the current thread is blocked. When

is released, the reentrant lock can also first obtain the current status value, under the premise that the current thread is the thread holding the lock. If status-1 == 0, it means that all repeated lock acquisition operations of the current thread have been executed, and the thread will actually release the lock. Instead of reentrant locks, after determining that the current thread is the thread holding the lock, it directly sets status to 0 and releases the lock.

There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

lock elimination and lock coarse

blog.csdn.net/qq_26222859…

lock elimination

lock elimination is a lock optimization method that occurs at the compiler level.
Sometimes the code we write does not require locking at all, but performs locking operations.

lock elimination is Java virtual machine . During JIT compilation, by scanning the running context, locks that cannot be competed for shared resources can be removed. Through lock elimination, meaningless request lock time can be saved.

For example, append operation of stringbuffer class:

@Overridepublic synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}Copy code 

From the source code, it can be seen that the append method uses the synchronized keyword, which is thread-safe. But we may only use StringBuffer as a local variable inside the thread, for example:

public static String test(String str1, String str2) {StringBuffer sb = new StringBuffer();sb.append(str1);sb.append(str2);return sb.toString();}}Copy code 

At this time, different threads call this method and will create different stringbuffer objects, and there will be no synchronization problems such as lock competition. Therefore, the compiler will do optimization at this time to remove locks that cannot be competed for shared resources, which is lock elimination. The main basis for

lock removal comes from data support for escape analysis. If it is determined that in a piece of code, all data on the heap will not escape and be accessed by other threads, then they can be treated as data on the stack, thinking that they are private by the thread, so there is no need to perform synchronous locking.

lock coarse

public void doSomethingMethod(){synchronized(lock){//do something} //There are some codes in the middle of the two locking processes, but the execution speed is very fast synchronized(lock){//do other thing}}Copy the code 

These two codes that require synchronization operation, and some other work needs to be done, and these work will only take a very little time. Then we can put these working codes into the lock and merge the two synchronized code blocks into one to reduce the system performance consumption caused by multiple lock requests, synchronization, and release. The merged code is as follows:

public void doSomethingMethod(){//Coarse lock: integrate into a lock request, synchronization, and release synchronized(lock){//do something//do other work that does not require synchronization but can be completed quickly//do other thing}} Copy code 

hand-tearing answer session -- This is a split line

synchronized How to ensure visibility? Before the thread locks, clear the value of the shared variable in the working memory , so when using the shared variable, you need to re-read the latest value from the main memory. After the
  • thread is locked, other threads cannot obtain shared variables in main memory. Before unlocking the
  • thread, the latest value of the shared variable must be refreshed to the main memory . How to ensure orderliness in
  • synchronized? The code block synchronized by

    synchronized has exclusiveness and can only be owned by one thread at a time. Therefore, synchronized ensures that the code is executed by a single thread at the same time.

    Because of the existence of as-if-serial semantics, single-threaded programs can ensure that the final result is orderly, but there is no guarantee that instructions will not be rearranged.

    So the order that synchronized ensures is the orderliness of execution results, rather than the orderliness of preventing instruction reordering. How to achieve reentrance by

    synchronized?

    synchronized is a reentrant lock, that is, allowing a thread to request a critical resource that holds the object lock by itself twice. This situation is called a reentrant lock.

    synchronized When locking the object, there is an counter . It will record the number of times the thread acquires the lock. After executing the corresponding code block, the counter will be -1, until the counter is cleared and the lock is released.

    is reentrant. This is because the synchronized lock object has a counter, which will count +1 after the thread acquires the lock. When the thread completes execution, -1 until the lock is cleared and released.

    lock upgrade? Do you understand synchronized optimization? There is a structure in the header of

    Java object, called Mark Word mark field, which will change with the state of the lock.

    64-bit virtual machine Mark Word is 64bit, let's take a look at its state changes:

    There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

    Mark Word stores the object's own running data, such as hash code, GC generation age, lock status flag, biased timestamp (Epoch) , etc.

    • bias lock: In the absence of competition, the current thread pointer is stored in Mark Word, and CAS operations are not done.
    • lightweight lock: When there is no multi-thread competition, it is relatively heavyweight lock, reducing the performance consumption caused by operating system mutex. However, if there is lock competition, in addition to the overhead of the mutex itself, there is also an additional overhead of CAS operation.
    • spin lock: reduce unnecessary CPU context switching. When the lightweight lock is upgraded to a heavyweight lock, the spin lock method is used to roughen
    • lock: multiple continuous locking and unlocking operations are connected together to expand into a lock with a larger range.
    • lock elimination: When the virtual machine instant compiler is running, it requires synchronization on some code, but it is detected that there is no possibility of shared data competition to eliminate the lock.

    upgrade specific process

    first has no lock, no competition

    bias lock

    and then bias lock. To determine whether it can be biased, check whether the thread ID is the current thread, if yes, execute directly, without CAS

    , CAS competes for the lock, if successful, set the thread ID to yourself If the cancellation fails, the cancellation of the

    biased lock is upgraded to a lightweight lock

    1. biased lock will not be released (revoked). The cancellation will only be performed when other threads compete. Since the cancellation requires knowing the current thread stack state that holds the biased lock, it must be executed until safepoint. At this time, the thread (T) holding the biased lock has two situations: ‘2’ and ‘3’;
    2. undoing--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 14 Upgrade to lightweight lock . The current thread performs the lock acquisition step in the lightweight lock state------ When the state reaches the threshold value of 40, batch undo

    lightweight lock

    1. is performed to perform lock operation, jvm will determine whether the heavyweight lock has been used. If not, a piece of space will be drawn in the current thread stack frame as the lock record, and the lock object MarkWord will be copied into the lock record. After the copy of
    2. successfully, jvm uses CAS operation to update the object header MarkWord to a pointer to the lock record, and point the owner pointer in the lock record to the object header MarkWord. If successful, execute ‘3’, otherwise execute ‘4’
    3. update, then the current thread holds the object lock, and the object MarkWord lock flag is set to ‘00’, which means that the object is in a lightweight lock state.
    4. update failed. jvm first checks whether the object MarkWord points to the lock record in the current thread stack frame. If so, execute ‘5’, otherwise execute ‘6’
    5. indicates lock reentry; then add a lock record to the current thread stack frame to null, and point to the Mark Word lock object, which plays a reentry counter.
    6. means that the lock object has been seized by other threads, then is spin-waited for (default 10 times). If the number of waits reaches the threshold, the lock has not been obtained. Then is upgraded to a heavyweight lock

    Upgrade process:

    There are all kinds of trivial matters everywhere. In MySQL, we have talked about various trivial matters ->MySQL lock mechanism && transactions. Today, let’s take a look at the lock upgrade process in Java and the comparison between various locks. It is pessimistic and optimisti - DayDayNews

    This article is a cold noodles stir-fry. If there is any error, please correct

    Author: Melo_
    Link: https://juejin.cn/post/7160296578173894663

    technology Category Latest News