Java multi-threading series V (common lock strategy + CAS + synchronized principle)


1. Optimistic lock & pessimistic lock

The implementer of the lock predicts the probability of the next lock conflict to decide what to do next. So it is divided into two major "schools":

Optimistic locking : Optimistic locking is an optimistic idea. It predicts that the probability of subsequent conflicts is unlikely or that conflicts will not occur between multiple threads. Therefore, it does not lock when accessing data, but records when reading data. A version number. If the version numbers are inconsistent when updating data, it is considered that the data has been modified by other threads, and the update needs to be retried (use the version number or timestamp to identify whether the current data access conflicts). For example, the AtomicInteger class in Java uses an optimistic locking mechanism in its internal implementation.

Pessimistic lock : Pessimistic lock is a pessimistic idea. It predicts that the probability of conflict will be relatively high or that conflicts will occur between multiple threads. Therefore, it will lock the data when accessing it to prevent other threads from accessing it at the same time.

Synchronized initially uses an optimistic locking strategy. When lock competition is found to be frequent, it will automatically switch to a pessimistic locking strategy.

Generally speaking, pessimistic locks generally have to do more work and are less efficient. Optimistic locking will do less work and be more efficient.

2. Heavyweight locks & lightweight locks

Knowledge supplement : The core feature of locks is "atomicity". This mechanism is traced back to the fact that it is provided by hardware devices such as CPUs. The CPU provides "atomic operation instructions", and the operating system implements mutexa mutex lock based on the atomic instructions of the CPU. The JVM implements keywords and classes such as synchronized and ReentrantLock based on the mutex lock provided by the operating system.

提供原子操作指令
提供mutex互斥锁
提供synchronized等关键字
CPU
操作系统
JVM
Java代码

Heavyweight lock : locking and unlocking, the process is more efficient. The locking mechanism relies heavily on the mutex provided by the OS. It involves a large number of kernel mode and user mode switching, which can easily cause thread scheduling. This operation is relatively expensive.

Lightweight lock : locking and unlocking, the process is more efficient. The locking mechanism should not use mutex as much as possible, but try to use user mode code to complete it. If you really can't figure it out, use mutex again. It involves a small amount of kernel mode user mode switching, which is not easy to cause thread scheduling.

In general: an optimistic lock is likely to be a lightweight lock (not absolute), and a pessimistic lock is likely to be a heavyweight lock (not absolute)

It should be noted that 用户态the time cost of is relatively controllable, while 内核态the time cost of is less controllable.

Programs in user mode can only access data and code in user space and cannot directly access data and resources in kernel space, while programs in kernel mode can access and operate all system resources.

Switching between user mode and kernel mode needs to be achieved through system calls, that is, falling from user mode to kernel mode, completing some privileged operations by executing kernel code, and returning the results to user mode. This switching process requires a lot of time and resources. Therefore, reducing the number of switches between user mode and kernel mode is an important way to optimize system performance.

3. Spin lock & suspend wait lock

Spin and blocking:
Spin is implemented for busy waiting and to get the lock as quickly as possible. Blocking and waiting means giving up the right to use the current CPU. Even if it is awakened later, there is no guarantee that the thread will get the CPU again as soon as possible.

Spin lock : It is a typical implementation of lightweight lock (usually stored in user mode and does not need to go through kernel mode).
A spin lock means that the current thread repeatedly checks the lock flag. If it is found that the flag has been set by another thread, the thread will continue to check in a loop until it obtains the lock. Spin lock is suitable for situations where the access to the shared data area is short and the competition intensity is not high, because spin waiting does not really release the CPU for other threads to use, but always occupies the CPU for loop checking, so if the spin waiting time is too long , which will waste CPU resources and affect system performance.

Suspended waiting lock (also called blocking lock) : It is a typical implementation of heavyweight lock. (Suspension waiting is usually implemented through the kernel mechanism)
Suspension waiting lock means that when a thread requests a lock, if it is found that the lock is already held by another thread, the thread will be suspended and wait until the lock is released. . During the process of suspending and waiting for the lock, the thread will enter the sleep state and release CPU resources for other threads to use. When the thread holding the lock releases the lock, the waiting thread will be awakened and request the lock again (the lock may not be acquired immediately, and the lock competition needs to be re-competed).

Generally speaking, spin locks are suitable for short-term access to the shared data area and low competition intensity to avoid the overhead caused by thread context switching; while hang-wait locks are suitable for long-term access to the shared data area and high competition intensity. In this case, the CPU resources can be effectively utilized and the idle time of the CPU can be reduced.

4. Mutex lock & read-write lock

Mutex : It is a mechanism used in multi-threaded programming to prevent two or more threads from accessing shared resources at the same time. By surrounding what multiple threads want to access with a lock 代码区域, only one thread can hold the lock, and other threads must wait for that thread to release the lock before they can continue execution. Mutex locks are usually used to protect single-threaded access to shared resources and ensure program efficiency while ensuring data correctness.

