Some commonly used locks in Java

Reprinted from: https://www.cnblogs.com/jyroy/p/11365935.html

One, pessimistic lock and optimistic lock

Pessimistic lock: For concurrent operations on the same data, pessimistic lock believes that there must be other threads to modify the data when using the data, so it will first lock when acquiring the data to ensure that the data will not be modified by other threads. In Java, the synchronized keyword and the implementation class of Lock are both pessimistic locks

Optimistic lock: Optimistic lock thinks that it will not have other threads modify the data when using the data, so it will not add locks. It just judges whether other threads have updated the data before updating the data. If this data is not updated, the current thread successfully writes the modified data. If the data has been updated by other threads, different operations (such as error reporting or automatic retry) are performed according to different implementations.

According to the concept description above, we can find:

  • Pessimistic lock is suitable for scenarios where there are many write operations. Locking first can ensure that the data is correct during write operations.

  • Optimistic lock is suitable for scenarios with many read operations, and the feature of no lock can greatly improve the performance of read operations.

Write an example below to verify:

public class Test {
    public int number = 1;
    /**  ------------------------ 悲观锁实现 start-------------------**/
    // 方式一:使用synchronized关键字
    public synchronized void testMethod(){
        number ++;// 操作同步资源
    }
    // 方式二:使用lock锁,需要保证多个线程使用同一个锁
    private ReentrantLock lock = new ReentrantLock();
    public void modify(){
        lock.lock();
        number ++;// 操作同步资源
        lock.unlock();
    }
    /**  ------------------------ 悲观锁实现 end-------------------**/

    /**  ------------------------ 乐观锁实现 start-------------------**/
    // java.util.concurrent包中的原子类
    private AtomicInteger atomicInteger = new AtomicInteger();
    public int increment(){
        return atomicInteger.incrementAndGet();// 执行自增1
    }
    /**  ------------------------ 乐观锁实现 end-------------------**/
}

Simulate concurrent requests

package com.xiateng;

