What are we talking about when we talk about thread safety

July 25, 2017 1511 point heat 0 likes 0 comments

When it comes to thread safety, perhaps everyone's first reaction is to ensure that the interface's operations on shared variables are atomic. In fact, in multithreaded programming, we need to pay attention to visibility, order and atomicity at the same time. This article will start from these three problems and explain in detail how volatile can ensure visibility and order in certain procedures with examples. At the same time, it will illustrate how synchronized can ensure visibility and atomicity at the same time. Finally, it will compare the applicable scenarios of volatile and synchronized.

Three core concepts in multithreaded programming

Atomicity

This is similar to the atomicity concept of database transactions, that is, an operation (which may contain multiple sub operations) is either fully executed (effective) or not executed (ineffective).

A classic example of atomicity is bank transfer: for example, A and B transfer 100000 yuan to C at the same time. If the transfer operation does not have atomicity, when A transfers to C, it reads C's balance as 200000 yuan, and then adds 100000 yuan to the transferred amount. It is calculated that there should be 300000 yuan at this time, but 300000 yuan will be written back to C's account in the future. At this time, B's transfer request comes, and B finds that C's balance is 200000 yuan, and then adds 100000 yuan to it and writes back. Then A's transfer operation continues - write 300000 back to C's balance. In this case, the final balance of C is 300000 instead of the expected 400000.

visibility

Visibility means that when multiple threads access a shared variable concurrently, the modification of a shared variable by one thread can be immediately seen by other threads. Visibility is something that many people ignore or misunderstand.

The efficiency of CPU reading data from main memory is relatively low. Now, mainstream computers have several levels of cache. When each thread reads a shared variable, it will load the variable into the cache of its corresponding CPU. After modifying the variable, the CPU will immediately update the cache, but not necessarily write it back to main memory immediately (actually, the time of writing it back to main memory is unpredictable). When other threads (especially threads not executing on the same CPU) access this variable, they read the old data from the main memory, not the updated data of the first thread.

This is an operating system or hardware level mechanism, so many application developers often ignore it.

Sequential

Sequential refers to the sequence of program execution according to the sequence of code execution.

Take the following code as an example

one
two
three
four
boolean started = false ; //Statement 1
long counter = 0L ; //Statement 2
counter = one ; //Statement 3
started = true ; //Statement 4

From the code order, the above four statements should be executed in sequence, but in fact, when the JVM actually executes this code, it does not guarantee that they will be executed in this order.

In order to improve the overall execution efficiency of the program, the processor may optimize the code. One of the optimization methods is to adjust the code order and execute the code in a more efficient order.

At this point, someone has to worry - what, the CPU does not execute the code in the order of my code, so how can we ensure that we get the effect we want? In fact, you can rest assured that although the CPU does not guarantee that the program will be executed in full code order, it will ensure that the final execution result of the program is consistent with the result of code order execution.

How Java solves the problem of multithreading concurrency

How Java guarantees atomicity

Locks and Synchronization

The commonly used tools to ensure the atomicity of Java operations are locks and synchronization methods (or synchronized code blocks). Using locks can ensure that only one thread can get the lock at the same time, and that only one thread can execute the code between lock application and lock release at the same time.

one
two
three
four
five
six
seven
eight
nine
public void testLock () {
lock.lock();
try {
int j = i;
i = j + one ;
} finally {
lock.unlock();
}
}

Similar to locks are synchronization methods or synchronization code blocks. When using the non static synchronization method, the current instance is locked; When using the static synchronization method, the class object of this class is locked; When using static code blocks, the lock is synchronized The object in parentheses after the keyword. The following is an example of a synchronized code block

one
two
three
four
five
six
public void testLock () {
synchronized (anyObject){
int j = i;
i = j + one ;
}
}

Whether locks or synchronized are used, the essence is the same. Resources are exclusive through locks, so that the actual target code segment can only be executed by one thread at a time, thus ensuring the atomicity of the target code segment. This is an approach at the expense of performance.

CAS(compare and swap)

The increment of basic type variable (i++) is an operation that is often mistaken by novices for atomic operation rather than actual operation. Java provides a corresponding atomic operation class to implement this operation and ensure atomicity. Its essence is to use CPU level CAS instructions. Because it is a CPU level instruction, its cost is less than that of locks that require the participation of the operating system. AtomicInteger can be used as follows.

one
two
three
four
five
six
seven
eight
AtomicInteger atomicInteger = new AtomicInteger();
for ( int b = zero ; b < numThreads; b++) {
new Thread(() -> {
for ( int a = zero ; a < iteration; a++) {
atomicInteger.incrementAndGet();
}
}).start();
}

