Super detailed analysis from synchronized to CAS and AQS


1. Synchronized keyword

Synchronized is an exclusive lock. When modifying a static method, the lock is a class object, such as Object.class. When modifying a non-static method, the object is locked, that is, this. When modifying a method block, the object in the brackets is locked. Each object has a lock and a waiting queue. The lock can only be held by one thread, and other threads that need the lock need to block and wait. After the lock is released, the object will take one from the queue and wake it up. Which thread to wake up is uncertain and fairness is not guaranteed.

Two, pessimistic lock and optimistic lock

Pessimistic locking
always assumes the worst case. Every time you go to get the data, you think that others will modify it, so you will lock it every time you get the data, so that others will block until they get the lock if they want to get the data ( Shared resources are only used by one thread at a time, other threads are blocked, and the resources are transferred to other threads after use). Many such locking mechanisms are used in traditional relational databases, such as row locks, table locks, etc., read locks, write locks, etc., all of which are locked before operations are performed. Exclusive locks such as synchronized and ReentrantLock in Java are the realization of pessimistic locking ideas.

Optimistic locking
always assumes the best situation. Every time you go to get the data, you think that others will not modify it, so it will not be locked. But when updating, you will judge whether others have updated the data during this period. You can useVersion number mechanism and CAS algorithmaccomplish. Optimistic locking is suitable for multi-read application types, which can improve throughput. Like the write_condition mechanism provided by the database, it is actually an optimistic locking provided. The atomic variable class under the java.util.concurrent.atomic package in Java is implemented using CAS, an implementation method of optimistic locking.

The usage scenarios of the two locks
From the introduction of the two locks above, we know that the two locks have their own advantages and disadvantages, and we cannot think that one is better than the other. Optimistic locks are suitable for situations where there are fewer writes (read more Scenarios), that is, when conflicts really rarely occur, this can save the overhead of locking and increase the overall throughput of the system. However, if there is a lot of writing, conflicts will often occur, which will cause the upper-layer application to perform retry continuously, which will actually reduce the performance. Therefore, pessimistic locking is more appropriate in the scenario of more writing.

3. Fair locks and unfair locks

Fair lock : multiple threads acquire locks in the order in which they apply for locks, and the threads will directly enter the queue to be queued, and they will always be the first in the queue to obtain the lock.

  • Advantages: All threads can get resources and will not starve to death in the queue.
  • Disadvantages: The throughput will drop a lot. Except for the first thread in the queue, other threads will be blocked, and the overhead of cpu waking up the blocked thread will be very high.

Unfair lock : When multiple threads acquire a lock, they will try to acquire it directly. If they cannot acquire it, they will enter the waiting queue. If they can acquire it, they will acquire the lock directly. It is worth noting that in the implementation of AQS , once the thread enters the queuing queue, even if it is an unfair lock, the thread has to queue obediently.

  • Advantages: It can reduce the overhead of CPU waking up threads, the overall throughput efficiency will be higher, and the CPU does not have to wake up all threads, which will reduce the number of waking up threads.
  • Disadvantages: You may have also discovered that this may cause the thread in the middle of the queue to fail to acquire the lock for a long time or fail to acquire the lock for a long time, resulting in starvation.

4. Reentrant locks and non-reentrant locks

Non-reentrant lock : Only judge whether the lock is locked or not, as long as the lock is locked, the thread that applies for the lock will be asked to wait. easy to implement

Reentrant lock : Not only judges whether the lock is locked, but also judges who locked the lock. When it is locked by itself, then he can still access critical resources again, and increase the number of locks by 1; release When locking, the number of locks is reduced by 1.

5. CAS

5.1. Operation model

image-20230130184011972

CAS is an abbreviation for compare and swap, that is, compare and exchange. It refers to an operating mechanism rather than a specific class or method. This operation is wrapped on the Java platform. In the Unsafe class, the calling code is as follows:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

