Reference Articles:

1.https://juejin.cn/post/7236213437800890423

2.https://juejin.cn/post/7038473714970656775

3.https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html


Author: JD Technology, Jiao Zebin

Source: JD Cloud Developer Community. Please indicate the source when转载.

4. Fine-grained Synchronized Global Lock

0 25
Implementation scheme of lock under monolithic architecture1. Global lock of Ree...

Implementation scheme of lock under monolithic architecture

1. Global lock of ReentrantLock

ReentrantLock (reentrant lock), refers to a situation where a thread's reentrant request for a lock it already holds on a critical resource will succeed.

Compared with the commonly used Synchronized:


ReentrantLockSynchronized
Lock Implementation MechanismDepends on AQSMonitor Pattern
FlexibilitySupports response timeout, interruption, and attempt to acquire lockInflexible
Release FormMust explicitly call unlock () to release the lockAutomatic release of the monitor
Lock TypeFair Lock & Non-fair LockNon-fair Lock
Condition QueueCan associate multiple condition queuesAssociate a condition queue
ReentrancyReentrantReentrant


AQS Mechanism:If the requested shared resource is idle, then the current thread requesting the resource is set to an effective working thread, and the shared resource is passed through CAScompareAndSetStateSet to the locked state; if the shared resource is occupied, it uses a certain blocking waiting and waking mechanism (CLH variant FIFO double-ended queue) to ensure lock allocation.

Reentrancy:Whether it is a fair lock or a non-fair lock, the locking process will use a state value

private volatile int state
  • The state value is initialized to 0, indicating that no thread holds the lock.
  • When a thread requests the lock, the state value will increment by 1, and if the same thread acquires the lock multiple times, it will increment by 1 each time, which is the concept of reentrancy.
  • Unlocking is also a decrement of the state value until 0, indicating that this thread has released the lock.
public class LockExample {

    static int count = 0;
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                try {
                    // Lock
                    lock.lock();
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                finally {
                    // Unlock, placed in the finally clause to ensure the release of the lock
                    lock.unlock();
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count: " + count);
    }
}

/**
 * Output
 * count: 20000
 */

2. MySQL row lock, optimistic lock

Optimistic lock is the lock-free idea, which is generally implemented based on CAS (Compare-And-Swap) idea, and in MySQL, optimistic lock is implemented through version number + CAS lock-free form; for example, when T1 and T2 two transactions are executed concurrently, when T2 transaction is successfully committed, it will increment version+1, so the version condition of T1 transaction execution cannot be established.

Locking the SQL statement and performing state machine operations can also avoid data inconsistency problems caused by concurrent access to the count value by different threads.

// Optimistic lock + state machine
update
    table_name
set
    version = version + 1,
    count = count + 1
where
    id = id AND version = version AND count = [The previous count value];

// Row lock + state machine
 update
    table_name
set
    count = count + 1
where
    id = id AND count = [The previous count value]
for update;

3. Fine-grained ReetrantLock lock

If we directly use ReentrantLock global locking, then in this case, a thread acquires the lock, and all threads in the entire program will be blocked here; but in our project, we want to implement mutual exclusion logic for each user when operating, so we need a more fine-grained lock.

public class LockExample {
    private static Map<String, Lock> lockMap = new ConcurrentHashMap<>();
    
    public static void lock(String userId) {
        // Add fine-grained lock resources to the Map
        lockMap.putIfAbsent(userId, new ReentrantLock());
        // Get the lock from the container and implement locking
        lockMap.get(userId).lock();
    }
    public static void unlock(String userId) {
        // First, get the lock from the container to ensure the existence of the lock
        Lock locak = lockMap.get(userId);
        // Release lock
        lock.unlock();
    }
}

Disadvantages:If each user requests shared resources, the lock will be acquired once, and after that, the user has not logged in to the platform, but the lock object will always exist in memory, which is equivalent to a memory leak, so the lock timeout and淘汰mechanism need to be implemented.

4. Fine-grained Synchronized Global Lock

The above locking mechanism uses a lock containerConcurrentHashMap, which is easy for thread safety, but the underlying implementation will still useSynchronizedmechanism, so in some cases, using lockMap needs to add two layers of locks.

Then can we directly useSynchronizedto implement a fine-grained locking mechanism

public class LockExample {
    public static void syncFunc1(Long accountId) {
        String lock = new String(accountId + "").intern();

        synchronized (lock) {

            System.out.println(Thread.currentThread().getName() + " acquired the lock");
            // Simulate business processing time
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + " released the lock");
        }
    }