ReadWrite Lock : It is a more advanced synchronization mechanism that allows multiple threads to read shared resources at the same time, but requires the protection of a mutex lock when writing to shared resources.

The implementation of read-write lock is that when reading shared resources, multiple threads can occupy the read lock at the same time; when writing to shared resources, only one thread is allowed to occupy the write lock, and other threads must wait for it to release the write lock. Get the lock. The usage scenarios of read-write locks are generally读操作频繁,但写操作比较少的场景,如数据库、文件系统等。

  1. There is no mutual exclusion between read locking and read locking.
  2. Write locking and write locking are mutually exclusive.
  3. There is mutual exclusion between read locking and write locking.

The Java standard library provides the ReentrantReadWriteLock class, which implements read-write locks:

  • The ReentrantReadWriteLock.ReadLock class represents a read lock. This object provides lock/unlock methods for locking and unlocking.
  • The ReentrantReadWriteLock.WriteLock class represents a write lock. This object also provides lock/unlock methods for locking and unlocking.

5. Reentrant locks & non-reentrant locks

Reentrant lock : A reentrant lock literally means "a lock that can be re-entered", which allows the same thread to acquire the same lock multiple times without being blocked. This kind of lock can avoid the occurrence of deadlock state when the same thread acquires the same lock, and at the same time ensures the efficiency and correctness of the code.

For example, if there is a locking operation in a recursive function, will the lock block itself during the recursive process? If not, then the lock is a reentrant lock

ReentrantLock is a common implementation of reentrant locks. It uses a counter to track the number of times the lock is held. Whenever a thread acquires a lock, the counter is incremented by 1, and when the lock is released, the counter is decremented by 1.

Non-reentrant lock : means that after a thread acquires the lock, it will be blocked when requesting to acquire the lock again until the lock is released. This kind of lock usually leads to a deadlock situation, because if a thread has acquired the lock and expects to continue to acquire the lock, it will wait for its own lock to be released.

Note : All ready-made Lock implementation classes provided by JDK, including synchronized keyword locks, are reentrant.

6. Fair lock & unfair lock

Agreement : If the lock is followed on a first-come, first-served basis, it is a fair lock. If it is not followed, it is an unfair lock.

Note : The system's thread scheduling is random, and the sychronized lock is unfair.

7. CAS

1. CAS features

CAS: The full name is Compare and swap, literally meaning: "Compare and swap".
CAS(V,A,B); The CAS operation includes three parameters: memory location V, expected value A and new value B. Its execution process is as follows:

  1. First, compare the value in memory location V to see if it is equal to the expected value A.
  2. If they are equal, the value in memory location V is replaced with the new value B and the operation succeeds. Otherwise, the operation fails.
  3. Regardless of whether the operation is successful or not, the current value of memory location V is returned.

pay attention:

  1. CAS is 原子的completed by a hardware instruction. CAS's memory read, compare, and write memory operations are a hardware instruction and are atomic.
  2. CAS directly reads and writes memory, rather than operating registers.
  3. When multiple threads perform CAS operations on a resource at the same time, only one thread can operate successfully, but it will not block other threads, and other threads will only receive a signal that the operation failed. CAS can be regarded as a kind of optimistic locking, or it can be understood that CAS is an implementation method of optimistic locking.

2. Application of CAS

Implement atomic classes.
Classes in the standard library java.util.concurrent.atomic are implemented using CAS (Compare-And-Swap) technology:

For example, the AtomicInteger class, these classes themselves are atomic, so related operations are safe even under multi-threading:

  1. num.getAndIncrement(); // This operation is equivalent to num++

  2. num.incrementAndGet(); // This operation is equivalent to ++num

  3. num.getAndDecrement(); // This operation is equivalent to num–

  4. num.decrementAndGet(); // This operation is equivalent to –num

Test atomic class:

public class CAS {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // 创建原子类,初始化值为0
        AtomicInteger num = new AtomicInteger(0);

        Thread t1 = new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                // num++ 操作
                num.getAndIncrement();
            }
        });

        Thread t2 = new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(num);

    }
}

num.getAndIncrement(); Operation pseudo code:

class AtomicInteger {
    
    
    private int value;
    public int getAndIncrement() {
    
    
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
    
    
            oldValue = value;
       }
        return oldValue;
   }
}

Note : The parameters in the CAS operation here oldValuecan be regarded as the values ​​in the working memory (register) valueand the values ​​in the main memory. If the values ​​of value and oldValue are the same, that is, the value of value has not changed during this update, then the value of oldValue+1 is assigned to value to achieve auto-increment. If the value is not equal to oldValue during comparison, it means that the value was changed during this update operation, so this update fails, and the oldValue value is refreshed for the next update operation.

3. CAS implements spin lock

Use CAS to implement spin lock pseudocode:

public class SpinLock {
    
    
    private Thread owner = null;
    public void lock(){
    
    
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
    
    
       }
   }
    public void unlock (){
    
    
        this.owner = null;
   }
}