It takes three parameters which are the memory location V, the old expected value A and the new value B. When operating, first read the value from the memory location, and then compare it with the expected value A. If equal, change the value of this memory location to the new value B, returning true. If it is not equal, it means that there is a conflict with other threads, then no change will be made and false will be returned.

This mechanism avoids concurrency conflicts without blocking other threads, and has much higher performance than exclusive locks. CAS is heavily used in Java's atomic classes and concurrent packages.

5.2. Retry mechanism (cyclic CAS)

1. CAS itself does not implement the processing mechanism after failure. It is only responsible for returning the Boolean value of success or failure, and the subsequent processing is handled by the caller. The most common way to deal with it is to retry.

2. When it actually fails, the original value has been modified. If you do not change the expected value, no matter how you compare it, it will fail. The new value also needs to be modified.

So the correct way is to use an infinite loop to perform the CAS operation. If it succeeds, it will end the loop and return. If it fails, it will read the value from the memory and calculate the new value again, and then call CAS.

5.3, the underlying implementation

CAS is mainly divided into three steps, read-compare-modify. The comparison is to detect whether there is a conflict. If other threads can modify this value after detecting no conflict, then CAS still cannot guarantee the correctness. So the most important thing is to ensure the atomicity of the comparison-modification two-step operation.

The bottom layer of CAS is completed by calling the cmpxchg of the CPU instruction set, which is the compare and exchange instruction in the x86 and Intel architectures. In the case of multi-core, this instruction cannot guarantee atomicity, and it needs to be preceded by a lock instruction. The lock instruction can ensure that a CPU core exclusively occupies a memory area during operation. So how is this possible?

In processors, there are generally two ways to achieve the above effects: bus locks and cache locks. In the structure of a multi-core processor, the CPU core cannot directly access the memory, but accesses it uniformly through a bus. The bus lock is to lock the bus so that other cores cannot access the memory. This method is too expensive and will cause other cores to stop working. The cache lock does not lock the bus, but only locks a certain part of the memory area. When a CPU core reads data from a memory area into its own cache, it locks the memory area corresponding to the cache. During the lock period, other cores cannot operate this memory area.

This is how CAS achieves the atomicity of compare and exchange operations. It is worth noting that CAS only guarantees the atomicity of operations, but does not guarantee the visibility of variables, so variables need to be added with the volatile keyword.

image-20230130185255141

5.4. ABA problem

As mentioned above, CAS guarantees the atomicity of comparison and exchange. But during the period from reading to starting comparison, other cores can still modify this value. If the core changes A to B, CAS can judge it. But if the core modifies A to B and back to A. Then CAS will think that this value has not been changed, and continue to operate. This is inconsistent with the actual situation. the solution isadd a version number

Six, reentrant lock ReentrantLock

ReentrantLock uses code to achieve the same semantics as synchronized, including reentrancy, guaranteed memory visibility, and solving race conditions. Compared with synchronized, it has the following advantages:

  • Support for acquiring locks in a non-blocking manner
  • Can respond to interrupts
  • time limit
  • Supports fair and unfair locks

The basic usage is as follows:

public class Counter {
    
    
    private final Lock lock = new ReentrantLock();
    private volatile int count;

    public void incr() {
    
    
        lock.lock();
        try {
    
    
            count++;
        } finally {
    
    
            lock.unlock();
        }
    }

    public int getCount() {
    
    
        return count;
    }
}

There are two internal classes in ReentrantLock, namely FairSync and NoFairSync, which correspond to fair locks and unfair locks. They both inherit from Sync. Sync is inherited from AQS.

7. AQS

image-20230130184126542

