Thread safety

Java Concurrency In Practice author Brian Goetz defines thread safety as follows: 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 classified into five categories: immutable, absolute thread-safe, relative thread-safe, thread-compatible, and thread-antagonistic, in order of “safety” from strong to weak.

  1. immutable

    In the Java language, immutable objects must be thread-safe, and neither the object’s method implementation nor its callers need any thread-safe safeguards. If shared data is a basic data type, it is guaranteed to be immutable simply by using the final keyword modifier when defining it. If you are sharing an object, you need to ensure that the behavior of the object does not affect its state in any way, such as the java.lang.String class.

  2. Absolute thread safety

    Absolute thread-safety meets Brian Goetz’s definition of thread-safety, and it usually takes a lot of effort for a class to achieve that “no matter what the runtime environment is, the caller doesn’t need any additional synchronization.” Even at an unrealistic cost.

  3. 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.

  4. 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 properly using synchronization on the calling side. Examples are the ArrayList class and the HashMap class.

  5. Thread opposite

    Thread opposition is code that cannot be used concurrently in a multithreaded environment, regardless of whether synchronization is taken on the calling end. For example, the suspend() and resume() methods of the Thread class, if there are two threads holding a Thread object, one trying to interrupt the Thread, the other trying to resume the Thread, if executed concurrently, regardless of whether the call is synchronized or not, the target Thread is at risk of deadlock.

Thread-safe implementation

  • The mutex synchronization

    Mutually exclusive synchronization is a common concurrency guarantee. Synchronization refers to ensuring that shared data is only used by one (or a few, when using semaphores) thread at a time when multiple threads concurrently access the data. The mutex is a means to realize synchronization, and the critical region, mutex and semaphore are the main ways to realize the mutex.

    In Java, the most basic means of mutually exclusive synchronization is the synchronized keyword, which, after compilation, forms two bytecode instructions, Monitorenter and Monitorexit, respectively, before and after the synchronized block. Both bytecodes require a reference type parameter to specify which object to lock and unlock. Synchronized blocks are reentrant to the same thread and do not lock themselves in the behavior described in the VIRTUAL machine specification for Monitorenter and Monitorexit. The synchronization block blocks subsequent threads until the incoming thread finishes executing.

    In addition to synchronized, you can use a ReentrantLock in the java.util.concurrent package to achieve synchronization. Compared with synchronized, ReentrantLock adds some advanced functions, mainly including three: wait can be interrupted, can achieve fair lock, and lock can bind multiple conditions.

    • Wait interruptible means that when the thread holding the lock does not release the lock for a long time, the thread waiting can choose to abandon the wait and process other things instead. The interruptible feature is helpful for processing synchronous blocks with very long execution time.

    • Fair lock means that when multiple threads are waiting for the same lock, they must obtain the lock in sequence according to the time sequence of lock application. Non-fair locks do not guarantee this, and any thread waiting for the lock has the opportunity to acquire it when the lock is released. A lock in synchronized is unfair, and a ReentrantLock is unfair by default, but a fair lock can be demanded through a constructor with a Boolean value.

    • Locking multiple conditions means that a ReentrantLock object can bind multiple conditions simultaneously, whereas in synchronized, the wait() and notify() or notifyAll() methods of a lock object can implement an implicit Condition, ReentrantLock does not have to add an additional lock to associate with more than one condition, but simply calls the newCondition() method multiple times.

  • Nonblocking synchronization

    The main problem with mutex synchronization is the performance problems associated with thread blocking and waking up, so it is also called blocking synchronization. With the development of hardware instruction sets, we have an alternative, optimistic concurrency strategy based on collision detection. In popular terms, the operation is performed first, and if no other threads compete for the shared data, the operation succeeds. Many implementations of this optimistic concurrency strategy do not require threads to be suspended, so this synchronization operation is called non-blocking synchronization.

    CAS instruction: The CAS instruction requires three operands, namely the memory location (which in Java is simply 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 handler updates the value of V with the new value B if and only if V matches the old expected value A, otherwise it does not perform the update, but returns the old value of V whether or not it has updated the value of V. This processing is an atomic operation.

    Logic loophole in CAS: If A variable V is A value when it is first read and is still A value when it is ready to assign, can we say that its value has not been changed by another thread? If its value was changed to B during that time and then changed back to A, the CAS operation would assume that it had never changed. This vulnerability is known as the “ABA” problem of CAS operations.

  • Asynchronous scheme

    If a method does not inherently involve sharing data, it naturally does not require any synchronization to ensure correctness, so some code is inherently thread-safe.

    • Reentrant code

      A method is thread-safe if it returns a predictable result that returns the same result as long as it inputs the same data.

    • Thread local storage

      If the data needed in one piece of code must be shared with other code, can the code that shares the data be guaranteed to execute in the same thread? If so, you can limit the visibility of shared data to the same thread, so that synchronization is not required to ensure that data contention between threads does not occur.

Lock the optimization

Spin locking and adaptive locking

The most significant impact on the performance of mutex synchronization is the implementation of blocking. The operations of suspending and resuming threads need to be completed in the kernel state, which brings great pressure to the concurrent performance of the system. At the same time, the virtual machine development team has noticed that in many applications, shared data is locked for only a short period of time, which is not worth suspending and resuming threads. If there is more than one processor on the physical machine and two or more threads can execute in parallel at the same time, we can tell the subsequent thread requesting the lock to “hold on,” but not give up the processor’s execution time, to see if the thread holding the lock will release the lock soon. To make the thread wait, we simply make the thread execute a busy loop, a technique known as spin locking.

Adaptive spin means that the spin time 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 wait is likely to succeed again, and will allow the spin wait to last a relatively long time. Also, if the spin is rarely successfully acquired for a lock, it is possible to omit the spin process in future attempts to acquire the lock to avoid wasting processor resources.

Lock elimination

Lock elimination refers to the elimination of locks that require synchronization on some code but are detected as impossible to compete for shared data when the virtual machine just-in-time compiler runs.

Lock coarsening

In principle, when writing code, it is always recommended to keep the scope of synchronized blocks as small as possible — to synchronize only in the actual scope of the shared data — so that the number of operations that need to be synchronized is as small as possible and that waiting threads can acquire the lock as quickly as possible if there is a lock contention. This principle is true in most cases, but if a series of consecutive operations repeatedly lock and unlock the same object, even if the locking operation occurs in the body of the loop, frequent mutex synchronization can cause unnecessary performance losses even if there is no thread contention. If the virtual machine detects a string of fragmented operations that lock the same object, the lock synchronization scope will be extended (coarsened) outside the entire operation sequence.