Multithreading and concurrency - several common implementations of locks

1. Pessimistic lock

As the name suggests, it refers to a conservative attitude towards data modification, thinking that other people will also modify the data. Therefore, when operating data, the data will be locked until the operation is completed. In most cases, pessimistic locking is implemented by the locking mechanism of the database to ensure the maximum degree of exclusivity of the operation. If the locking time is too long, other users will not be able to access it for a long time, which will affect the concurrent access of the program. At the same time, it will also have a great impact on database performance overhead, especially for long transactions, such overhead is often unbearable.

If it is a stand-alone system, we can use the synchronized keyword that comes with JAVA to lock resources by adding it to a method or a synchronization block. If it is a distributed system, we can use the lock mechanism of the database itself to achieve:

select * from 表名 where id= #{id} for update

When using pessimistic locks, we should pay attention to the lock level. When MySQL innodb locks, only the explicitly specified primary key or (index field) will use the row lock; otherwise, the table lock will be executed to lock the entire table. performance will be poor. When using pessimistic locking, we must turn off the autocommit attribute of the MySQL database, because mysql uses the autocommit mode by default. Pessimistic locking is suitable for scenarios with many writes, and the requirements for concurrency performance are not high.

2. Optimistic lock

Optimistic lock, which can be guessed from the literal meaning, is very optimistic when operating data, thinking that others will not modify the data at the same time, so optimistic lock will not be locked. It is only when the update is submitted that the data conflict or not will be officially determined. to test. If a conflict is found, an error message will be returned to let the user decide how to do it, fail-fast mechanism. Otherwise, perform this operation.

It is divided into three stages: data reading, writing verification, and data writing.

If it is a stand-alone system, we can implement it based on JAVA's CAS. CAS is an atomic operation, which is implemented with the help of hardware comparison and exchange.

If it is a distributed system , we can add a field in the database table  版本号 , such as: version

update 表 
set ... , version = version +1 
where id= #{id} and version = #{version} 

Before the operation, read the version number of the record first, and compare the version number through the SQL statement to see if it is consistent when updating. If consistent, update the data. Otherwise, the version will be read again and the above operation will be retried.

3. Distributed lock

The synchronized and ReentrantLock in JAVA all solve the resource mutual exclusion problem of single-machine deployment of single application. With the rapid development of business, when a single application evolves into a distributed cluster, multi-threads and multi-processes are distributed on different machines, and the original single-machine concurrency control lock strategy becomes invalid.

At this time, we need to introduce distributed locks to solve the cross-machine mutual exclusion mechanism to control access to shared resources.

What conditions do distributed locks need to meet:

  • The same resource mutual exclusion function as the stand-alone system, which is the basis of the lock

  • High-performance acquisition and release of locks

  • high availability

  • reentrant

  • There is a lock failure mechanism to prevent deadlock

  • Non-blocking, no matter whether the lock is acquired or not, it must be able to return quickly

There are various implementation methods, based on database, Redis, and Zookeeper, etc. Here are the mainstream Redis-based implementation methods:

Lock:

SET key unique_value  [EX seconds] [PX milliseconds] [NX|XX]

Through the atomic command, if the execution is successful and returns 1, it means that the lock is successful. Note: unique_value is the unique identifier generated by the client. Special attention should be paid to distinguish the unlocking of the lock operation from different clients. First judge whether the unique_value is a locked client, and then allow unlocking and deletion. After all, we cannot delete locks added by other clients.

Unlocking: There are two command operations for unlocking, and Lua scripts are needed to ensure atomicity.

// 先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

With the high performance of Redis, Redis's implementation of distributed locks is also the current mainstream implementation method. But everything has advantages and disadvantages. If the server that locks is down, when the slave node has not had time to back up the data, it is not that other clients can also obtain the lock.

In order to solve this problem, Redis officially designed a distributed lock Redlock.

Basic idea: Let the client and multiple independent Redis nodes request to apply for locks in parallel. If more than half of the nodes can successfully complete the lock operation, then we believe that the client has successfully obtained the distributed lock, otherwise lock fail.

4. Reentrant lock

Reentrant locks, also known as recursive locks, mean that when the same thread is calling the outer method to acquire the lock, then entering the inner method will automatically acquire the lock.

There is a counter inside the object lock or class lock. Every time a thread acquires a lock, the counter is +1; when it is unlocked, the counter is -1.

The number of times of locking corresponds to the number of times of unlocking, and locking and unlocking appear in pairs.

Both ReentrantLock and synchronized in JAVA are reentrant locks. One advantage of reentrant locks is that deadlocks can be avoided to a certain extent.

5. Spin lock

The spin lock is used to let the current thread execute in the loop body continuously, and only enter the critical section when the condition of the loop is changed by other threads. The spin lock just keeps the current thread executing the loop body without changing the thread state, so the response speed is faster. But when the number of threads continues to increase, the performance drops significantly, because each thread needs to be executed, which will occupy CPU time slices. If the thread competition is not fierce, and keep the lock time period. Suitable for use with spinlocks.

Disadvantages of spin locks:

  • deadlock

  • May be using CPU for too long

