0 foreword

Thread-safety issues do not occur in a single thread, whereas in multithreaded programming, it is possible to access the same shared, mutable resource simultaneously: a variable, an object, a file, and so on. Two points in particular:

  1. Shared: means that the resource can be accessed by multiple threads at the same time.
  2. Mutable: means that the resource can be modified during its lifetime;

Simply put, your code is thread-safe if it always gets the same result when executing in a single thread as it does when executing in multiple threads. So what are the thread-safety requirements we face when doing multithreaded programming? And how to solve it?

1 Thread safety features

1.1 atomic

Similar to the concept of atomicity in database transactions, an operation (which may contain more than one child operation) either executes at all (effective) or does not execute at all (ineffective).

A classic example of atomicity is the bank transfer problem:

For example, A and B transfer 100,000 yuan to C at the same time. If you don’t have atomic transfer operation, A to C transfer, read the C A balance of 200000, 100000, then add transfer calculated at this time there should be 300000, but also the future and write 300000 back to C account, B transfer request at this time come over and find A balance of 200000 C B, then add 100000 and write back. Then A’s transfer continues — write the $300,000 back to C’s balance. In this case, the final balance of C is $300,000, not $400,000 as expected.

1.2 the visibility

Visibility means that when multiple threads concurrently access a shared variable, changes made to the shared variable by one thread are immediately visible to other threads. The visibility issue is one that many people ignore or misunderstand.

The CPU is relatively inefficient at reading data from main memory, and most modern computers have several levels of caching. When each thread reads a shared variable, it loads the variable into its corresponding CPU’s cache. After modifying the variable, the CPU updates the cache immediately, but does not necessarily write it back to main memory immediately (in practice, writing it back to main memory takes an unpredictable amount of time). When other threads (especially threads not executing on the same CPU) access the variable, they read the old data from main memory instead of the updated data from the first thread.

This is an operating system or hardware level mechanism that many application developers often overlook.

1.3 order

Orderliness means that the order in which the program is executed is the order in which the code is executed. Take this code as an example:

boolean started = false; / / 1
long counter = 0L; / / 2
counter = 1; / / 3
started = true; / / 4
Copy the code

In code order, the above four statements should be executed sequentially, but the JVM does not guarantee that they will be executed exactly in that order when the code is actually executed.

The processor may optimize the code to improve the overall efficiency of the program. One way to optimize the code is to adjust the code order so that the code is executed in a more efficient order.

At this point, someone has to worry — what, the CPU doesn’t execute my code in the order I want it to, so how do we guarantee that we’ll get what we want? In fact, you can rest assured that the CPU does not guarantee that the code will execute exactly in order, but it does guarantee that the program will execute exactly as it did in order.

2 Thread safety issues

2.1 Race conditions and critical regions

Threads share heap space, so be careful to avoid race conditions when programming. The danger is that multiple threads are accessing the same resource and reading and writing at the same time. Before one thread needs to perform an operation based on the state of a variable, the variable is likely to have been modified by another thread.

That is, when two threads compete for the same resource, a race condition is said to exist if the order in which the resources are accessed is sensitive. The code that causes a race condition to occur is called a critical region.