public class MyThread extends Thread{
    private Test test;
    public MyThread(Test test){
        this.test = test;
    }
    @Override
    public void run(){
        try {
            Thread.sleep(1000);// 模拟逻辑处理时间
            int increment = test.increment();
            System.out.println("number = "+ increment);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 Start 10 threads to process resource test will find out;

class test1{
    public static void main(String[] args) {
        Test test = new Test();
        for (int i = 1; i < 10; i++){
            MyThread thread = new MyThread(test);
            new Thread(thread).start();
        }
    }
}

If you don’t use locks: threads cannot be executed sequentially, leading to dirty data

Use pessimistic locks: Operate synchronization resources after explicit locking

Use optimistic locking: directly operate synchronization resources

Why can optimistic locks directly manipulate synchronization resources and achieve thread synchronization?

Optimistic locking is realized through CAS. The full name of CAS is Compare And Swap (Compare And Swap), which is a lock-free algorithm. Realize variable synchronization between multiple threads without using locks (no threads are blocked).

The CAS algorithm involves three operands:

  • The memory value V that needs to be read and written.

  • The value A to be compared.

  • The new value B to be written.

If and only if the value of V is equal to A, CAS uses the new value B to update the value of V atomically ( comparison + update is an atomic operation as a whole ), otherwise it will not perform any operation. In general, "update" is an operation that is constantly retried.

The source code is as follows:

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = this.getIntVolatile(o, offset);
        } while(!this.compareAndSwapInt(0, offset, v, v + delta));

        return var5;
    }

According to the source code of OpenJDK 8, we can see that the getAndAddInt() loop gets the value v at the offset in the given object o, and then judges whether the memory value is equal to v. If they are equal, the memory value is set to v + delta, otherwise it returns false, and the loop continues to retry, until the setting is successful, the loop can be exited and the old value is returned. The entire "compare + update" operation is encapsulated in compareAndSwapInt(), which is completed by a CPU instruction in JNI. It is an atomic operation and can ensure that multiple threads can see the modified value of the same variable.

Subsequent JDK uses the cmpxchg instruction of the CPU to compare the A in the register with the value V in the memory. If they are equal, store the new value B to be written into the memory. If they are not equal, the memory value V is assigned to the value A in the register. Then call the cmpxchg instruction again through the while loop in the Java code to retry until the setting is successful.

Although CAS is very efficient, there are three problems:

1. ABA problem, CAS needs to check whether the memory value has changed when operating the value, and will update the memory value if there is no change, but if the principle of the memory value is A, then it becomes B, and then it becomes A, then The CAS check will think that the memory value has not changed, and the solution can be to add the version number in front of the variable.

2. When the cycle time is long, the overhead is relatively large. If the CAS operation is unsuccessful for a long time, it will cause it to spin all the time, which brings a lot of overhead to the CPU.

3. Only the atomic operation of one shared variable can be guaranteed. If there are multiple shared variables, CAS cannot guarantee the atomicity of the operation

2. Spin lock and adaptive spin lock

So what is a spin lock?

Blocking or waking up a Java thread requires the operating system to switch the CPU state to complete. Switching the state requires processor time. If the logic processing of the synchronized code block is relatively simple, the state switching may take longer than the code execution time. The gain is not worth the loss.

In order to let the current thread "wait for a while", we need to spin the current thread. If the previous lock has been released after the spin is completed, then the current thread can acquire the lock directly without blocking, thus avoiding switching threads The overhead, this is the spin lock.

Advantages and disadvantages: Although spin waiting avoids the overhead of thread switching, it takes up processor time. If the lock is occupied for a short time, the spin effect will be very good. On the contrary, if the lock is occupied for a long time, then the self Spinning threads will only waste processor resources bye bye, so the spin waiting time must have a certain limit. If the spin exceeds the limited number of times and the lock is not successfully obtained, the current thread will be suspended.

Spin lock was introduced in JDK1.4.2. Use -XX:+UseSpinning to turn it on. In JDK 6, it is turned on by default, and an adaptive spin lock (adaptive spin lock) is introduced.

Adaptive means that the time (number of times) of the spin is no longer fixed, but is determined by the previous spin time on the same lock and the state of the lock owner. If on the same lock object, spin wait has just successfully acquired the lock, and the thread holding the lock is running, then the virtual machine will think that this spin is also likely to succeed again, and it will allow the spin The wait lasts for a relatively longer time. If the spin is rarely successfully obtained for a certain lock, it is possible to omit the spin process when trying to acquire the lock in the future, and directly block the thread to avoid wasting processor resources.

Three, the four evolution stages of synchronized: no lock -> biased lock -> lightweight lock -> heavyweight lock

1、synchronized

Introduction

Synchronized is a keyword at the jvm level, which can synchronize a certain piece of code. Unlike ReentrantLock, it cannot be interrupted.

Monitor

Monitor can be understood as a synchronization tool or a synchronization mechanism, usually described as an object. Every Java object has an invisible lock called an internal lock or a monitor lock.

Monitor is a thread-private data structure. Each thread has a list of available monitor records and a global list of available monitor records. Each locked object is associated with a monitor, and there is an Owner field in the monitor to store the unique identifier of the thread that owns the lock, indicating that the lock is occupied by this thread.

Synchronized thread synchronization is achieved through the Monitor, which relies on the Mutex Lock (mutual exclusion lock) of the underlying operating system to achieve thread synchronization.

Java object header

The object header mainly includes two parts of data: Mark Word (marked field), Klass Pointer (type pointer)

Mark Word: The HashCode, generation age and lock flag information of the object are stored by default. (The relationship between the content stored in Mark Word and the lock flag is as follows)

Klass Point: The pointer of the object to its class metadata. The virtual machine uses this pointer to determine which class instance this object is.
Insert picture description here
Synchronized before JDK1.6: When using non-spin locks (mutexes), "blocking or waking up a Java thread requires the operating system to switch the CPU state to complete, and this state transition requires processor time. If you synchronize the code block The content of is too simple, and the time consumed by state transition may be longer than the execution time of user code." This method is the way that synchronized originally realized synchronization, which is the reason for the low efficiency of synchronization before JDK 6. This kind of lock that depends on the operating system Mutex Lock is called "heavyweight lock". In order to reduce the performance cost of acquiring and releasing locks, JDK 6 introduced "biased locks" and "lightweight locks". ".

2. No lock

No lock does not lock the resource, all threads can access and modify the same resource, but only one thread can modify it successfully at the same time.

The feature of lock-free is that the modification operation is performed in a loop, and the thread will constantly try to modify the shared resource. If there is no conflict, the modification succeeds and exits, otherwise the loop will continue to try. If multiple threads modify the same value, one thread must be able to modify it successfully, and other threads that fail to modify will continue to retry until the modification is successful. The principle and application of CAS we introduced above is the realization of lock-free. Lock-free cannot fully replace with lock, but the performance of lock-free in some situations is very high.

3. Bias lock

A biased lock means that a piece of synchronized code has been accessed by a thread, then the thread will automatically acquire the lock, reducing the cost of acquiring the lock.

In most cases, the lock is always acquired by the same thread multiple times, and there is no multi-thread competition, so there is a biased lock. The goal is to improve performance when only one thread executes synchronized code blocks.

When a thread accesses the synchronized code block and acquires the lock, the thread ID of the lock bias is stored in the Mark Word. When the thread enters and exits the synchronized block, the CAS operation is no longer used to lock and unlock, but to check whether the Mark Word stores a biased lock pointing to the current thread. The introduction of biased locks is to minimize unnecessary lightweight lock execution paths without multi-threaded competition, because the acquisition and release of lightweight locks rely on multiple CAS atomic instructions, and biased locks only need to replace the ThreadID Just rely on the CAS atomic instruction once.

The biased lock will only release the lock when other threads try to compete for the biased lock, and the thread will not release the biased lock actively. To revoke a biased lock, you need to wait for a global security point (at this point where no bytecode is being executed), it will first suspend the thread that owns the biased lock to determine whether the lock object is locked. After revoking the bias lock, return to the state of no lock (the flag bit is "01") or lightweight lock (the flag bit is "00").

The bias lock is enabled by default in the JVM of JDK 6 and later. You can turn off the biased lock through the JVM parameter: -XX:-UseBiasedLocking=false. After closing, the program will enter the lightweight lock state by default.

4. Lightweight lock

It means that when the lock is a biased lock and is accessed by another thread, the biased lock will be upgraded to a lightweight lock, and other threads will try to acquire the lock in the form of spin without blocking, thereby improving performance.

When the code enters the synchronization block, if the lock state of the synchronization object is unlocked (the lock flag is in the "01" state, whether it is a biased lock is "0"), the virtual machine will first create one in the stack frame of the current thread The space named Lock Record is used to store a copy of the current Mark Word of the lock object, and then copy the Mark Word in the object header to the lock record.

After the copy is successful, the virtual machine will use the CAS operation to try to update the Mark Word of the object to a pointer to the Lock Record, and point the owner pointer in the Lock Record to the Mark Word of the object.

If the update action is successful, then the thread has the lock of the object, and the lock flag of the object Mark Word is set to "00", indicating that the object is in a lightweight lock state.

If the update operation of the lightweight lock fails, the virtual machine will first check whether the Mark Word of the object points to the stack frame of the current thread. If it is, it means that the current thread already owns the lock of this object, so you can directly enter the synchronization block to continue. Execute, otherwise it means that multiple threads compete for locks.

If there is only one waiting thread, the thread waits by spinning. But when the spin exceeds a certain number of times, or one thread is holding a lock, one is spinning, and there is a third visit, the lightweight lock is upgraded to a heavyweight lock.

5. Heavyweight lock

When upgrading to a heavyweight lock, the status value of the lock flag becomes "10". At this time, the pointer to the heavyweight lock is stored in the Mark Word. At this time, the threads waiting for the lock will enter the blocking state.

Summary: The partial lock solves the locking problem by comparing Mark Word and avoids the execution of CAS operations. The lightweight lock solves the locking problem by using CAS operations and spins to avoid thread blocking and wake-up that affect performance. Heavyweight locks block all threads except the thread that owns the lock.

Four, fair lock and unfair lock

Fair lock

A fair lock means that multiple threads acquire the lock in the order in which they apply for the lock. The thread directly enters the queue, and the first thread in the queue can acquire the lock. The advantage of a fair lock is that threads waiting for the lock will not starve to death. The disadvantage is that the overall throughput efficiency is lower than that of unfair locks. All threads in the waiting queue except the first thread will be blocked, and the overhead of waking up blocked threads by the CPU is larger than that of unfair locks.

Unfair lock

Unfair locks are when multiple threads try to acquire the lock directly when they are locked, and they will wait until the end of the waiting queue until they cannot be acquired. But if the lock is just available at this time, then this thread can directly acquire the lock without blocking, so an unfair lock may occur when the thread that applies for the lock first acquires the lock. The advantage of unfair locks is that they can reduce the overhead of invoking threads, and the overall throughput efficiency is high, because threads have a chance to directly obtain locks without blocking, and the CPU does not have to wake up all threads. The disadvantage is that threads in the waiting queue may starve to death or wait a long time before acquiring the lock.

Next, we will explain fair locks and unfair locks through the source code of ReentrantLock.

According to the code, there is an internal class Sync in ReentrantLock. Sync inherits AQS (AbstractQueuedSynchronizer). Most of the operations of adding locks and releasing locks are actually implemented in Sync. It has two subcategories: FairSync and NonfairSync. ReentrantLock uses unfair locks by default, and you can also explicitly specify the use of fair locks through the constructor.