    public static void syncFunc2(Long accountId) {
        String lock = new String(accountId + "").intern();

        synchronized (lock) {

            System.out.println(Thread.currentThread().getName() + " acquired the lock");
            // Simulate business processing time
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + " released the lock");
        }
    }

    // Use Synchronized to implement finer-grained locking
    public static void main(String[] args) {
        new Thread(()-> syncFunc1(123456L), "Thread-1").start();
        new Thread(()-> syncFunc2(123456L), "Thread-2").start();
    }
}

/**
 * Print
 * Thread-1 acquired the lock
 * Thread-1 released the lock
 * Thread-2 acquired the lock
 * Thread-2 released the lock
 */
  • From the code, we find that the object that implements locking is actually a string object related to the user ID. There may be some doubts here, as each new thread that comes in creates a new string object, but the content of the string is the same. How can we ensure that the shared resource can be locked safely;
  • This actually needs to be attributed to the followingintern()The function's functionality;
  • intern()The function is used to add a string to the string constant pool in the heap space at runtime, if the string already exists, return the reference in the string constant pool.

Lock implementation in a distributed architecture

Core issue:We need to find a visible area among all threads in multiple processes to define this mutual exclusion variable.

An excellent distributed lock implementation should meet the following characteristics:

  1. In a distributed environment, it can ensure mutual exclusion between threads of different processes
  2. At the same time, only one thread can successfully acquire the lock resource at a time
  3. Ensure high availability where mutual exclusion is required
  4. It needs to ensure that locks and releases can be obtained and released with high performance
  5. It can support reentrancy of the same thread's lock
  6. It has a reasonable blocking mechanism, and threads that fail to compete for the lock should have a corresponding solution
  7. Supports non-blocking lock acquisition. Threads that fail to acquire the lock can directly return
  8. It has a reasonable lock expiration mechanism, such as timeout expiration, which can ensure that deadlocks do not occur

Redis implements distributed locking

  • Redis is a middleware that can be independently deployed;
  • It is visible to different Java processes, and the performance is also very可观
  • depending on the commands provided by Redis itselfsetnx key valueto implement distributed locking; it is different from the ordinarysetThe instruction is to set successfully only when the key does not exist, and return failure to set when the key exists

Code example:

// Inventory deduction interface
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    // Acquire lock
    String lockKey = "lock-" + inventory.getInventoryId();
    int timeOut = 100;
    Boolean flag = stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey, "Bamboo-Panda",timeOut,TimeUnit.SECONDS);
    // Adding an expiration time can ensure that deadlocks are also released within a certain period of time
    stringRedisTemplate.expire(lockKey,timeOut,TimeUnit.SECONDS);
    
    if(!flag){
        // Non-blocking implementation
        return "Server busy...Please try again later!!!";
    }
    
    // ----Only after successfully obtaining the lock can the following inventory reduction business be executed----        
    try{
        // Query inventory information
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "Insufficient inventory, please contact the seller....";
        }
        
        // Deduct inventory
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
    } catch (Exception e) { // Ensure that the lock can be released even if the business throws an exception, to avoid deadlock
        // Release lock
        stringRedisTemplate.delete(lockKey);
    }
    
    if (n > 0)
        return "Port-" + port + ", inventory deduction successful!!!";
    return "Port-" + port + ", inventory deduction failed!!!";
}

Author: Bamboo loves panda
Link: https://juejin.cn/post/7038473714970656775

Analysis of the rationality of expiration time:

Because for different businesses, the length of the expiration time we set will be different, too long is not appropriate, and too short is also not appropriate;

Therefore, the solution we thought of is to set up a child thread to extend the life of the current lock resource. Specifically, the child thread queries whether the key has expired every 2-3 seconds, and if it has not expired, it means that the business thread is still executing the business, so the expiration time of the key is extended by 5 seconds.

However, in order to avoid the situation where the child thread continues to extend the life of the main thread after the main thread unexpectedly dies, causing the phenomenon of 'immortal lock', the child thread is changed to a守护线程 of the main (business) thread, so that the child thread will die along with the main thread.

