• With the advent of multiple processes and multiple threads, competition for shared resources (devices, data, etc.) often results in random, unordered use of resources
  • For example, one thread wants to print “I am fine” on the console, and as soon as it writes “I am”, another thread preempts the console and prints “naughty”, resulting in “I am naughty”; What can we do about the preemption of resources? Of course, not cold salad, can use the lock for synchronization management, so that resources during the lock, other threads can not preempt use

1 Lock classification

  • Pessimistic locking
    • Pessimistic locking, which assumes that data will be preempted every time it is requested (pessimistic thinking); So each time the data is manipulated, the lock must be placed first, and other threads must wait to acquire the lock while modifying the data. Synchronized is a pessimistic lock for situations where you write more than you read
  • Optimistic locking
    • When requesting data, the change was felt unpreempted. When the data is really updated, it can judge whether others have modified it during this period (read out a version number or update timestamp in advance, and judge whether it has changed when updating, and no modification is made during the period if it has not changed); Unlike pessimistic locks, the interim data is allowed to be modified by other threads
  • spinlocks
    • In a word, magic turns around. When an attempt is made to lock a resource and another line locks it first, instead of blocking and waiting, the lock is repeated
    • In cases where locks are often held for a short period of time, thread blocking and suspension cause frequent CPU context switches, which can be resolved by spin-locking. But it takes up CPU idling during spin, so it is not suitable for scenarios where the lock is held for a long time

2 Synchronized Underlying principles

  • What does the bytecode look like after compilation using synchronized
public class Test {
    public static void main(String[] args){
        synchronized(Test.class){
            System.out.println("hello");
        }
 } } Copy the code

Intercept part of the bytecode as follows

    4: monitorenter
    5: getstatic    #9    // Field java/lang/System.out:Ljava/io/PrintStream; 
    8: ldc           #15   // String hello
    10: invokevirtual #17  // Method java/io/PrintStream.println:(Ljava/lang/String;) V
    13: aload_1
 14: monitorexit Copy the code

The bytecode appears with the instructions 4: monitorenter and 14: monitorexit; Literally, watch in, watch out. This can be interpreted as locking before the block executes and unlocking when it exits synchronization

  • So what are monitorenter and Monitorexit doing behind our backs?
  • When monitorenter is executed, the thread associates an ObjectMonitor object with the lock object
objectMonitor.cpp
  ObjectMonitor(a) {
    _header       = NULL;
    _count        = 0; Used to record the number of threads that acquired the lock    _waiters      = 0. _recursions = 0; \\ Number of lock reentries _object = NULL;  _owner = NULL; The thread currently holding ObjectMonitor _WaitSet = NULL; The thread waiting queue after the \\wait() method is called _WaitSetLock = 0 ;  _Responsible = NULL ;  _succ = NULL ;  _cxq = NULL; \\ Block wait queue FreeNext = NULL ;  _EntryList = NULL; \synchronized queue of incoming threads _SpinFreq = 0 ;  _SpinClock = 0; \\ Spin calculation OwnerIsThread = 0 ;  } Copy the code
  • Each thread has two lists of ObjectMonitor objects, the free and used lists. If the free list is currently empty, the thread requests an ObjectMonitor to the global Global List
  • The owner, WaitSet, Cxq, and EntryList attributes of the ObjectMonitor are key. The queue elements of WaitSet, Cxq, and EntryList are objects after wrapping threads -ObjectWaiter; The thread that acquires the owner is the thread that acquires the lock
  • Monitorenter The corresponding execution method