How Java ensures visibility

Java provides volatile Keyword to ensure visibility. When volatile is used to decorate a variable, it will ensure that the modification of the variable will be immediately updated to memory, and the cache of the variable in other caches will be set to invalid. Therefore, when other threads need to read the value, they must read it from the main memory to get the latest value.

How Java guarantees orderliness

As mentioned above, when the compiler and processor reorder the instructions, they will ensure that the reordered execution results are consistent with the sequential code execution results. Therefore, the reordering process will not affect the execution of single threaded programs, but may affect the correctness of concurrent execution of multi-threaded programs.

Java can be accessed through volatile Sequence can be guaranteed in certain programs. In addition, it can be guaranteed through synchronized and locks.

Synchronized and locks guarantee order and atomicity by ensuring that only one thread will execute the target code segment at the same time.

In addition to guaranteeing the execution order of the target code segment from the application level, the JVM also implicitly guarantees the order through the so-called happens before principle. As long as the execution order of the two operations can be deduced from the happens before, the JVM will guarantee their order. On the contrary, the JVM does not guarantee their order, and can reorder them as necessary to achieve high efficiency.

Happens before principle

  • Transfer rule: If operation 1 precedes operation 2 and operation 2 precedes operation 3, operation 1 will definitely occur before operation 3. This rule shows that the happens before principle is transitive
  • Locking rule: an unlock operation will definitely occur before the subsequent lock operation on the same lock. This is easy to understand. The lock can only be acquired again after it is released
  • Volatile variable rule: a write operation modified by volatile occurs first in the subsequent read operation of the variable
  • Program order rule: execute in code order within a thread
  • Thread startup rule: the start() method of the Thread object occurs first in other actions of this thread
  • Thread termination principle: all other operations in the thread after thread termination detection
  • Thread interrupt rule: the call to thread interrupt () method occurs first when the interrupt exception is obtained
  • Object termination rule: an object's construction occurs before its finalization

Volatile application scenarios

Volatile is applicable to scenarios where atomicity is not required, but visibility is required. A typical usage scenario is to use it to decorate the status flag used to stop threads. As shown below

one
two
three
four
five
six
seven
eight
nine
ten
eleven
twelve
thirteen
boolean isRunning = false ;
public void start () {
new Thread( () -> {
while (isRunning) {
someOperation();
}
}).start();
}
public void stop () {
isRunning = false ;
}

In this implementation mode, even if other threads set isRunning to false by calling the stop() method, the loop may not end immediately. The volatile keyword can be used to ensure that the while loop gets the latest status of isRunning in time, thus stopping the loop in time and ending the thread.

Thread safety 100000 why

Q: Usually, locks and synchronized are used more frequently in projects, but volatile is rarely used. Is visibility not guaranteed?
Answer: Locks and synchronized can guarantee both atomicity and visibility. This is achieved by ensuring that only one thread executes the target code segment at the same time.

Q: Why can locks and synchronized ensure visibility?
Answer: According to Java doc of JDK 7 Middle pair concurrent Package description. The write result of one thread ensures that the read operation of another thread is visible, as long as the write operation can be happen-before The principle infers that it occurs before the read operation.

The results of a write by one thread are guaranteed to be  visible  to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

Q: Since locks and synchronized can guarantee atomicity and visibility, why do we need volatile?
Answer: Synchronized and locks need to be arbitrated by the operating system to determine who gets the lock. The cost is relatively high, while volatile costs much less. Therefore, the performance of using volatile is much higher than that of using locks and synchronized when only visibility is required.

Q: Since locks and synchronized can guarantee atomicity, why do we need AtomicInteger classes to guarantee atomic operations?
Answer: Lock and synchronized need to arbitrate who gets the lock through the operating system, which has a high overhead. AtomicInteger uses CPU level CAS operations to ensure atomicity, which has a low overhead. Therefore, the purpose of using AtomicInteger is to improve performance.

Q: Is there any other way to ensure thread safety
Answer: Yes. Try to avoid the condition that causes non thread safety - shared variables. If the use of shared variables can be avoided by design, non thread safety can be avoided, and the problems of atomicity, visibility, and order need not be solved through locks, synchronized, and volatile.

Q: Synchronized can modify non static methods, static methods and code blocks. What's the difference
Answer: When synchronized modifies a non static synchronization method, the current instance is locked; When synchronized modifies a static synchronization method, the class object of the class is locked; When synchronized modifies a static code block, it locks synchronized The object in parentheses after the keyword.

Gcod

If life is just like the first sight, what is the sad autumn wind painting fan

Article comments