What are we talking about when we talk about thread safety

July 25, 2017 790 point heat 0 liked it 0 comments

When it comes to thread safety, perhaps your first reaction is to ensure that the interface's operations on shared variables should be specific and atomic. In fact, in multithreaded programming, we need to focus on visibility, ordering, and atomicity at the same time. Starting from these three problems, this article will explain how volatile ensures visibility and sequence in a certain program, and how synchronized ensures visibility and atomicity at the same time. Finally, it compares the applicable scenarios of volatile and synchronized.

Three core concepts in multithreading programming

Atomicity

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

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 is not atomic, a reads the balance of C as 200000 when transferring to C, and then adds the 100000 of the transfer, and calculates that there should be 300000 at this time, but also returns the 300000 in the future and writes it back to the account of C. at this time, the transfer request of B comes, and B finds that the balance of C is 200000, and then adds 100000 and writes back. Then a's transfer continues - write 300000 back to C's balance. In this case, the final balance of C is 300000, not the expected 400000.

visibility

Visibility means that when multiple threads access shared variables simultaneously, other threads can immediately see the changes made by one thread. Visibility is something that many people ignore or understand wrong.

CPU read data from the main memory efficiency is relatively low, now mainstream computers, there are several levels of cache. When each thread reads a shared variable, it will be loaded into the cache of its corresponding CPU. After modifying the variable, the CPU will immediately update the cache, but it will not necessarily write it back to main memory immediately (in fact, the write back time to main memory is unpredictable). When other threads (especially those not executing on the same CPU) access the variable, they will read the old data from the main memory instead of the updated data of the first thread.

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

Sequence

Sequence refers to that the sequence of program execution follows 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

In terms of code order, the above four statements should be executed in turn, but in fact, when the JVM 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 according to my code order, so how can we ensure the effect we want? In fact, you can rest assured that although the CPU does not guarantee that the code is executed completely, it will ensure that the final execution result of the program is consistent with that of the code sequence execution.

How to solve the problem of multithreading concurrency in Java

How Java guarantees atomicity

Locking and synchronization

Common tools to ensure the atomicity of java operations are locking and synchronization methods (or synchronized code blocks). Using locks can ensure that only one thread can get the lock at the same time, which also ensures that only one thread can execute the code between applying for the lock and releasing the lock 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 synchronized code blocks. When using the non static synchronization method, the current instance is locked; When static synchronization method is used, the class object of this class is locked; When using static code blocks, what is locked is synchronized The object in parentheses after the keyword. Here is an example of a synchronization code block

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

Whether lock or synchronized is used, the essence is the same. Lock is used to realize the exclusivity of resources, so that the actual object code segment can only be executed by one thread at the same time, thus ensuring the atomicity of the object code segment. This is an approach at the expense of performance.

CAS(compare and swap)

Basic type variable auto increment (I + +) is a kind of operation which is often mistaken by novice as atomic operation but not actually. Java provides the corresponding atomic operation class to implement the operation and ensure atomicity. Its essence is to use CAS instructions at CPU level. Because it is a CPU level instruction, its cost is less than that of the lock that requires the operating system to participate. 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 using volatile to modify a variable, it will ensure that the modification of the variable will be immediately updated to the memory, and set the cache of the variable in other caches to be invalid. Therefore, other threads must read the value from the main memory to get the latest value.

How does Java ensure ordering

As mentioned above, when the compiler and processor reorder instructions, they will ensure that the execution result after reordering is consistent with that of code sequence execution. Therefore, the reordering process will not affect the execution of single threaded programs, but may affect the correctness of concurrent execution of multithreaded programs.

In Java, the volatile In addition, we can ensure the sequence through synchronized and lock.

The principle of synchronized and lock to ensure sequentiality and atomicity are realized by ensuring that only one thread will execute the target code segment at the same time.

In addition to ensuring the sequence of object code segment execution from the application level, the JVM also implicitly guarantees the sequence through a principle called happens before. As long as the execution order of the two operations can be deduced by happens before, the JVM will guarantee the order of the two operations. Otherwise, the JVM does not guarantee the order of the two operations, and can reorder them as necessary to obtain high efficiency.

Happens before principle

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

Applicable scenarios of volatile

Volatile is suitable for 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 a thread. 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, even if other threads set isrunning to false by calling the stop() method, the loop does not necessarily 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 and ending the thread in time.

Thread safety 100000 why

Q: usually, locks and synchronized are used more often in projects, but volatile is rarely used. Is there no guarantee of visibility?
A: locks and synchronized guarantee both atomicity and visibility. They are implemented by ensuring that only one thread executes the target code segment at the same time.

Question: why do locks and synchronized guarantee visibility?
A: according to the Java DOC of JDK 7 China vs concurrent Package description. The write result of one thread is guaranteed to be visible to the read operation of another thread, as long as the write operation can be accessed by the 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 locking and synchronized guarantee atomicity and visibility, why volatile is needed?
A: synchronized and locks need to arbitrate who gets the lock through the operating system, which costs a lot more than volatile. Therefore, the performance of volatile is much higher than that of locking and synchronized when only visibility is guaranteed.

Q: since locks and synchronized guarantee atomicity, why do you need classes like atomicinteger to guarantee atomic operations?
A: locking and synchronized need to arbitrate who gets the lock through the operating system, which costs a lot. Atomicinteger ensures atomicity through CAS operation at CPU level, and the overhead is relatively small. So the purpose of using atomicinteger is to improve performance.

Q: is there any other way to ensure thread safety
A: Yes. Try to avoid non thread safe conditions -- shared variables. If we can avoid the use of shared variables in design, we can avoid the occurrence of non thread safety, and we don't need to solve the problems of atomicity, visibility and order through locks, synchronized and volatile.

Question: what's the difference between synchronized and non static methods, static methods, and code blocks
A: when synchronized modifies non static synchronous methods, the current instance is locked; When synchronized modifies a static synchronization method, the class object of this class is locked; When synchronized modifies a static block of code, what is locked is 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 review