This article is participating in “Java Theme Month – Java Debug Notes Event”, see < Event link > for more details.
The use of threads has always been difficult to control. When used properly, threads can effectively reduce costs such as program development and maintenance, while improving the performance of complex applications. In GUI applications, improve user interface responsiveness, and in server applications, improve resource utilization and system throughput.
However, threads can carry a number of unpredictable risks if not used properly. Java’s support for threads is a double-edged sword. Although Java is a cross-platform language (writing is unusual, run anywhere), the JDK and provides the corresponding class libraries, simplify the procedure of development, but more in the treatment of complicated application, will need to use the thread, then introduce the problem of “concurrency” (thread safety), became the developers consider the difficulty.
In multithreading, the order of operations is unpredictable, and sometimes the results are unexpected and surprising. For example, the numeric sequence generator in the following code snippet is used to generate an incrementing sequence.
package com.xcbeyond.thread;
import net.jcip.annotations.NotThreadSafe;
/** * non-thread-safe numeric sequence generator *@authorXcbeyond * 2018-5-6 PM 03:17:33 */
public class UnSafeThreadSequence {
private int value;
/** * returns a unique value *@return* /
public int getValue(a) {
returnvalue++; }}Copy the code
If executed in a single thread, there are no problems and the results are as expected. In the case of multi-threaded concurrent operation, there will be alternating operations between different threads. Different threads are likely to read the same value and get the same value at the same time, resulting in different threads returning the same sequence value, which is exactly the opposite of our expectation.
This illustrates a common thread concurrency hazard: race conditions. Because threads share the same memory space address and execute concurrently, they may access or modify variables that are being used by other threads, and variable contention can occur.
Thread safety refers to the locking mechanism used in multi-thread access. When one thread accesses a certain data of the class, it is protected and cannot be accessed by other threads until the thread finishes reading the data. There will be no data inconsistencies or data contamination. Thread insecurity means that data access protection is not provided. Multiple threads may change data successively, resulting in dirty data.
How do you write thread-safe code?
The core of writing thread-safe code is to manage state access, especially for Shared and Mutable states. Whether an object is needed to be thread-safe depends on whether the object is accessed by multiple threads. This refers to how objects are accessed in the program, not what they are intended to do. To make an object thread-safe, synchronization mechanisms are used to coordinate access to the mutable state of the object. Java’s common synchronization mechanism is Synchronized. It also includes volatile variables, display locks, and atomic variables.
Atomicity:
Suppose there are two operations A and B that are atomic to each other if, from the point of view of the thread executing A, the other thread executing B either completes B completely or does not execute B at all. An atomic operation is an operation that is performed atomically for all operations that access the same state, including the operation itself.
Race Condition: The correctness of a calculation/program depends on the timing of the alternate execution of multiple threads. (The result may be different depending on the timing of the thread)
“Check before you execute,” a potential transition value to determine what to do next.
A condition is observed to be true and the relevant program is executed, but in a multithreaded environment, the observation may become invalid between the result of the condition judgment and the start of the program execution (another thread performs the relevant action in the meantime), resulting in invalidation.
Read-modify-write defines the state transition of an object based on its previous state. Even volatile variables are subject to race conditions when incrementing in a multithreaded environment, so volatile does not guarantee absolute thread-safety.
Locking mechanism:
In the definition of thread-safety, the invariance condition must not be broken regardless of the execution timing or alternation of operations between multiple threads. When multiple variables are involved in invariance conditions, each variable is not independent of each other, and the change of one variable will constrain the values of other variables. Thus, when one variable is changed, other related variables are updated within the same atomic operation.
Built-in lock:
A Synchronized Block consists of two parts: an object reference that acts as a lock and a Block of code that is protected by the lock. The keyword Synchronized modifier is a block of Synchronized code, the lock is the object on which the method is called, and a static Synchronized method uses a Class object as the lock. Built-in or monitor locks are those that synchronize objects.
Built-in locks act as mutex locks in Java, meaning that at most one thread can own the lock. When thread A attempts to request A lock held by thread B, thread A must wait or block until thread B releases the lock. If B does not release the lock, A will wait forever.
While synchronized code blocks address thread-safety issues, threads can wait or block, resulting in poor response performance. The size of each synchronized code block can be adjusted as much as possible, i.e., the synchronized code block is “small enough” to solve both the thread-safety problem and the poor performance problem. Determining the appropriate size for a synchronized block of code requires trade-offs between various design requirements, including security (which must be met), simplicity, and performance.