Principle explanation : The above CAS pseudo code indicates that if the current lock holder is empty, it is relatively successful, and the lock acquisition right can be given to the current thread, and the lock completion cycle ends. If ownerit is not empty, it means that the current lock is held by other threads. At this time, the CAS operation fails and enters an idling loop, continuously asking whether the current lock holder is empty. At this time, once other threads release the lock, the current thread can immediately acquire the lock. .

4. ABA problem of CAS

CAS can only compare whether the values ​​are the same, but cannot determine whether the value has changed in the middle. This may cause misjudgments or incorrect results when the thread operates on the value.

For example, thread T1 reads a memory location V with a value of A, then performs some operations, and finally updates the value to B. During this period, thread T2 modifies the value of V from A to C and back to A. At this time, when T1 performs the CAS operation again, it will find that the value of V is still A, so it thinks that it has not been modified by other threads, and will update the value of V to B. But in fact, the value of V has been modified by other threads during this process, which causes an ABA problem.

For example, when performing a money withdrawal operation : Suppose I have a deposit of 100 yuan at this time and need to withdraw 50 yuan. At this time, two threads are established to perform the money withdrawal operation. Under normal circumstances, thread 1 and thread 2 read the current deposit of 100 yuan, and then thread 1 modifies it to 50 yuan, and the deduction is successful. Thread 2 blocks and ends. Comparing the difference between the current deposit of 50 yuan and 100 yuan, thread 2 fails. But if after the deduction is successful in thread 1, someone suddenly transfers another 50 yuan to me, and the deposit becomes 100 yuan again, and then when thread 2 starts the deduction operation, it will be found that the deposit and read values ​​​​are the same, and it will happen again Deduction. This is an ABA problem.

The solution is that
给要修改的数据引入版本号。 while CAS compares the current value of the data with the old value, it also needs to compare whether the version number meets expectations. If it is found that the current version number is consistent with the version number read before, the modification operation is actually performed and the version number is incremented. If it is found that the current version number is greater than the previously read version number, the operation is considered failed.

8. Synchronized principle

1. Basic characteristics of synchronized

Combining the above lock strategies, we can conclude that Synchronized has the following characteristics:

  1. It starts with optimistic locking, and if lock conflicts occur frequently, it is converted to pessimistic locking.
  2. It starts with a lightweight lock implementation, and if the lock is held for a long time, it is converted to a heavyweight lock.
  3. The lightweight lock is partially implemented based on the spin lock, and the heavyweight lock is partially implemented based on the pending wait lock.
  4. Is a reentrant lock.
  5. Not a read-write lock.
  6. Right and wrong lock.

2. Synchronized lock upgrade strategy

It can be seen from the strategies of synchronized locks mentioned above that synchronized locks can be upgraded according to actual scenarios. In the JVM, there are mainly the following lock upgrade strategies for synchronized locks:

遇到锁竞争
锁竞争更激烈
无锁
偏向锁
轻量级锁
重量级锁

The concept of biased lock is designed into the above lock strategy:

Bias lock : Do not lock unless necessary. Biased locking is not a real "locking". It just puts a "biased lock mark" in the object header to record which thread the lock belongs to:

  1. If no other threads compete for the lock of the currently marked object during the entire code execution process, there is no need to actually lock at this time. This saves the overhead caused by locking and unlocking.
  2. 如果后续有其他线程来竞争该锁,由于已经在该锁对象中记录了当前锁属于哪个线程了,因此很容易识别当前申请锁的线程是不是之前记录的线程,如果不是,那就取消原来的偏向锁状态,进入一般的轻量级锁状态。

简单来说,偏向锁就相当于是“搞暧昧”,一旦发现潜在危险,就立即官宣!

总之synchronized的锁升级策略主要指:当一个线程访问共享资源时,秉承非必要不加锁, 优先进入偏向锁状态。随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态。如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁。

3、synchronized 锁优化操作

锁消除
在程序中,可能存在有些程序的代码,用到了 synchronized,但其实没有在多线程环境下。此时这些加锁操作是非常没有必要的,而且会白白浪费加锁和解锁的资源开销。(如单线程下使用StringBuffer)这时我们的编译器+JVM 就会判断锁是否可消除,如果可以,就直接消除。

锁粗化
在一代码段逻辑中,如果出现多次加锁解锁,编译器 + JVM 会自动进行锁的粗化。

这里的锁粗化(细化)是相对于锁的粒度的,锁粒度即synchronized代码块包含代码的多少(代码越多,粒度越粗。越少粒度越细)。一般写代码的时候,多数情况下,希望锁的粒度更小一点(串行执行的代码少一些,并发执行的代码多一些,充分利用CPU内核资源)。但是实际上可能并没有其他线程来抢占这个锁进行并发,这种情况 JVM 就会自动把锁粗化,避免频繁申请释放锁带来额外开销。

Guess you like

Origin blog.csdn.net/LEE180501/article/details/130546165