We can set a cycle time or number of cycles. When the threshold is exceeded, the thread will enter the blocking state to prevent the thread from occupying CPU resources for a long time. The CAS in the JUC concurrent package uses a spin lock, and compareAndSet is the core of the CAS operation, and the bottom layer is implemented using the Unsafe object.

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

If the var2 field value of the var1 object in memory is equal to the expected var5, then update the location to the new value (var5 + var4), otherwise do nothing and keep retrying until the operation succeeds.

CAS includes Compare and Swap operations, how to ensure atomicity? CAS is an atomic operation supported by the CPU, and its atomicity is controlled at the hardware level.

In particular, CAS may cause ABA problems, we can introduce incrementing the version number to solve.

6. Exclusive lock

Exclusive locks are also called exclusive locks. Regardless of read or write operations, only one thread can acquire the lock, and other threads are blocked.

Disadvantages: The read operation does not modify the data, and most systems read more and write less. If the read and read are mutually exclusive, the performance of the system will be greatly reduced. The following shared lock will solve this problem.

Both ReentrantLock and synchronized in JAVA are exclusive locks.

7. Exclusive lock

Shared locks allow multiple threads to hold locks at the same time, and are generally used for read locks. The shared lock of the read lock can ensure that concurrent reading is very efficient. Read-write, write-read, and write-write are mutually exclusive. Exclusive locks and shared locks are also implemented through AQS, by implementing different methods to achieve exclusive or shared.

ReentrantReadWriteLock, its read lock is a shared lock, and its write lock is an exclusive lock.

8. Read lock/write lock

If a resource is a read operation, multiple threads will not affect each other, and can be shared by adding a read lock. If there is a modification action, in order to ensure the concurrent security of the data, only one thread can acquire the lock at this time, which we call a write lock. Read-read is shared; while read-write, write-read, and write-write are mutually exclusive.

Like ReentrantReadWriteLock in JAVA is a kind of read-write lock

9. Fair lock/unfair lock

Fair lock : multiple threads acquire locks in the order in which they apply for locks, and all threads are queued in the queue, based on the fairness principle of first come, first acquired.

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 CPU will wake up the next blocked thread with system overhead.

Unfair lock : multiple threads do not acquire locks in the order in which they apply for locks, but directly try to acquire locks by jumping in the queue at the same time. When it arrives (queue jumping is successful), the lock is acquired directly.

Advantages : It can reduce the overhead of CPU waking up threads, and the overall throughput efficiency will be higher.

Disadvantages : It may cause the threads queued in the queue to fail to acquire the lock for a long time or fail to acquire the lock for a long time, starving to death.

For Java multi-threaded concurrent operations, most of our operation locks are implemented based on Sync itself, but Sync itself is an internal class of ReentrantLock, and Sync inherits AbstractQueuedSynchronizer

Like ReentrantLock, which is an unfair lock by default, we can pass true in the constructor to create a fair lock.

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

10. Interruptible lock/uninterruptible lock

Interruptible lock: Refers to a thread that can interrupt its blocked state because it has not acquired the lock while it is blocking and waiting. Uninterruptible lock: On the contrary, if the lock is acquired by other threads, the current thread can only block and wait. If the thread holding the lock never releases the lock, other threads that want to acquire the lock will always be blocked.

The built-in lock synchronized is an uninterruptible lock, while ReentrantLock is an interruptible lock.

There are three ways for ReentrantLock to acquire locks:

  • lock(), if the lock is acquired, return immediately, if another thread holds the lock, the current thread will be blocked until the thread acquires the lock

  • tryLock(), if the lock is acquired, return true immediately, if another thread is holding the lock, return false immediately

  • tryLock(long timeout, TimeUnit unit), if the lock is acquired, it will return true immediately. If other threads are holding the lock, it will wait for the time given by the parameter. During the waiting process, if the lock is acquired, it will return true. If Wait for timeout, return false;

  • lockInterruptibly(), returns immediately if the lock is acquired; if the lock is not acquired, the thread is blocked until the lock is acquired or the thread is interrupted by another thread

11. Segment lock

Segmented lock is actually a lock design, the purpose is to refine the granularity of the lock, not a specific lock. For ConcurrentHashMap, its concurrent implementation is to achieve efficient concurrent operations in the form of segmented locks.

The segment lock in ConcurrentHashMap is called Segment, which is similar to the structure of HashMap (the implementation of HashMap in JDK7), that is, it has an Entry array inside, and each element in the array is a linked list; it is also a ReentrantLock (Segment Inherited from ReentrantLock).

When you need to put elements, instead of locking the entire HashMap, you first know which segment to put in through the hashcode, and then lock the segment, so when multi-threaded put, as long as it is not placed in the same In a segment, parallel insertion is supported.

12. Lock upgrade (no lock|biased lock|lightweight lock|heavyweight lock)

Before JDK 1.6, synchronized was still a heavyweight lock with low efficiency. However, after JDK 1.6, the JVM optimized synchronized in order to improve the efficiency of lock acquisition and release, and introduced biased locks and lightweight locks. Since then, there have been four types of locks: no lock, biased lock, and lightweight Level lock, heavyweight lock. These four states will gradually escalate with the competition situation, and they cannot be downgraded.