/** * the following code has race conditions, where return ++count is the critical section. * /
public class Obj
{

    private int count;

    public int incr(a)
    {
        return++count; }}Copy the code

2.2 a deadlock

Deadlock: A situation in which two or more processes (or threads) are waiting for each other to execute because they are competing for resources and cannot proceed without external action. The system is said to be in a deadlock state or a deadlock occurs in the system. These processes that are always waiting for each other are called deadlocked processes.

About the conditions under which deadlocks occur:

  1. Mutual exclusion: Thread access to a resource is exclusive. If one thread pair occupies a resource, the other threads must wait until the resource is released.
  2. Request and hold conditions: Thread T1 has held at least one resource, R1, but requests another resource, R2, which is occupied by another thread, T2. Therefore, thread T1 must also wait, but does not release its held resource, R1.
  3. Non-deprivation condition: a thread cannot deprive a resource acquired by another thread before it is used up. It can only release the resource after it is used up.
  4. Loop waiting conditionWhen a deadlock occurs, there must be a “process-resource ring chain”, that is:{p0,p1,p2,... pn}, process P0 (or thread) waits for resources occupied by P1, process P1 waits for resources occupied by P2, and PN waits for resources occupied by P0.(In the most intuitive sense, p0 waits for a resource occupied by P1, and P1 waits for a resource occupied by P0, so the two processes wait for each other.).

2.3 live lock

Live lock: thread 1 can use the resource, but it politely lets other threads use the resource first. Thread 2 can use the resource, but it politely lets other threads use the resource first. So you make me, I make you, the last two threads can’t use the resource.

Deadlocks and live locks

Deadlock: the coming car A and car B crossing the street, A car got half way resources (1: A deadlock occurs condition is exclusive access to resources, I can’t you come up the road, unless you climb on my head), car B accounts for the other half of the car A resource of the road, want to the past must be A request the other half is occupied by A B road (deadlock occurs condition 2: Must whole body space to be opened in the past, I have been accounted for half of nima half way), and is occupied by A B B if you would also have to wait for A way in the past, A is lamborghini, B is A prick silk for chery QQ, A quality of the lower window to B crazy scold: give Lao tze get out of the way, B is very angry, your mama force, Lao tze not to let (deadlock occurs condition 3: The resource cannot be taken away by another thread until it is used up), so that neither thread can go (deadlock condition 4: loop waiting condition), and the subsequent vehicles in the whole lane cannot go either.

Live lock: there is A small bridge in the middle of the road, can only accommodate A car through, the bridge comes to two cars A and B, A is more polite, indicating B first, B is also more polite, indicating A first, the results of two people have been humble let who also can not pass.

2.4 hunger

Hungry: if thread T1 occupies resource R, thread T2 requests to block R, and then T2 waits. T3 also requests resource R, and when T1 releases the blockade on R, the system first approves T3’s request, while T2 still waits. T4 then requests to block R, and when T3 releases the block on R, the system approves T4’s request…… T2 may wait forever.

That is, if a thread can’t get CPU time because it lost all its CPU time to other threads, this state is called “hungry.” The thread was “starved to death” because it was denied CPU time.

On the metaphor of hunger:

In “first” one day in Beijing, the gloomy weather, the air is filled with smog and the smell of cooking oil, A helpless pain temporary traffic police are dealing with traffic jam, there are two road is full of vehicles on A and B, with A wall of the longest time, B is relatively short time, at this time, the road has been cleared, in front of the traffic police, in accordance with the principle of optimal allocation to traffic signal path B, After traffic on road B passes by, the longest queue on lane A fails to pass, so they have to wait for no traffic on lane B to pass, and then wait for the police to send instructions for lane A to pass in turn. This is the unfair lock mechanism provided by ReentrantLock. It is up to the user to decide which locking strategy to use according to the specific usage scenario), unfair locking can improve throughput but inevitably starve some threads.

There are three common causes of thread hunger in Java, as follows:

  1. High-priority threads eat up the CPU time of all low-priority threads

    You can set a separate thread priority for each thread. The higher the priority, the more CPU time the thread gets, the higher the priority value is between 1 and 10, and the exact interpretation of the behavior of these values depends on the platform on which your application is running. For most applications, it’s best not to change the priority value.

  2. A thread is permanently blocked in a state waiting to enter a synchronized block because other threads can always access the synchronized block before it does

    Java’s synchronized code area is also a hunger factor. Java’s synchronous code area has no guarantee about which threads are allowed to enter in order. This means that there is a theoretical risk that a thread trying to enter the synchronization zone will be permanently blocked because other threads will always get access ahead of it, a “starvation” problem, and a thread will “starve to death” just because it doesn’t get a chance to run CPU time.

  3. A thread is waiting on an object that itself (on which wait() is called) is also in perpetual wait, because other threads are constantly being woken up

    If multiple threads are in wait() execution and notify() does not guarantee that any thread will be awakened, any thread may be in a waiting state. Therefore, there is a risk that one waiting thread will never be woken up because other waiting threads can always be woken up.

2.5 fair

The solution to hunger is called “fairness” – that is, all threads are given an equal chance to run. To implement a fairness scheme in Java, you need:

  1. Use locks instead of synchronous blocks;
  2. Use fair locks;
  3. Pay attention to performance;

Implement fairness in Java. Although Java cannot achieve 100% fairness, it is still possible to improve fairness between threads through synchronization structures.

First, learn a simple code for the same gait:

public class Synchronizer{
    public synchronized void doSynchronized (a) {
        // do a lot of work which takes a long time}}Copy the code

If multiple threads call the doSynchronized() method, the other threads will block until the first thread to acquire access completes, and there is no guarantee which thread will acquire access next in this multithreaded blocking scenario.

In order to improve the fairness of waiting threads, we use lock mode to replace synchronous block:

public class Synchronizer{
    Lock lock = new Lock();
    public void doSynchronized(a) throws InterruptedException{
        this.lock.lock();
        //critical section, do a lot of work which takes a long time
        this.lock.unlock(); }}Copy the code

Notice that doSynchronized() is no longer declared synchronized, but is replaced with lock.lock() and lock.unlock(). Here is an implementation using the Lock class:

public class Lock{

    private boolean isLocked      = false;

    private Thread lockingThread = null;

    public synchronized void lock(a) throws InterruptedException{
        while(isLocked){
            wait();
        }

        isLocked = true;
        lockingThread = Thread.currentThread();
    }

    public synchronized void unlock(a){

        if(this.lockingThread ! = Thread.currentThread()){throw new IllegalMonitorStateException("Calling thread has not locked this lock");
        }

        isLocked = false;
        lockingThread = null; notify(); }}Copy the code

Note that in the implementation of Lock above, if there are multiple threads accessing Lock () concurrently, those threads will block access to the Lock () method. In addition, if the lock isLocked, the threads will block in the wait() call of the while(isLocked) loop. Keep in mind that while a thread is waiting to enter lock(), it can call wait() to release the synchronized lock corresponding to its lock instance, allowing multiple threads to enter the lock() method and call wait().

Look at doSynchronized() this time, and you’ll notice the comment between lock() and unlock() : code between these calls will run for a long time. Further, imagine that this code would run for a long time, compared to entering lock() and calling wait(). This means that most of the time spent waiting for the lock to enter and the critical section to enter is spent waiting for wait(), rather than being blocked trying to enter the lock() method.

As mentioned earlier, synchronization blocks do not guarantee access to multiple threads waiting to enter, nor does wait() guarantee that a thread will wake up when notify() is called. There is therefore no difference between this version of the Lock class and the doSynchronized() version in terms of ensuring fairness.

But we can change that, as follows:

The current version of the Lock class calls its own wait() method. If each thread calls wait() on a different object, then only one thread will call wait() on that object. The Lock class determines which object can call notify() on it, thus effectively choosing which thread to wake up.

Let’s convert the above Lock class to FairLock. You’ll notice that the new implementation is slightly different from synchronization and wait()/notify() in the previous Lock classes. The point is that each thread that calls lock() enters a queue, and when unlocked, only the first thread in the queue is allowed to lock the FairLock instance, and all other threads are kept in a waiting state until they are at the head of the queue. As follows:

public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread = null;
    private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();

    public void lock(a) throws InterruptedException{
        // The current thread creates "token"
        QueueObject queueObject = new QueueObject();
        boolean isLockedForThisThread = true;
        synchronized(this) {// queueObject token for all threads
            waitingThreads.add(queueObject);
        }

        while(isLockedForThisThread){
            synchronized(this) {// 1. Check whether it is locked: Whether a thread has acquired the lock and is executing a synchronized code block
                // 2. Check whether the header token is consistent with the current thread token.
                isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
                if(! isLockedForThisThread){ isLocked =true;
                    // Remove the header token
                    waitingThreads.remove(queueObject);
                    lockingThread = Thread.currentThread();
                    return; }}try{
                // Other threads do doWait() to wait
                queueObject.doWait();
            }catch(InterruptedException e){
                synchronized(this) { waitingThreads.remove(queueObject); }
                throwe; }}}public synchronized void unlock(a){
        if(this.lockingThread ! = Thread.currentThread()){throw new IllegalMonitorStateException("Calling thread has not locked this lock");
        }
        isLocked = false;
        lockingThread = null;
        if(waitingThreads.size() > 0) {
            // Wake up the thread corresponding to the header token, which can be executed
            waitingThreads.get(0).doNotify(); }}}public class QueueObject {
    private boolean isNotified = false;

    public synchronized void doWait(a) throws InterruptedException {
        while(! isNotified){this.wait();
        }
        this.isNotified = false;
    }

    public synchronized void doNotify(a) {
        this.isNotified = true;
        this.notify();
    }

    public boolean equals(Object o) {
        return this== o; }}Copy the code

Notice first that the lock() method is no longer declared synchronized, but nested in synchronized for code that must be synchronized.

FairLock creates a new instance of QueueObject and enlists each thread that calls lock(). A thread that calls unlock() gets the QueueObject from the queue header and calls doNotify() on it to wake up the threads waiting on that object. In this way, only one waiting thread is awakened at a time, instead of all waiting threads. This is at the heart of FairLock’s fairness.

Note also that QueueObject is actually a Semaphore. The doWait() and doNotify() methods hold signals in QueueObject. This is done to avoid signal loss when one thread calls QueueObject.dowait () before another thread calls unlock() and then reenters queueObject.donotify (). Queueobject.dowait () calls are placed outside the synchronized(this) block to avoid being nested by monitor, so additional threads can be unlocked as long as no thread is executing in the synchronized(this) block of the lock method.

Finally, notice how QueueObject.dowait () is called in the try — catch block. With InterruptedException thrown, the thread is allowed to leave lock() and be removed from the queue.

3 How to ensure thread-safe features

3.1 How to ensure atomicity

3.1.1 Locking and Synchronization

Common tools to ensure atomicity in Java operations are locking and synchronizing methods (or synchronizing blocks of code). With locks, only one thread can acquire the lock at a time, which ensures that only one thread can execute the code between applying for and releasing the lock at a time.

public void testLock (a) {
    lock.lock();
    try{
        int j = i;
        i = j + 1;
    } finally{ lock.unlock(); }}Copy the code

Similar to locks are synchronized methods or blocks of synchronized code. With non-statically synchronized methods, the current instance is locked; With statically synchronized methods, the Class object of that Class is locked; When a static block of code is used, the objects enclosed in parentheses after the synchronized keyword are locked. Here is an example of a block of synchronized code:

public void testLock (a) {
    synchronized (anyObject){
        int j = i;
        i = j + 1; }}Copy the code

The essence of both lock and synchronized is the same. The exclusivity of resources is realized through lock or synchronization, so that the actual object code segment can only be executed by one thread at a time, thus ensuring the atomicity of the object code segment. This is an approach that comes at the expense of performance.

3.1.2 CAS (Compare and Swap)

Base type variable increment (i++) is an operation that is often mistaken for an atomic operation by novices when it is not. Java provides a corresponding atomic operation class to implement this operation and ensure atomicity, essentially taking advantage of cpu-level CAS instructions. Because they are CPU-level instructions, they are less expensive than locks that require operating system participation. AtomicInteger can be used as follows:

AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
    new Thread(() -> {
        for(int a = 0; a < iteration; a++) {
            atomicInteger.incrementAndGet();
        }
    }).start();
}
Copy the code

3.2 How do I ensure visibility

Java provides the volatile keyword to ensure visibility. When volatile is used to modify a variable, it guarantees that changes to the variable are immediately updated into memory and invalidates the cache of the variable in other threads’ caches, so that any other thread that needs to read the value must read it from main memory to get the latest value.

Volatile is used in scenarios where atomicity is not required, but visibility is required. A typical use scenario is to use it to modify a status flag used to stop a thread. As follows:

boolean isRunning = false;
public void start (a) {
    new Thread( () -> {
        while(isRunning) {
            someOperation();
        }
    }).start();
}
public void stop (a) {
    isRunning = false;
}
Copy the code

In this implementation, even if another thread sets isRunning to false by calling the stop() method, the loop does not necessarily end immediately. The volatile keyword is used to stop the while loop and terminate the thread by ensuring that it gets the latest isRunning status.

3.3 How to ensure order

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

In Java, ordering is guaranteed programmatically by volatile, as well as by synchronized and locking.

Synchronized and lock ensure orderliness in the same way that they ensure atomicity by ensuring that only one thread executes the target code segment at a time.

In addition to ensuring sequential execution of target code segments at the application level, the JVM also implicitly guarantees sequential execution through something called the happens-before principle. As long as the order of two operations can be deduced by happens-before, the JVM guarantees that they are sequential, whereas the JVM makes no guarantee that they are sequential and can reorder them as necessary to be efficient.

The happens-before principle is as follows:

  1. Passing rule: If operation 1 precedes operation 2 and operation 2 precedes operation 3, operation 1 must occur before operation 3. This rule illustrates that the happens-before principle is transitive.
  2. Lock rule: An unlock operation must occur before a subsequent lock operation on the same lock. This makes sense; the lock can only be acquired again if it is released.
  3. Rule for volatile variables: Writes to a volatile variable occur before reads to that variable.
  4. Sequence rule: execute code in sequence within a thread.
  5. Thread start rule: The start() method of the Thread object precedes other actions of the Thread.
  6. Thread termination principle: The termination detection of a thread is followed by all other operations in the thread.
  7. Thread interrupt rule: A call to the threadinterrupt () method first occurs when the interrupt exception is fetched.
  8. Object finalization rule: An object construct occurs before its Finalize.

A few whys about thread safety

  1. If you use locks and synchronized more often than volatile, isn’t visibility guaranteed?

    Locking and synchronized guarantee both atomicity and visibility. This is done by ensuring that only one thread executes the target code snippet at a time.

  2. Why do locks and synchronized guarantee visibility?

    According to the Instructions for the Concurrent package in the Java Doc in JDK 7, the result of a write from one thread is guaranteed to be visible to a read from another thread as long as the write can be inferred by the coin-before principle that the read occurred.

  3. If locking and synchronized guarantee atomicity and visibility, why volatile?

    Synchronized and locks require the operating system to arbitrate who gets the lock, which is expensive, whereas volatile is much less expensive. Thus, using volatile is much better than using locks and synchronized when visibility is all that is required.

  4. If locking and synchronized guarantee atomicity, why do you need a class like AtomicInteger to guarantee atomic operations?

    Lock and synchronized require the operating system to arbitrate who obtains the lock and have high overhead, while AtomicInteger ensures atomicity through CPU-level CAS operation and has low overhead. So again, the purpose of AtomicInteger is to improve performance.

  5. Is there any other way to be thread safe?

    There is. If possible, avoid causing non-thread-safe conditions — shared variables. If the use of shared variables can be avoided by design, non-thread-safe occurrences can be avoided, and atomicity, visibility, and ordering problems can be solved by locking or synchronized and volatile.

  6. Synchronized can modify non-static methods, static methods, and code blocks. What’s the difference?

    Synchronized modifies non-static synchronized methods by locking the current instance; Synchronized modifies static synchronization methods by locking the Class object of that Class. When synchronized modifies a static block of code, it locks the objects enclosed in parentheses after the synchronized keyword.