07 Which Scenarios Require Extra Attention to Thread Safety Issues

07 Which Scenarios Require Extra Attention to Thread Safety Issues #

In this lesson, we mainly learn about the scenarios that require extra attention to thread safety. Here, we summarize four types of scenarios.

Accessing shared variables or resources #

The first scenario is when accessing shared variables or shared resources. Typical scenarios include accessing properties of shared objects, accessing static variables, accessing shared caches, etc. Because these pieces of information are not only accessed by one thread but may also be accessed by multiple threads simultaneously, thread safety issues may occur during concurrent reads and writes. For example, we mentioned in the previous lesson the example of multiple threads simultaneously performing i++:

/**
* Description: Thread safety issues caused by shared variables or resources
*/
public class ThreadNotSafe1 {
    static int i;

    public static void main(String[] args) throws InterruptedException {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 10000; j++) {
                    i++;
                }
            }
        };

        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(i);
    }
}

As shown in the code, two threads perform i++ operations on i simultaneously. The final output may be a number less than 20000, such as 15875, instead of the expected 20000. This is a typical thread safety problem caused by shared variables.

Operations dependent on timing #

The second scenario that requires attention is operations dependent on timing. If the correctness of our operations depends on timing, and in the case of multithreading, the order of execution cannot be guaranteed to be consistent with our expectations, thread safety issues may occur. For example, consider the following code:

if (map.containsKey(key)) {
    map.remove(obj)
}

In the code, we first check if there is an element corresponding to the key in the map. If there is, we continue to execute the remove operation. At this point, this combination of operations is dangerous because it checks first and then operates, and the execution process may be interrupted. If two threads enter the if() statement at the same time and both check that there is an element corresponding to the key, they both want to execute the subsequent remove operation. Then, one thread deletes obj first, and the other thread, who has just checked that an element corresponding to the key exists and the if condition is satisfied, continues to execute the remove operation. However, in reality, obj in the collection has already been deleted by the previous thread. This can lead to thread safety issues.

There are many similar situations, such as checking x = 1 first, and if x = 1, modifying the value of x, as shown in the following code:

if (x == 1) {
    x = 7 * x;
}

Similar scenarios follow the same principle. “Check and execute” is not an atomic operation and can be interrupted in the middle. The result of the check may also be expired or invalid during execution. In other words, obtaining the correct result depends on lucky timing. In this case, we need to protect the operation’s atomicity by adding locks or other protection measures.

Binding relationship between different data #

The third scenario that requires attention is when there is a binding relationship between different data. Sometimes, different data appear as groups, with corresponding or bound relationships, such as IP and port numbers. Sometimes, when we change the IP, we often need to change the port number at the same time. If we do not bind these two operations together, it is possible to change only the IP or the port number independently. If the information has already been published externally at this point, the information recipient may obtain an incorrect binding of IP and port. This will result in thread safety issues. In this case, we also need to ensure the atomicity of the operations.

The other party has not declared itself thread-safe #

The fourth scenario that deserves attention is the case where we use other classes that have not declared themselves thread-safe. If the other class does not declare itself as thread-safe, when performing concurrent operations on other classes in a multi-threaded environment, thread safety issues may occur. For example, let’s say we define an ArrayList, which is not thread-safe by itself. If multiple threads perform concurrent read/write operations on the ArrayList, thread safety issues may occur, causing data errors. However, ArrayList is not responsible for this, because it is not inherently thread-safe, as stated in the source code comments:

Note that this implementation is not synchronized. If multiple threads
access an ArrayList instance concurrently, and at least one of the threads
modifies the list structurally, it must be synchronized externally.

This means that if we use ArrayList in a multi-threaded scenario, we need to manually use synchronization or other methods externally to ensure concurrency safety.

Therefore, ArrayList is not suitable for concurrent reading and writing by default, and we mistakenly used it, resulting in thread safety issues. So, when using other classes, if there may be concurrent scenarios, we must first confirm whether the other party supports concurrent operations. These are the four scenarios that require extra attention to thread safety: accessing shared variables or resources, operations dependent on timing, binding relationship between different data, and the other party not declaring itself as thread-safe.