void ATTR ObjectMonitor::enter(TRAPS)  {
.    // Acquire the lock: cmpxchg_ptr atomic operation that attempts to replace _owner with itself and returns the old value
    cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL);. // Obtain the lock repeatedly, incremented by 1, return  if (cur == Self) {  _recursions ++ ;  return ;  }  // Obtain the lock condition for the first time  if (Self->is_lock_owned ((address)cur)) {  assert (_recursions == 0."internal state error");  _recursions = 1 ;  _owner = Self ;  OwnerIsThread = 1 ;  return ;  } . // Try to spin the lock  if (Knob_SpinEarly && TrySpin (Self) > 0) { .Copy the code
  • Monitorexit Corresponds to the executormethodvoid ATTR ObjectMonitor::exit(TRAPS)...The code is too long, so I won’t post it. The number of recursions decreased by 1, count decreased by 1, or set owner to NULL if the thread is no longer holding the owner(non-reentrant locking), and then wake up the THREAD in the Cxq queue

conclusion

  • When a thread encounters synchronized, it enters the EntryList queue, attempts to set the owner variable to the current thread, and incrementing the count in monitor by one to acquire the lock. Otherwise, byTry to spin the lock a certain number of timesIf the failure occurs, the Cxq queue blocks and waits
  • The owner variable is restored to NULL, and count is decrement by 1, so that other threads can enter the lock
  • Synchronized is similar in principle. Instead of using the monitor directive, we use ACC_SYNCHRONIZED to identify the synchronization of methods
    public synchronized void lock(a){
        System.out.println("world");
    }
.  public synchronized void lock(a);
 descriptor: ()V  flags: (0x0029) ACC_PUBLIC, ACC_SYNCHRONIZED  Code:  stack=2, locals=0, args_size=0  0: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;  3: ldc #26 // String world  5: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/String;) V  Copy the code
  • Synchronized is a reentrant, but not a fair lock, because the entryList thread spins and attempts to lock it first, rather than joining the CXQ queue, which is unfair

3 Principles of the Wait and notify methods of an Object

  • Wait, notify must be called by the thread holding the current Object lock Monitor (Object lock represents ObjectMonitor/Monitor, lock Object represents Object)
  • As mentioned above, when a sychronized Object is locked and a wait is called, a waitSet is added to the queue. The element Object of the waitSet is ObjectWaiter
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;  ParkEvent * _event;  volatile int _notified ;  volatile TStates TState ;  Sorted _Sorted ; // List placement disposition  bool _active ; // Contention monitoring is enabled  public:  ObjectWaiter(Thread* thread);  void wait_reenter_begin(ObjectMonitor *mon);  void wait_reenter_end(ObjectMonitor *mon); }; Copy the code

When the wait() method of the object lock is called, the thread is wrapped into ObjectWaiter and suspended using the park method