 Enter the hasQueuedPredecessors() method

We will find that this method is mainly to determine whether the current thread is the first in the synchronization queue, if it is, it returns true, otherwise it returns false.

to sum up

Fair lock is to realize that multiple threads acquire locks in the order in which they apply for locks by synchronizing the queue, thereby realizing the characteristics of fairness. When the unfair lock is locked, the queue waiting is not considered, and the lock is directly tried to obtain the lock, so it is the case that the lock is obtained after the application is applied.

Five, reentrant locks and non-reentrant locks

Reentrant lock, also known as recursive lock, means that when the same thread acquires the lock in the outer method, the inner method of the thread will automatically acquire the lock (provided that the lock object must be the same object or class). Blocked because it has been acquired before but has not been released. Both ReentrantLock and synchronized in Java are reentrant locks. One advantage of reentrant locks is that deadlocks can be avoided to a certain extent.

The following sample code is used for analysis:

 In the above code, the two methods in the class are modified by the built-in lock synchronized, and the doOthers() method is called in the doSomething() method. Because the built-in lock is reentrant, the same thread can directly obtain the lock of the current object when calling doOthers(), and enter doOthers() for operation.

If it is a non-reentrant lock, the current thread needs to release the lock of the current object acquired during doSomething() before calling doOthers(). In fact, the object lock has been held by the current thread and cannot be released. So there will be a deadlock at this time.

Why can reentrant locks call synchronized resources repeatedly?

Take ReentrantLock as an example. AQS, the parent class of ReentrantLock, maintains a synchronization status status internally to count the number of reentries. The initial value of status is 0.

When a thread tries to acquire a lock, the reentrant lock first tries to acquire and update the status value. If status == 0 means that no other threads are executing synchronization code, the status is set to 1, and the current thread starts execution. If status != 0, judge whether the current thread is the thread that has acquired the lock, if it is, execute status+1, and the current thread can acquire the lock again. The non-reentrant lock is to directly acquire and try to update the current status value. If status != 0, it will fail to acquire the lock and the current thread will be blocked.

When the lock is released, the reentrant lock also first obtains the value of the current status, provided that the current thread is the thread holding the lock. If status-1 == 0, it means that all repeated lock acquisition operations of the current thread have been executed, and then the thread will actually release the lock. The non-reentrant lock is to directly set the status to 0 after determining that the current thread is the thread holding the lock, and release the lock.

Six, exclusive lock and shared lock

Exclusive locks and shared locks are also a concept. We first introduce the specific concepts, and then introduce exclusive locks and shared locks through the source code of ReentrantLock and ReentrantReadWriteLock.

An exclusive lock is also called an exclusive lock, which means that the lock can only be held by one thread at a time. If thread T adds an exclusive lock to data A, other threads can no longer add any type of lock to A. The thread that obtains the exclusive lock can both read the data and modify the data. The implementation classes of synchronized in JDK and Lock in JUC are mutual exclusion locks.

Shared lock means that the lock can be held by multiple threads. If thread T adds a shared lock to data A, other threads can only add a shared lock to A, and cannot add an exclusive lock. The thread that obtains the shared lock can only read the data and cannot modify the data.

Exclusive locks and shared locks are also realized through AQS. By implementing different methods, exclusive or shared locks can be realized.

The following figure shows part of the source code of ReentrantReadWriteLock:

 

We see that ReentrantReadWriteLock has two locks: ReadLock and WriteLock, which are understood by words, one read lock and one write lock, collectively called "read-write lock". Further observation can be found that ReadLock and WriteLock are locks implemented by the internal class Sync. Sync is a subclass of AQS, and this structure also exists in CountDownLatch, ReentrantLock, and Semaphore.

In ReentrantReadWriteLock, the main body of the read lock and write lock is Sync, but the lock method of read lock and write lock is different. Read locks are shared locks, and write locks are exclusive locks. The shared lock of the read lock can ensure that concurrent reading is very efficient, and the processes of reading, writing, reading, and writing are mutually exclusive, because the read lock and the write lock are separated. Therefore, the concurrency of ReentrantReadWriteLock has been greatly improved compared to general mutex locks.

What is the difference between the specific locking method of read lock and write lock? Before understanding the source code, we need to review other knowledge.

When we first mentioned AQS, we also mentioned the state field (int type, 32 bits), which is used to describe how many threads hold locks.

In an exclusive lock, this value is usually 0 or 1 (if it is a reentrant lock, the state value is the number of reentrants), and in a shared lock, the state is the number of locks held. But there are two locks for reading and writing in ReentrantReadWriteLock, so you need to describe the number of read locks and write locks (or state) on an integer variable state. So the state variable is "bitwise cut" into two parts, the upper 16 bits represent the read lock status (number of read locks), and the lower 16 bits represent the write lock status (number of write locks). As shown below:

After understanding the concept, let's look at the code, first look at the lock source code of the write lock:

  • This code first obtains the number of current locks c, and then uses c to obtain the number of write locks w. Because the write lock is the lower 16 bits, take the maximum value of the lower 16 bits and do the AND operation with the current c (int w = exclusiveCount(c); ), the upper 16 bits and 0 are AND after the operation is 0, and the rest is the lower bit The value of the operation is also the number of threads holding the write lock.

  • After fetching the number of write lock threads, first determine whether any threads already hold the lock. If there is already a thread that holds the lock (c!=0), check the current number of write lock threads. If the number of write threads is 0 (that is, there is a read lock at this time) or the thread holding the lock is not the current thread, it will return failure (Involving the realization of fair locks and unfair locks).

  • If the number of write locks is greater than the maximum number (65535, 2 to the 16th power -1), an Error is thrown.

  • If the number of write threads is 0 (then the read thread should also be 0, because the c!=0 situation has been handled above), and the current thread needs to be blocked, then return failure; if increasing the number of write threads through CAS fails, it also returns failure .

  • If c=0, w=0 or c>0, w>0 (reentrant), set the owner of the current thread or lock and return success!

In addition to the reentry condition (the current thread is the thread that has acquired the write lock), tryAcquire() adds a judgment for the existence of a read lock. If there is a read lock, the write lock cannot be acquired, because the operation of the write lock must be ensured to be visible to the read lock. If the read lock is allowed to acquire the write lock when it has been acquired, then other reader threads are running It is impossible to perceive the operation of the current writer thread.

Therefore, the write lock can be acquired by the current thread only after waiting for other reader threads to release the read lock, and once the write lock is acquired, subsequent accesses of other read and write threads are blocked. The release process of the write lock is basically similar to the release process of ReentrantLock. Each release reduces the write state. When the write state is 0, it means that the write lock has been released, and then the waiting read and write threads can continue to access the read and write lock. The modification of the writing thread is visible to the subsequent reading and writing threads.

Next is the code for reading the lock:

It can be seen that in the tryAcquireShared(int unused) method, if other threads have already acquired the write lock, the current thread fails to acquire the read lock and enters the waiting state. If the current thread acquires the write lock or the write lock is not acquired, the current thread (thread safety, relying on CAS guarantee) increases the read status and successfully acquires the read lock. Each release of the read lock (thread-safe, multiple reader threads may release the read lock at the same time) reduces the read status, and the reduced value is "1<<16". Therefore, the read-write lock can share the process of reading and reading, and the processes of reading, writing, reading, and writing are mutually exclusive.

At this point, let's look back at the source code of the fair lock and unfair lock in the mutex ReentrantLock:

 

We found that although there are fair locks and unfair locks in ReentrantLock, they all add exclusive locks. According to the source code, when a thread calls the lock method to acquire a lock, if the synchronization resource is not locked by other threads, the current thread will successfully preempt the resource after successfully updating the state with CAS. If the public resource is occupied and not occupied by the current thread, then the lock will fail. Therefore, it can be determined that the added locks of ReentrantLock are exclusive locks regardless of read operation or write operation.

Seven, explicit lock and implicit lock

Explicit lock: Lock Implicit lock: Synchronize

What is the difference between explicit lock and implicit lock?

1. Different levels

Synchronize: The keywords in Java are maintained by the JVM. It is a lock at the JVM level.

Lock: It is a specific class that only appeared after JDK5. Using lock is to call the corresponding API. It is a lock at the API level

2. Different ways of use

Synchronize: There is no need to manually lock and unlock, the system will automatically release the lock, which is maintained by the system, and generally no deadlock will occur unless there is a logic problem

Lock: Need to manually lock and unlock, not manually unlocking will cause deadlock. Manual lock method: lock.lock(). Release the lock: unlock method. Need to cooperate with tyr/finaly statement block to complete.

3. Whether the waiting can be interrupted

Synchronize: Cannot be interrupted unless an exception is thrown or execution is complete.

Lock: Can be interrupted. Call the set timeout method tryLock(long timeout, timeUnit unit), then call lockInterruptibly() into the code block, and then call the interrupt() method to interrupt

4. Is it a fair lock

Synchronize: Unfair lock

Lock: Both are okay. The default is unfair lock. You can pass in Boolean values ​​in its construction method.

5. The lock binds multiple conditions to condition

Synchronize: Either randomly wake up one thread, or wake up all threads

Lock: Used to realize the thread that needs to be awakened by group wakeup, which can be accurately awakened

6、

 

 

Guess you like

Origin blog.csdn.net/qq_43037478/article/details/111625768