The full name of AQS is AbstractQueuedSynchronizer. There are two important members in AQS:

  • Member variable state. It is used to represent the current state of the lock, modified with volatile to ensure memory consistency. At the same time, all state operations are performed using CAS. A state of 0 means that no thread holds the lock. After the thread holds the lock, the state will be incremented by 1, and it will be decremented by 1 when it is released. If you hold and release multiple times, you will add and subtract multiple times.
  • There is also a doubly linked list. Except for the head node, each node of the linked list records thread information, representing a waiting thread. This is a FIFO linked list.

Let's take a look at the principle of AQS with the code of ReentrantLock unfair lock.

7.1, request lock

There are three possibilities when requesting a lock:

  1. If no thread holds the lock, the request is successful and the current thread directly acquires the lock.
  2. If the current thread already holds the lock, use CAS to add 1 to the state value, indicating that it has applied for the lock again, and subtract 1 when releasing the lock. This is the realization of reentrancy.
  3. If the lock is held by another thread, add yourself to the waiting queue.

7.2. Create a Node node and join the linked list

If there is no competition for the lock, it will enter the waiting queue at this time. The queue has a head node by default and does not contain thread information. In case 3 above, addWaiter will create a Node and add it to the end of the linked list, and the Node holds the reference of the current thread. At the same time, there is a member variable waitStatus, which indicates the waiting state of the thread, and the initial value is 0. We also need to focus on two values:

  • CANCELLED, the value is 1, indicating the cancel status, that is, I don't want this lock anymore, please remove me.
  • SINGAL, the value is -1, which means that the next node is pending and waiting. Note that it is the next node, not the current node.

7.3. Hang up and wait

  • If the previous node of this node is the head node, try to acquire the lock again, remove it and return if acquired. If you can’t get it, go to the next step;
  • Judge the waitStatus of the previous node, if it is SINGAL, return true, and call LockSupport.park() to suspend the thread;
  • If it is CANCELLED, remove the previous node;
  • If it is another value, mark the waitStatus of the previous node as SINGAL and enter the next cycle.

7.4, release the lock

  • Call tryRelease, which is implemented by subclasses. The implementation is very simple, if the current thread is the thread holding the lock, the state will be decremented by 1. If the state is greater than 0 after the subtraction, it means that the current thread still holds the lock and returns false. If it is equal to 0, it means that there is no thread holding the lock, return true, and go to the next step;
  • If the waitStatus of the head node is not equal to 0, call LockSupport.unpark() to wake up the next node. The next node of the head node is the first thread in the waiting queue, which reflects the first-in-first-out feature of AQS. In addition, even if it is an unfair lock, after entering the queue, it still has to be in order.

Eight, reentrant read-write lock ReentrantReadWriteLock

Read-write lock mechanism

After understanding ReentrantLock and AQS, it is very simple to understand read-write locks. A read-write lock has a read lock and a write lock, which correspond to read operations and lock operations respectively. The characteristics of the lock are as follows:

  • Only one thread can acquire the write lock. When acquiring a write lock, only if no thread holds any lock can the acquisition be successful;
  • If a thread is holding a write lock, no other thread can acquire any lock;
  • Multiple threads can acquire read locks while no thread holds a write lock.

The characteristics of the lock above ensure that it can be read concurrently, which greatly improves the efficiency and is very useful in actual development. So how is it realized in detail?

Realization principle

Although there are two locks for read-write locks, there is actually only one waiting queue.

  • When acquiring a write lock, ensure that no thread holds the lock;
  • After the write lock is released, the first thread in the queue will be awakened, which may be a read lock and a write lock;
  • When acquiring a read lock, first determine whether the write lock is held, if not, the acquisition can be successful;
  • After successfully acquiring the read lock, the threads waiting for the read lock in the queue will be woken up one by one to know the position of the thread waiting for the write lock;
  • When releasing the read lock, check the number of read locks, if it is 0, wake up the next thread in the queue, otherwise do not operate.

Reference article : From synchronized to CAS and AQS - Thoroughly understand various concurrency locks in Java

Guess you like

Origin blog.csdn.net/weixin_54040016/article/details/128807318