//objectMonitor.cpp
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS){
.    // Threads are encapsulated as ObjectWaiter objects
    ObjectWaiter node(Self);
 node.TState = ObjectWaiter::TS_WAIT ; . When a thread does join the WaitSet, suspend it using the park method  if (node._notified == 0) {  if (millis <= 0) {  Self->_ParkEvent->park () ;  } else {  ret = Self->_ParkEvent->park (millis) ;  }  }  Copy the code

When object locks use notify()

  • If waitSet is empty, it is returned directly
  • WaitSet is not empty Get an ObjectWaiter from waitSet, then add it to EntryList or pass it depending on the PolicyAtomic::cmpxchg_ptrInstruction spin operation addedCXQ queueOr just unpark the wake-up call
void ObjectMonitor::notify(TRAPS){
    CHECK_OWNER();
    // If waitSet is empty, return it directly
    if (_WaitSet == NULL) {
        TEVENT (Empty-Notify) ;
 return ;  } . // Get the first ObjectWaiter in the _WaitSet list with DequeueWaiter  Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify"); ObjectWaiter * iterator = DequeueWaiter() ;  if(iterator ! =NULL) { . if (Policy == 2) { // prepend to cxq  // 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 ;  }  }  }  } Copy the code
  • The notifyAll method of Object correspondsvoidObjectMonitor::notifyAll(TRAPS), the process is similar to notify. However, the for loop will pull out the ObjectWaiter node of the WaitSet and wake up all the threads in turn

4 JVM optimization of synchronized

  • Let’s first introduce the structure of JAVA object headers on 32-bit JVMS

  • Biased locking

    • When unlocked, the lock flag is 01, containing the hash, age generation, and bias lock flag bit (0)
    • When biased locking is applied, the hash value and some unused memory are converted to the thread information of the owner of the lock and the timestamp epoch of the lock. When the lock flag bit is unchanged, the bias lock flag is changed to 1
    • When locking, first judge whether the current thread ID is consistent with the thread ID of MarkWord, and then execute the synchronization code. If not, check whether the bias flag is biased; if not, CAS is used to lock; Non-biased CAS locking failure and biased locking can cause biased locks to inflate into lightweight locks, or re-bias
    • A biased lock is released only when another thread is competing for a biased lock. A thread does not actively release a biased lock
  • Lightweight lock

    • When multiple threads are competing, the bias lock becomes a lightweight lock with a lock flag of 00
    • The thread that acquired the lock will first undo the bias lock (at the safe point) and create a LockRecord in the stack frame. The MarkWord of the object is copied to the newly created LockRecord. Then the CAS attempts to point the owner of the record to the lock object. Then point the MarkWord of the lock object to the lock, and the lock is successfully added
    • If the CAS fails to lock, the thread willSpin a certain number of times to lock, and then fail to upgrade to a heavyweight lock
  • Heavyweight lock

    • Heavyweight locking is the locking mechanism introduced above by synchronized using Monitor
    • The lock continues to grow into a heavyweight lock, also a mutex, with the lock flag bit 10, and the rest of MarkWord is replaced with a pointer to the object lock Monitor
  • spinlocks

    • Reduce unnecessary CPU context switches; Spin locking is used when a lightweight lock is upgraded to a heavyweight lock
  • Lock coarsening

    • Multiple locking operations are also costly within the JVM, and if multiple locks can be combined into a single lock, unnecessary overhead can be reduced
Test.class
// The compiler will consider merging the two locks
public void test(a){
    synchronized(this) {        System.out.println("hello");   
 }  synchronized(this) { System.out.println("world");  } } Copy the code
  • Lock elimination
    • Remove unnecessary locking operations. If the variable is a stack variable that belongs to only one thread, it is safe to lock it or not, and the compiler will attempt to remove the lock
    • Enabling lock elimination needs to be set on JVM parameters-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
// Synchronized is added to the append operation of the StringBuffer,
// But buf is safe without a lock. The compiler removes the lock
public void test(a) {
    StringBuffer buf = new StringBuffer();
    buf.append("hello").append("world");
} Copy the code
  • Other lock optimizations
    • Segmental locking, segmental locking is not an actual lock, but an idea; ConcurrentHashMap is a best practice for learning segmenting locking. The main thing is to split the large object into small objects, and then the lock operation of the large object becomes the lock of the small object, increasing the degree of parallelism

5 Underlying CAS principles

  • involatile int i = 0; i++Volatile reads and writes are atomically synchronized, but i++ does not guarantee synchronization.
  • You can use synchronized; There is also CAS(compare and exchange), which uses the idea of synchronization with optimistic locking, first judging if a shared variable has changed and then updating if not. Let’s take a look at the out-of-sync version of CAS
int expectedValue = 1;
public boolean compareAndSet(int newValue) {
    if(expectedValue == 1) {        expectedValue = newValue;
        return ture;
 }  return false; } Copy the code

There is a synchronous version of the CAS solution available in the JDK that uses the underlying methods of unsafe.java

//UnSafe.java
    @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset, int expected, int x).    @HotSpotIntrinsicCandidate
 public final native int compareAndExchangeInt(Object o, long offset, int expected, int x).Copy the code

Let’s look at the local method, compareAndSwapInt in unsafe.cpp

//unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
 return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END Copy the code

On Linux x86, the Atomic:: CMPXCHG method is implemented as follows

/ * *1 __asm__ indicates the beginning of assembly;2 volatile prohibits compiler optimization. // Disable reordering3 LOCK_IF_MP is an inline function,Depending on whether the current system is a multi-core processor,Determines whether to add a lock prefix // memory barrier for CMPXCHG directives* / inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {  int mp = os::is_MP();  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"  : "=a" (exchange_value)  : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)  : "cc"."memory");  return exchange_value; } Copy the code

At this point, it can be summarized that the CAS mechanism provided by the JDK, at the assembly level, disables the optimization of instructions on both sides of a variable, then compares and updates the value of the variable using the CMPXCHG directive (atomization), and uses lock (cache lock, MESI) if it is multi-core.

6 CAS synchronization is faulty

  • ABA problem
    • Thread X is about to change the value of the variable from A to B, while thread Y changes the value of the variable from A to C, and then to A again. Finally thread X detects that the variable value is A and replaces it with B. But actually, A is no longer the same as A
    • The solution is to make the variable unique. The value can be appended to a version number, or a timestamp. A1->B2->A3 = A1-> A3 = A1->B2->A3
  • Atomic operations that guarantee only one shared variable
    • Only one atomic operation of a shared variable is guaranteed. When synchronizing multiple shared variables, the cyclic CAS is an atomic operation that cannot be guaranteed

7 Synchronization locking based on volatile + CAS

  • CAS can only synchronize changes to one variable, so how do we use it to lock blocks of code?
  • Let’s talk about the elements of implementing locks
    • 1 Only one thread can execute a synchronized code block at a time
    • The locking action happens-before to synchronize the action in the code block, and the action in the code block happens-before to unlock the action
    • 3. After the end of the synchronized code block, variables changed by it are visible relative to other threads (memory visibility).
  • Element 1: You can take advantage of the atomicity of CAS so that only one thread can successfully manipulate variables at any one time
    • Consider that the shared variable of CAS operation is a synchronized state variable associated with a code block. Before synchronization, CAS updates the state variable to the locked state, and after synchronization, CAS updates the state variable to the unlocked state
    • If a second thread locks during this period, the state variable is found to be locked and the synchronization block is abandoned
  • Element 2: Use volatile to modify state variables to disallow order reordering
    • Volatile guarantees that an action in synchronized code happens before an action to unlock, and a lock happens before an action in a block of code
  • Element 3: Once again, volatile variables insert memory barriers before and after writing instructions
    • The dirty data in the synchronized block will be updated and visible to each thread before the state variable modified by volatile is made unlocked by CAS
/ / pseudo code
volatile state = 0 ;   // 0- no lock 1- lock; Volatile disallows reordering of instructions, adding memory barriers
.if(cas(state, 0 , 1)) {// 1 Lock successfully, only one thread can successfully lock
.// 2 Synchronize the code block
 cas(state, 1.0); // Operation 2 is visible when unlocked by 3 }  Copy the code

8 LockSupport

  • The LockSupport class is based on the Unsafe class and is provided by the JDK for threading operations. The LockSupport class is designed to suspend and wake up threads. Unpark.park, the unpark operation calls the parker agent variable on the current thread. Parker code
JavaThread* thread=JavaThread::thread_from_jni_environment(env);
.thread->parker()->park(isAbsolute ! =0, time);
Copy the code
class PlatformParker : public CHeapObj {
  protected:
    // The variable type is mutually exclusive
    pthread_mutex_t _mutex [1];   // Condition variable type
 pthread_cond_t _cond [1];.}  class Parker : public os::PlatformParker { private:  volatile int _counter ; .public:  void park(bool isAbsolute, jlong time);  void unpark(a); .} Copy the code
  • In Linux, the POSIX thread library pThread mutex and condition are used to suspend and wake up threads
  • Note: When park, the counter variable is set to 0, and when unpark, this variable is set to 1
  • When unpark and park are executed differently, the states of counter and cond change as follows
    • After the first park unpark; Park: counter is unchanged, but a cond is set; Unpark: incrementing counter by 1, checking for cond presence, reducing counter to 0
    • First unpark, then park; Park: counter is changed to 1, but cond is not set; Unpark: counter reduced to 0(thread does not hang because of park)
    • First unpark several times; Counter is only set to 1

9 Difference between locksupport. park and object. wait

  • Both methods have the ability to have suspended threads
  • After object. wait, the thread must wait for object. notify to wake up
  • LockSupport can unpark the thread first and wait for the thread to execute locksupport. park is not suspended and can continue execution
  • Note that even if the thread unpark multiple times; Also only allow the thread the first time the park is not suspended

10 AbstractQueuedSynchronizer(AQS)

  • AQS is actually based on volatile+ CAS implementation lock template; If the thread needs to block and wait, then use LockSupport to suspend and wake up the thread
//AbstractQueuedSynchronizer.java
public class AbstractQueuedSynchronizer{
    // Thread node
    static final class Node {
. volatile Node prev;  volatile Node next;  volatile Thread thread; . } . //head Wait for the first and last nodes in the queue  private transient volatile Node head;  private transient volatile Node tail;  // The synchronization state  private volatile int state; . // Provide the CAS operation, the state specific changes by the subclass implementation  protected final boolean compareAndSetState(int expect, int update) {  return STATE.compareAndSet(this, expect, update);  } } Copy the code
  • AQS maintains a synchronous queue internally, and the element is a Node that wraps threads
  • The first node in the synchronization queue is the node that acquired the lock. When it releases the lock, it wakes up the subsequent node, and when the subsequent node acquired the lock, it sets itself as the first node
public final void acquire(int arg) {
        if(! tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
Copy the code
  • The thread will try to acquire the lock first, and if it fails, it will be encapsulated as Node and CAS will be added to the tail of the synchronization queue. When it joins the end of the synchronization queue, it determines whether the precursor node is a head node and tries to lock it (it may just release the lock), otherwise the thread will block and wait

ConditionObject () ConditionObject () ConditionObject () ConditionObject () ConditionObject () ConditionObject () ConditionObject () ConditionObject () ConditionObject () ConditionObject () ConditionObject

//AbstractQueuedSynchronizer.java
public class ConditionObject implements Condition.java.io.Serializable {
    // Conditional queue; Node reuse Node defined in AQS
    private transient Node firstWaiter;
    private transient Node lastWaiter;
.Copy the code
  • Each Condition object contains a FIFO Condition queue of Node elements inside
  • When a thread calls condition.await (), it will release the lock, construct the Node to join the Condition queue and enter the wait state
/ / similar Object. Wait
public final void await(a) throws InterruptedException{
.    Node node = addConditionWaiter(); // Construct the Node and join the conditional queue
    int savedState = fullyRelease(node);
 int interruptMode = 0;  while(! isOnSyncQueue(node)) { // Suspend the thread  LockSupport.park(this);  if((interruptMode = checkInterruptWhileWaiting(node)) ! =0)  break;  }  // Notify After the thread wakes up, it joins the synchronization queue and continues to compete for locks  if(acquireQueued(node, savedState) && interruptMode ! = THROW_IE) interruptMode = REINTERRUPT; Copy the code
  • When condition.signal is called, the first node of the Condition queue is fetched, moved to the synchronization queue and the thread in the node is woken up with LockSupport. Then continue with the pre-wait state and call acquireQueued(Node, savedState) to compete for the synchronization state
    / / similar Object. Notify
    private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
 first.nextWaiter = null;  } while(! transferForSignal(first) &&(first = firstWaiter) ! =null);  } Copy the code
  • Volatile + CAS ensures code synchronization and visibility, while AQS encapsulates the logic that threads block and wait to suspend, unlocking the logic that wakes up other threads. AQS subclass only needs to judge whether the lock can be obtained and whether the lock can be released successfully according to the state variable
  • Inheriting AQS requires selective overwriting of the following interfaces
protected boolean tryAcquire(int arg);// Try exclusive locking
protected boolean tryRelease(int arg);// Release the lock corresponding to tryAcquire
protected int tryAcquireShared(int arg);// Try sharing locks
protected boolean tryReleaseShared(int arg);TryAcquireShared releases the lock
protected boolean isHeldExclusively(a);// If the thread is monopolizing resources, it needs to be implemented only when condition is used
Copy the code

11 ReentrantLock

  • Already realized the Lock interface, and the inner class Sync (Sync AbstractQueuedSynchronizer inheritance) to realize synchronous operation
  • ReentrantLock inner class Sync
abstract static class Sync extends AbstractQueuedSynchronizer{
.    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
 if (c == 0) {  // The CAS status is locked directly, which is not fair operation  if (compareAndSetState(0, acquires)) {  setExclusiveOwnerThread(current);  return true;  }  } . // Overwrite tryRelease  protected final boolean tryRelease(int releases) {  c = state - releases; Change the synchronization status . // Modify the state variables modified by volatile  setState(c);  return free;  } } Copy the code
  • The Sync subclasses NonfairSync and FairSync both override tryAcquire
  • The tryAcquire method of NonfairSync calls the parent nonfairTryAcquire method, and FairSync overwrites the tryAcquire logic itself. Hasqueuedhouseholds () is called to determine whether a queued Node exists, and if so, returns false (false would cause the current thread to queue for the lock).
    static final class NonfairSync extends Sync {
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
. static final class FairSync extends Sync {  protected final boolean tryAcquire(int acquires) {  final Thread current = Thread.currentThread();  int c = getState();  if (c == 0) {  if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) {  setExclusiveOwnerThread(current);  return true;  }  } .Copy the code

12 Example demo of AQS exclusive locks

public class TwinsLock implements Lock {

    private final Sync sync = new Sync(2);
    @Override
    public void lockInterruptibly(a) throws InterruptedException {  throw new RuntimeException(""); }
 @Override  public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {throw new RuntimeException(""); } @Override  public Condition newCondition(a) { return sync.newCondition(); }  @Override  public void lock(a) { sync.acquireShared(1); }  @Override  public void unlock(a) { sync.releaseShared(1); }} @Override  public boolean tryLock(a) { return sync.tryAcquireShared(1) > -1; } } Copy the code

Let’s look at the code for Sync

class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("count must large than zero");
            }
 setState(count);  }  @Override  public int tryAcquireShared(int reduceCount) {  for(; ;) { int current = getState();  int newCount = current - reduceCount;  if (newCount < 0 || compareAndSetState(current, newCount)) {  return newCount;  }  }  }  @Override  public boolean tryReleaseShared(int returnCount) {  for(; ;) { int current = getState();  int newCount = current + returnCount;  if (compareAndSetState(current, newCount)) {  return true;  }  }  }  public Condition newCondition(a) {  return new AbstractQueuedSynchronizer.ConditionObject();  }  } Copy the code

Can locking prevent thread loops

  • The answer is not necessarily; It can be done for a single resource; For example, thread A holds resource X and waits for resource Y, while thread B holds resource Y and waits for resource X
  • With locks, you can control resources and states, but you also need to prevent deadlocks by breaking one of the four conditions that create deadlocks
  • 1 The resource cannot be used by two or more users
  • 2 The user holds resources and waits for other resources
  • 3 Resources cannot be preempted
  • Four users form a cycle of waiting for each other’s resources

14 ThreadLocal specifies whether to ensure resource synchronization

  • When a variable is declared using a ThreadLocal, the ThreadLocal provides a separate copy of the variable for each thread that uses the variable, and each thread can change its own copy independently without affecting the corresponding copies of other threads
  • From the above concept, ThreadLocal does not guarantee the synchronization of variables, but only assigns a copy of a variable to each thread

Follow the public number, we communicate together

Refer to the article

  • objectMonitor.cpp
  • How Moniter works
  • JVM source analysis of Object. Wait /notify implementation
  • Java object headers and locks
  • Basic use and principle of park and unpark in LockSupport
  • Java concurrency Condition