05-Thread-java other basic knowledge of concurrency

Thread safety issues in java

Shared resource means that the resource is held by multiple threads or that multiple threads can access the resource.

Thread safety issues refer to problems that cause dirty data or other unforeseen results when multiple threads read and write a shared resource at the same time without any synchronization measures.
Dirty Read means that the data in the source system is no longer available. Within a certain range, it is meaningless to the actual business, or the data format is illegal, and there is irregular coding and vague business logic in the source system.

Memory visibility problem of shared variables in java

Insert picture description here
The Java memory model stipulates that all variables are stored in the main memory. When a thread uses variables, it will copy the variables in the main memory to its own working space or called working memory. The thread operates itself when reading and writing variables. Variables in working memory.

In the java memory model, the thread’s local memory (working memory) corresponds to the CPU’s L1 and L2 caches or registers; when a thread manipulates shared variables, it first copies the shared variables from the main memory to its own working memory, and then changes to the working memory The variable is processed, and the variable value is updated to the main memory after processing. At this time, due to the existence of Cache, the problem of invisible memory will be caused.

Using volatile in Java can solve the problem of invisible memory.

synchronized keyword

Synchronized block is an atomic built-in lock provided by Java. Every object in Java can be used as a synchronized lock. These built-in Java locks that users cannot see are called internal locks, also called Make a monitor lock. The execution code of the thread will automatically acquire the internal lock before entering the synchronized code block. At this time, other threads will be blocked and suspended when accessing the synchronized code block. The thread that has acquired the internal lock will release the built-in lock after it exits the synchronization code block normally or throws an exception, or when the wait series method of the built-in lock resource is called in the synchronization block. The built-in lock is an exclusive lock, that is, when a thread acquires the lock, other threads must wait for the thread to release the lock before acquiring the lock.
In addition, because the threads in Java correspond to the native threads of the operating system one-to-one, when a thread is blocked, it is necessary to switch from the user mode to the kernel mode to perform the blocking operation. This is a very time-consuming operation, and the use of synchronized Context switch

Synchronized memory semantics

The memory semantics of entering the synchronized block is to clear the variables used in the synchronized block from the working memory of the thread, so that when the variable is used in the synchronized block, it will not be obtained from the working memory of the thread, but directly from the main Get in memory. The memory semantics of exiting the synchronized block is to flush the modification of shared variables in the synchronized block to the main memory.

This is also the semantics of locking and releasing the lock. When the lock is acquired, the shared variables that will be used in the local memory in the lock block will be cleared. When these shared variables are used, they will be loaded from the main memory, and the local memory will be released when the lock is released. The shared variable modified in is flushed to the main memory. In addition to solving the problem of shared variable memory visibility, synchronized is often used to achieve atomic operations. Please also note that the synchronized keyword will cause thread context switching and bring thread scheduling overhead.

volatile keyword

Use the synchronized keyword to solve the memory visibility problem by locking, but this method is too cumbersome, because it will bring thread context switching overhead. For memory visibility issues, Java also provides a weak form of synchronization, which is to use the volatile keyword.

This keyword can ensure that updates to a variable are immediately visible to other threads. When a variable is declared as volatile, the thread will not cache the value in a register or other place when writing the variable, but will flush the value back to the main memory. When other threads read the shared variable, they will retrieve the latest value from the main memory instead of using the value in the working memory of the current thread.

Look at an example of using the volatile keyword to solve the memory visibility problem;

//这是线程不安全的例子
public class ThreadNotSafeInteger{
    
    
	private int value;
	public int get(){
    
    
	return value;
	}
	public void set(int value){
    
    
	 this.value =value;
	}
}
//使用synchronized关键字进行同步的方式
public class ThreadNotSafeInteger{
    
    
	private int value;
	public synchronized int get(){
    
    
	return value;
	}
	public synchronized void set(int value){
    
    
	 this.value =value;
	}
}
//这是使用volatile关键字进行同步的方式
public class ThreadNotSafeInteger{
    
    
	private  volatile int value;
	public int get(){
    
    
	return value;
	}
	public void set(int value){
    
    
	 this.value =value;
	}
}

The use of synchronized and volatile here are equivalent, both solve the memory visibility problem of shared variable value, but the former is an exclusive lock, and only one thread can call the get() method at the same time, and other calling threads will be blocked, and at the same time There will be the overhead of thread context switching and thread rescheduling, which is also a disadvantage of using locks. The latter is a non-blocking algorithm and does not cause the overhead of thread context switching.