no lock

Lock-free does not lock resources. All threads can access and modify the same resource, but only one thread can modify it successfully at the same time. That is what we often call optimistic locking.

Bias lock

The thread that is biased towards the first access lock, when executing the synchronized code block for the first time, modifies the lock flag bit in the object header through CAS, and the lock object becomes a biased lock.

When a thread accesses a synchronized code block and acquires a lock, it will store the lock-biased thread ID in the Mark Word. When the thread enters and exits the synchronized block, it no longer uses the CAS operation to lock and unlock, but detects whether there is a bias lock pointing to the current thread stored in the Mark Word. The acquisition and release of lightweight locks rely on multiple CAS atomic instructions, while biased locks only need to rely on one CAS atomic instruction when replacing ThreadID.

After executing the synchronization code block, the thread will not actively release the bias lock. When the thread executes the synchronization code block for the second time, the thread will judge whether the thread holding the lock is itself (the thread ID holding the lock is also in the object header), and if so, it will continue to execute normally. Since the lock has not been released before, there is no need to re-lock it here, and there is almost no additional overhead for biased locks, and the performance is extremely high.

Only when other threads try to compete for the biased lock, the thread holding the biased lock will release the lock, and the thread will not actively release the biased lock. Regarding the cancellation of the biased lock, it is necessary to wait for the global security point, that is, when no bytecode is being executed at a certain point in time, it will first suspend the thread that owns the biased lock, and then determine whether the lock object is locked. If the thread is not active, set the object header to the lock-free state, cancel the bias lock, and return to the lock-free (flag bit is 01) or lightweight lock (flag bit is 00) state.

Biased lock means that when a piece of synchronization code is always accessed by the same thread, that is, when there is no competition among multiple threads, then the thread will automatically acquire the lock during subsequent access, thereby reducing the cost of acquiring the lock.

lightweight lock

The current lock is a biased lock. At this time, there are multiple threads competing for the lock at the same time, and the biased lock will be upgraded to a lightweight lock. Lightweight locks believe that although competition exists, ideally the degree of competition is very low, and locks are acquired by spinning.

There are two situations for lightweight lock acquisition:

  • When the bias lock function is turned off

  • Multiple threads competing for biased locks cause biased locks to be upgraded to lightweight locks. Once a second thread joins the lock competition, the biased lock is upgraded to a lightweight lock (spin lock)

In the lightweight lock state, the lock competition continues, and the thread that has not grabbed the lock will spin, and it will continuously loop to determine whether the lock can be successfully acquired. The operation of acquiring a lock is actually to modify the lock flag in the object header through CAS. First compare whether the current lock flag is "released", and if so, set it to "locked". This process is atomic. If the lock is grabbed, then the thread modifies the current lock holder information to itself.

heavyweight lock

If the thread competition is very exciting, and the thread spin exceeds a certain number of times (the default cycle is 10 times, which can be changed through the virtual machine parameters), the lightweight lock is upgraded to a heavyweight lock (still CAS to modify the lock flag, but not Modify the thread ID holding the lock), when the subsequent thread tries to acquire the lock and finds that the occupied lock is a heavyweight lock, it will directly suspend itself (instead of busy waiting), waiting for future wake-up.

A heavyweight lock means that when a thread acquires a lock, all other threads waiting to acquire the lock will be blocked. In short, all control rights are given to the operating system, and the operating system is responsible for scheduling between threads and changing the state of threads. And this will frequently switch the thread running state, suspend and wake up the thread, thereby consuming a lot of system resources.

13. Lock optimization technology (lock coarsening, lock elimination)

Lock coarsening tells us that everything has a limit. In some cases, we want to combine many lock requests into one request to reduce the performance loss caused by a large number of lock requests, synchronization, and release in a short period of time.

For example: there is a loop body, inside

for(int i=0;i<size;i++){
    synchronized(lock){
        ...业务处理,省略
    }
}

The code after lock coarsening is as follows:

synchronized(lock){
    for(int i=0;i<size;i++){
        ...业务处理,省略
    }
}

Lock elimination refers to the fact that in some cases, if the JVM virtual machine cannot detect the possibility of a certain piece of code being shared and competed, it will eliminate the synchronization lock to which this piece of code belongs, so as to improve the performance of the program.

The basis for lock elimination is the data support of escape analysis, such as the append() method of StringBuffer, or the add() method of Vector, in many cases, lock elimination can be performed, such as the following code:

public String method() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    return sb.toString();
}

The compiled bytecode of the above code is as follows:

From the above results, it can be seen that the thread-safe locked StringBuffer object we wrote before was replaced with an unlocked and unsafe StringBuilder object after the bytecode was generated. The reason is that the variable of StringBuffer belongs to a local variable , and will not escape from this method, so we can use lock elimination (no lock) to speed up the running of the program.

Guess you like

Origin blog.csdn.net/qq_34272760/article/details/128003221