// Life extension child thread
public class GuardThread extends Thread { 
    private static boolean flag = true;

    public GuardThread(String lockKey, 
        int timeOut, StringRedisTemplate stringRedisTemplate){
        ……
    }

    @Override
    public void run() {
        // Start the loop to renew
        while (flag){
            try {
                // Sleep for half the time first
                Thread.sleep(timeOut / 2 * 1000);
            }
                e.printStackTrace();
            }
            // Go to renew after half the time has passed
            // First check if the key has expired
            Long expire = stringRedisTemplate.getExpire(
                lockKey, TimeUnit.SECONDS);
            // If it has expired, it means the main thread has released the lock
            if (expire <= 0){
                // Stop the loop
                flag = false;
            }
            // If it has not expired
            // Then renew it for half the time
            stringRedisTemplate.expire(lockKey,expire
                + timeOut/2,TimeUnit.SECONDS);
        }
    }
}


// Create a child thread to keep the lock alive
GuardThread guardThread = new GuardThread(lockKey,timeOut,stringRedisTemplate);
// Set as the daemon thread of the current business thread
guardThread.setDaemon(true);
guardThread.start();

Author: Bamboo loves panda 
Link: https://juejin.cn/post/7038473714970656775

The issue of lock expiration under Redis master-slave architecture

To ensure high availability of Redis during the development process, a master-slave replication architecture is adopted for read-write separation, thereby improving the throughput and availability of Redis. However, if a thread successfully acquires a lock on the Redis master node, but the master node has not yet had time to replicate it to the slave node before crashing, the other thread accessing Redis will access the slave node and also successfully acquire the lock. In this case, the access to the critical resource will present a security issue.

Solution:

  • Redlock Algorithm (official solution): Write data to multiple independent Redis instances at the same time. If more than half of the machines write successfully within the lock timeout period, the lock acquisition is successful and the locks on the successful machines are released if the operation fails. However, the drawback of this approach is that it is expensive and requires the independent deployment of multiple Redis nodes.
  • Additional lock status recording: an additional lock status is recorded through other independently deployed middleware (such as DB), and before a new thread acquires the lock, it needs to first query the lock holding record in the DB, and only when the lock status is not held should it try to acquire the distributed lock.HoweverThe disadvantages of this situation are obvious, the process of obtaining the lock is complex, and the performance overhead is also very large; in addition, it is also necessary to cooperate with the timer function to update the lock status in the DB to ensure the reasonable failure mechanism of the lock.
  • Implementation using Zookeeper

Zookeeper Implementation of Distributed Lock

The data of Zookeeper is different from that of redis, the data is real-time synchronized, and more than half of the nodes need to write after the master node writes in order to return success. Therefore, if projects such as e-commerce and education pursue high performance, they can give up some stability and recommend using redis to implement; for example, projects such as finance, banking, and government, which pursue high stability, can sacrifice some performance and recommend using Zookeeper to implement.

Distributed Lock Performance Optimization

The locking above indeed solves the problem of thread safety under concurrent conditions, but how should we deal with the scenario where 1 million users are competing to buy 1000 products at the same time?1701742007_656e85b7aff02e08fee1e.png!small?1701742008102

  • It is also possible to preheat the shared resources and store a segmented and dispersed copy. The flash sale time is at 15:00 in the afternoon, and the product quantity is divided into 10 parts around 14:30 in advance, and each piece of data is separately locked to prevent concurrent exceptions.
  • In addition, it is also necessary to write 10 keys in redis, and each new thread that enters first randomly allocates a lock, then performs the subsequent inventory reduction logic, releases the lock after completion, so that it can be used by subsequent threads.
  • The idea of this distributed lock is that, in order to improve the access speed of multi-threading under instantaneous conditions, it is also necessary to ensure a way of implementation under the condition of concurrent safety for the function of multi-threaded synchronization access to shared resources that can be realized with a single lock.

Reference Articles:

1.https://juejin.cn/post/7236213437800890423

2.https://juejin.cn/post/7038473714970656775

3.https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html


Author: JD Technology, Jiao Zebin

Source: JD Cloud Developer Community. Please indicate the source when转载.

你可能想看:
最后修改时间:
admin
上一篇 2025年03月26日 01:37
下一篇 2025年03月26日 02:00

评论已关闭