Thread safety
Brian Goetz, author of Java Concurrency In Practice, defines thread safety as: When multiple threads access to an object, if it’s not the old donkey these threads in the runtime environment of scheduling and execution alternately, also do not need to undertake additional synchronization, or any other coordinated operation in the caller, call the object’s behavior can get the right results, that the object is thread-safe.
Thread safety in the Java language
The data shared by various operations in the Java language can be divided into five categories, in order of thread safety “safety” from strongest to weakest: immutable, absolute thread-safe, relative thread-safe, thread-compatible, and thread-oppositional.
-
immutable
In the Java language, immutable objects must be thread-safe, and neither the object’s method implementation nor the method caller needs to take any thread-safe measures. If the shared data is a basic data type, then it is guaranteed to be immutable as long as the final keyword is used in the definition. If you are sharing an object, you need to ensure that the behavior of the object does not affect its state, such as the java.lang.string class.
-
Absolute thread safety
Absolute thread-safety fulfills Brian Goetz’s definition of thread-safety, and it usually takes a lot of effort for a class to reach a point where “no additional synchronization measures are required by the caller, regardless of the runtime environment.” Or even unrealistic costs.
-
Relative thread safety
Relative thread safety is what we generally speaking thread-safe, it need to make sure that this object is a separate operation is thread-safe, we don’t need the extra security measures when call, but for some particular order calls in a row, can we call end use additional synchronization methods are needed to ensure the correctness of the call.
-
The thread is compatible with
Thread-compatible means that the object itself is not thread-safe, but can be safely used in a concurrent environment by using synchronization methods correctly on the calling side. For example: the ArrayList class and the HashMap class.
-
Thread opposite
Threading opposition refers to code that cannot be used concurrently in a multithreaded environment regardless of whether the caller takes steps to synchronize or not. For example, with the suspend() and resume() methods of the Thread class, if two threads hold a Thread object at the same time, one trying to interrupt the Thread and the other trying to resume the Thread, the target Thread runs the risk of deadlock if executed concurrently, regardless of whether the call was synchronized or not.
Thread safe implementation
-
The mutex synchronization
Mutual-exclusive synchronization is a common means of ensuring the correct concurrency. Synchronization is when multiple threads access shared data concurrently, ensuring that the shared data is used by only one (or several, when using semaphore) thread at the same time. Mutual exclusion is a means to achieve synchronization, and critical sections, mutexes and semaphore are the main ways to achieve mutual exclusion.
In Java, the most basic means of mutually exclusive synchronization is the synchronized keyword, which, when compiled, forms the bytecode instructions Monitorenter and Monitorexit, respectively, before and after the synchronized block. Both of these bytecodes require a parameter of type Reference to indicate the object to lock and unlock. In the virtual machine specification’s description of the behavior of Monitorenter and MonitoreXit, synchronized blocks are reentrant to the same thread and do not lock themselves. The synchronization block blocks subsequent threads from entering until the already entered thread has finished executing.
In addition to synchronized, you can also use ReentrantLock in the java.util.concurrent package. ReentrantLock adds several advanced features over synchronized, including three: wait interruptible, fair locks can be implemented, and locks can bind multiple conditions.
- Wait interruptible means that when the thread holding the lock does not release the lock for a long time, the waiting thread can choose to give up waiting and do something else instead. The interruptible feature is helpful for processing synchronization blocks that take a long time to execute.
- Fair lock means that when multiple threads are waiting for the same lock, they must obtain the lock in the time order of applying for the lock. A non-fair lock does not guarantee this. When the lock is released, any thread waiting for the lock has a chance to acquire it. Locks in synchronized are unfair, and ReentrantLock is unfair by default, but fair locks can be required through a constructor with a Boolean value.
- Lock binding multiple conditions means that a ReentrantLock object can bind multiple conditions simultaneously. In synchronized, the wait() and notify() or notifyAll() methods of a lock object can implement an implied Condition. ReentrantLock does not need to add an extra lock if it is associated with more than one condition. It simply calls the newCondition() method several times.
-
Nonblocking synchronization
The primary problem with mutually exclusive synchronization is the performance problem with thread blocking and wake-up, so this synchronization is also called blocking synchronization. As the hardware instruction set evolved, we had an alternative, an optimistic concurrency strategy based on collision detection, which, in general, was to operate first and succeed if no other thread competed for the shared data. Many implementations of this optimistic concurrency strategy do not require threads to be suspended, so this synchronous operation is called non-blocking synchronization.
CAS directive: The CAS directive requires three operands, which are the memory location (which in Java can be simply understood as the memory address of A variable, represented by V), the old expected value (represented by A), and the new value (represented by B). When the CAS instruction executes, the processor updates the value of V with the new value B if and only if V conforms to the old expected value A, otherwise it does not perform the update, but returns the old value of V regardless of whether the value of V has been updated. This processing is an atomic operation.
A logic loophole in CAS: If A variable V was first read with A value, and when it is assigned, it is checked that it is still A value, can we say that its value has not been changed by other threads? If its value has been changed to B in the intervening period and then changed back to A, the CAS operation will assume that it has never changed. This vulnerability is known as the “ABA” problem of CAS manipulation.
-
Asynchrony scheme
If a method does not inherently involve sharing data, then it does not require any synchronization measures to ensure correctness, so some code is inherently thread-safe.
-
Reentrant code
If a method returns a predictable result and always returns the same result as long as the same data is entered, then it meets the requirement of reentrancy and is thread-safe.
-
Thread local storage
If the data needed in one piece of code must be shared with other code, is the code that shares the data guaranteed to execute in the same thread? If you do, you can limit the visibility of the shared data to the same thread, thus ensuring that there is no data contention between threads without synchronization.
-
Lock the optimization
Spin lock and adaptive lock
The most significant impact of mutually exclusive synchronization on performance is the implementation of blocking. The operations of suspending threads and resuming threads need to be completed in the kernel state. These operations put great pressure on the system’s concurrent performance. At the same time, the virtual machine development team noted that in many applications, shared data is locked for only a short period of time, and it is not worth suspending and resuming threads for that time. If there is more than one processor on the physical machine that allows two or more threads to execute simultaneously in parallel, we can ask the later thread requesting the lock to “hold on” without giving up the processor’s execution time, and see if the thread holding the lock releases the lock soon. To make the thread wait, all we need to do is make the thread execute a busy loop, a technique known as spin locking.
An adaptive spin is one in which the time of the spin is no longer fixed, but is determined by the time of the previous spin on the same lock and the state of the owner. If the spin wait has just successfully acquired the lock on the same lock object, and the thread holding the lock is running, the virtual machine will assume that the spin is likely to succeed again this time, and it will allow the spin wait to last for a relatively longer time. In addition, if the spin is rarely successfully acquired for a lock, then the spin process may be omitted in future attempts to acquire the lock to avoid wasting processor resources.
Lock elimination
Lock elimination refers to the virtual machine real-time compiler in the runtime, some code requires synchronization, but is detected that there is no possible shared data competition lock elimination.
Lock coarsening
In principle, when writing code, it is always recommended to keep the scope of the synchronization block as small as possible — to synchronize only within the actual scope of the shared data, so that the number of operations that need to be synchronized is as small as possible, and if there is a lock contention, the waiting thread can get the lock as quickly as possible. For the most part, this principle is correct, but if a series of consecutive operations lock and unlock the same object repeatedly, even if the locking occurs within the body of a loop, frequent mutually exclusive synchronization operations without thread contention can cause unnecessary performance cost. If the virtual machine detects such a fragmented sequence of operations all locking on the same object, the lock synchronization scope is extended (coarsed) outside the entire sequence of operations.