But not all of them are equivalent to use in all cases. Although volatile provides visibility guarantees, it does not guarantee the atomicity of operations.

Under what circumstances should the volatile keyword be used?
1. When writing the variable value does not depend on the current value of the variable. Because if you rely on the current value, it will be a three-step operation of obtain-calculate-write. These three-step operations are not atomic, and volatile does not guarantee atomicity.
2. There is no lock when reading and writing variable values. Because the lock itself already guarantees memory visibility, there is no need to declare the variable as volatile at this time.

Atomic operations in java

The so-called atomic operation means that when a series of operations are executed, these operations are either all executed or not executed at all. There is no case that only some of them are executed. When designing a counter, generally read the current value first, then +1, and then update. This process is a read-modify-write process. If this process cannot be guaranteed to be atomic, thread safety issues will arise. The following code is not thread-safe, because ++value cannot be guaranteed to be an atomic operation.

public class ThreadNotSafeCount{
    
    
	private Long value;

	public Long getCount(){
    
    
	return value;
	}

	public void inc(){
    
    
	++value;
}

}

If you use the Javap -c command to view the assembly code, you can find that ++value is divided into many steps to complete the operation together. Therefore, the simplest statement such as ++value cannot guarantee atomic operation.

The easiest way is to use the synchronized keyword, which can guarantee atomic operation.

public class TreadSafeCount{
    
    
	private Long value;
	public synchronized Long getCount(){
    
    
	return value;
	}

	public synchronized void inc(){
    
    
	++value;
}

Using the synchronized keyword can indeed achieve thread safety, that is, memory visibility and atomicity, but synchronized is an exclusive lock, threads that have not acquired the internal lock will be blocked, and the getCount method here is just a read operation, and multiple threads are called at the same time There will be no thread safety issues; but after the synchronzied keyword is added, only one thread can be called at the same time, which obviously greatly reduces the concurrency. You may ask, since it is only a read operation, why not remove the synchronized keyword on the getCount method? In fact, it can't be removed, don't forget to test sychronized here to realize the memory visibility of value.

It is a good choice to use AtomicLong implemented by the non-blocking CAS algorithm to achieve atomic operations internally.

CAS operation

In Java, locks occupy a place in concurrent processing, but there is a downside to using locks, that is, when a site does not acquire the lock, it will be blocked and suspended, which will lead to thread context switching and rescheduling overhead. Java provides a non-blocking volatile keyword to solve the visibility problem of shared variables, which makes up for the overhead caused by locks in certain programs. But volatile can only guarantee the visibility of shared variables, and cannot solve the atomic problem of read-modify-write. CAS is a non-blocking atomicity provided by the JDK. It guarantees the atomicity of comparison-update operations through hardware. The Unsafe class in the JDK provides a series of comapreAndSwap methods.

Sometimes the use of CAS cannot guarantee the correct execution of the program. There is a classic ABA problem with CAS operation.

The ABA problem means that during the CAS operation, other threads change the variable value A to B, but it is changed back to A. When this thread uses the expected value A to compare with the current variable, it is found that the variable A has not changed, so CAS changes The value of A has been exchanged, but in fact the value has been changed by other threads, which is inconsistent with the design idea of ​​optimistic locking. The solution to the ABA problem is to add 1 to the version number of the variable every time the variable is updated, then ABA will become A1-B2-A3. As long as the variable is modified by a thread, the version number corresponding to the change will be Incremental changes occur, thereby solving the ABA problem.

Unsafe class

The Unsafe class in the JDK's rt.jar package provides hardware-level atomic operations. The methods in the Unsafe class are all native methods. They use JNI to access the local C++ implementation library.

Read the material: https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

Java instruction reordering

The Java memory model allows the compiler and processor to reorder instructions to improve operating performance, and only reorder instructions that do not have data dependencies. Reordering in a single thread can ensure that the final execution result is consistent with the result of the program execution, but there will be problems in multithreading.

You can use the volatile keyword to avoid instruction reordering and memory visibility issues.

lock

Optimistic and pessimistic locking

Optimistic locking and pessimistic locking are terms introduced in the database, but similar ideas are also introduced in concurrent package locking.
Pessimistic locking refers to a conservative attitude towards data being modified by the outside world, thinking that data is easily modified by other threads, so the data is locked before the data is processed, and the data is locked during the entire data processing process.
Optimistic locks are relatively pessimistic locks. It believes that data will not cause conflicts under normal circumstances, so it will not be exclusively locked before accessing the record, but will formally check whether the data conflicts or not when the data is submitted and updated. Detection.
The realization of pessimistic locking often relies on the locking mechanism provided by the database.
Optimistic locking does not use the locking mechanism provided by the database. It is generally implemented by adding a version field to the table or using business status; optimistic locking is not locked until the commit, so no deadlock will occur.

Fair lock and unfair lock

According to the preemption mechanism of acquiring locks on site, locks can also be divided into fair locks and unfair locks. Fair locks indicate that the order of acquiring locks on site is determined by the time when the thread requests the lock, that is, the thread that requests the lock first will acquire it earliest. To the lock. Unfair locks break in at runtime, that is, first come and not necessarily first served.
ReetrantLock provides the realization of fair and unfair locks.
Fair lock: ReetrantLock fairlock = new ReetrantLock(true)
Unfair lock: ReetrantLock fairlock = new ReetrantLock(false0. If the constructor is not passed, the default is unfair lock
. Try to use unfair locks without fairness requirements, because fairness Locking will bring performance overhead.

Exclusive lock and shared lock

Depending on whether the lock can only be held by a single thread or by multiple threads, locks can be divided into exclusive locks and shared locks.
The exclusive lock guarantees that only one site can get the lock at any time, and ReetrantLock is implemented in an exclusive way. Shared locks can be held by multiple threads at the same time, such as ReadWriteLock, which allows a resource to be read by multiple threads at the same time.

Exclusive lock is a kind of pessimistic lock, because each access to the resource is added to the mutual exclusion lock, which limits concurrency, because the read operation does not affect the consistency of the data, and the exclusive lock only allows one thread at the same time To read data, other threads must wait for the current thread to release the lock before reading.
The shared lock is an optimistic lock, which widens the locking conditions and allows multiple threads to perform read operations at the same time.

Reentrant lock

When a thread wants to acquire an exclusive lock held by another site, the thread will be blocked. Will it be blocked when a thread acquires the lock it has already acquired again? If it is not blocked, then we lock the lock to be reentrant, which means that as long as the thread acquires the lock, it can enter the locked code block nearly countless times.

Let's look at an example, under what circumstances reentrant locks are used.

public class Hello{
    
    
		public synchronized void helloA(){
    
    
		System.out.println("hello");
	}
	public synchronized void helloB(){
    
    
	System.out.println("hello B");
	helloA();
}
}

In the above code, the built-in lock is acquired before calling the helloB method, and then the output is printed. After calling the helloA method, the built-in lock will be acquired before the call. If the built-in lock is not reentrant, the calling thread will always be blocked.
In fact, synchronized internal locks are reentrant locks. The principle of reentrant locks is to maintain a thread mark inside the lock to indicate which thread the lock is currently occupied, and then associate a counter. The counter is 0 at the beginning, indicating that the lock is not occupied by any thread. When a thread acquires the lock, the value of the counter will become 1. When other threads acquire the lock again, they will find that the owner of the lock is not themselves and is blocked and suspended.

Spin lock

Since the threads in Java correspond to the threads in the operating system one-to-one, when a scene fails to acquire a lock (such as an exclusive lock), it will be switched to the kernel state and suspended. When the thread acquires the lock, it needs to switch to the kernel state to wake up the thread. However, the overhead of switching from the user state to the kernel state is very large, which will affect the concurrent performance in a certain program. The spin lock is that when the current thread acquires the lock, if it finds that the lock is already occupied by other threads, it will not block itself immediately. Without giving up the right to use the CPU, it will try to acquire it multiple times (the default number is 10, you can use it) -XX:PreBlockSpinsh parameter sets this value), it is very likely that other threads have released the lock in the next few attempts. If the lock is not acquired after the specified number of attempts, the current thread will be blocked and suspended. It can be seen from this that spinlock uses CPU time in exchange for the overhead of thread blocking and scheduling, but it is very likely that this CPU time will be wasted.

Guess you like

Origin blog.csdn.net/qq_41729287/article/details/113941606