02 How to Properly Stop a Thread and Why the Volatile Flag Stop Method Is Wrong

02 How to Properly Stop a Thread and Why the Volatile Flag Stop Method Is Wrong #

In this lesson, we will mainly learn how to stop a thread correctly and why the stop method using the volatile flag is incorrect.

First, let’s review how to start a thread. To start a thread, we need to call the start() method of the Thread class and define the tasks to be executed in the run() method. Starting a thread is very simple, but stopping it correctly is not so easy.

Introduction to principles #

Usually, we don’t manually stop a thread, but allow the thread to run until it ends and then let it stop naturally. However, there are still many special situations where we need to stop a thread in advance, such as when the user suddenly closes the program or when the program is restarted due to an error.

In this case, the thread that is about to be stopped is still valuable in many business scenarios. Especially when we want to write a program with good robustness that can handle various scenarios safely, it is especially important to stop threads correctly. However, Java does not provide a simple and easy-to-use ability to stop threads safely.

Why not force stop? But notify and collaborate #

For Java, the most correct way to stop a thread is to use interrupt. However, interrupt only serves the purpose of notifying the thread to be stopped. For the stopped thread, it has complete autonomy and can choose to stop immediately, stop after a period of time, or not stop at all. So why doesn’t Java provide the ability to force stop threads?

In fact, Java hopes that programs can notify and collaborate with each other to manage threads, because if you don’t know what the other party is doing, forcibly stopping a thread may cause some security issues. In order to avoid problems, the other party needs some time to tidy up the finishing work. For example, if a thread is writing to a file and receives a termination signal, it needs to judge based on its own business whether to choose to stop immediately or stop after the entire file is successfully written. If it chooses to stop immediately, it may cause incomplete data. Neither the initiator nor the receiver of the interruption command wants data problems to occur.

How to stop a thread using interrupt #

while(!Thread.currentThread().isInterrupted() && more work to do) {
    do more work
}

After understanding the design principles of stopping threads in Java, let’s see how to implement the logic of stopping threads with code. Once we call the interrupt() of a thread, the interrupt flag of this thread will be set to true. Each thread has such a flag, and when the thread is running, it should periodically check this flag. If the flag is set to true, it means that there is a program that wants to terminate this thread. Going back to the source code, we can see that in the while loop condition, the thread is first checked for interruption by Thread.currentThread().isInterrupted(), and then it is checked whether there is more work to do. The && logical operator indicates that the next work will only be executed when both conditions are satisfied.

Let’s take a look at a specific example.

public class StopThread implements Runnable {
    @Override
    public void run() {
        int count = 0;
        while(!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
}

In the run() method of the StopThread class, the thread is first checked for interruption, and then it is checked whether the count value is less than 1000. The work content of this thread is very simple, just print numbers from 0 to 999, and the count value is incremented by 1 for each printed number. As you can see, the thread will check if it has been interrupted before each iteration starts. Next, in the main function, the thread is started, then sleeps for 5 milliseconds, and then immediately interrupts the thread. The thread will detect the interruption signal, so it will stop before printing all 1000 numbers. This is an example of correctly stopping a thread using interrupt.

Can interruption be felt during sleep #

Runnable runnable = () -> {
    int num = 0;
    try {
        while(!Thread.currentThread().isInterrupted() && num <= 1000) {
            System.out.println(num);
            num++;
            Thread.sleep(1000000);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Now let’s consider a special case and rewrite the above code. If the thread has a sleep requirement while performing the task, that is, it enters sleep once for every printed number, and at this time, the sleep time of Thread.sleep() is set to 1000 seconds.

public class StopDuringSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
int num = 0;

try {

    while (!Thread.currentThread().isInterrupted() && num <= 1000) {

        System.out.println(num);

        num++;

        Thread.sleep(1000000);

    }

} catch (InterruptedException e) {

    e.printStackTrace();

}

};

Thread thread = new Thread(runnable);

thread.start();

Thread.sleep(5);

thread.interrupt();

}

After the main thread sleeps for 5 milliseconds, it signals the child thread to interrupt it. At this time, the child thread is still executing the sleep statement and is in a sleeping state. So we need to consider whether the sleeping thread can feel the interrupt signal. Do we need to wait until the sleep ends before interrupting the thread? If so, it will cause serious problems because the response to the interrupt is too slow. Because of this, the designers of Java considered this point from the beginning of the design.

If the blocking thread is sleeping due to methods like sleep or wait, and the sleeping thread is interrupted, the thread can feel the interrupt signal and throw an InterruptedException exception, while clearing the interrupt signal and setting the interrupt flag to false. In this way, we don’t have to worry about the sleeping thread not feeling the interrupt for a long time, because even if the thread is still sleeping, it can still respond to the interrupt notification and throw an exception.

Two best handling methods #

In actual development, it is definitely a collaboration within the team. Different people are responsible for writing different methods and then calling each other to implement the entire business logic. So if the method we are responsible for writing needs to be called by others, and our method calls methods that can respond to interrupts such as sleep or wait, it is not enough to just catch the exception.

void subTask() {

try {

    Thread.sleep(1000);

} catch (InterruptedException e) {

    // It is not good to not handle this exception here

}

}

We can use try/catch in the method or declare throws InterruptedException in the method signature.

Method signature throws an exception, run() forcibly try/catch #

Let’s first look at the logic of try/catch. As shown in the above code, the catch block is empty, and it does not perform any processing. Assuming that the thread is executing this method and is sleeping, at this time, another thread sends an interrupt notification in an attempt to interrupt the thread, it will immediately throw an exception and clear the interrupt signal. The thrown exception is caught by the catch block.

However, the caught exception catch block does not perform any processing logic, which is equivalent to hiding the interrupt signal. This is very unreasonable. How should it be handled? First, you can choose to throw an exception in the method signature.

void subTask2() throws InterruptedException {

Thread.sleep(1000);

}

As the code shows, every method call is required to handle the exception, either by using try/catch and correctly handling the exception in the catch, or by declaring the exception in the method signature. If every layer of logic follows the rules, the interrupt signal can be passed layer by layer to the top level, and finally the run() method can catch the exception. For the run() method, it cannot throw a checkedException, it can only handle the exception through try/catch. The logic of passing exceptions layer by layer guarantees that no exceptions will be missed, and for the run() method, it can handle the exception according to different business logic.

Interrupt again #

private void reInterrupt() {

try {

    Thread.sleep(2000);

} catch (InterruptedException e) {

    Thread.currentThread().interrupt();

    e.printStackTrace();

}

}

In addition to the method recommended just now to declare the exception in the method signature, you can also interrupt the thread again in the catch statement. As the code shows, you need to call Thread.currentThread().interrupt() in the catch block. Because if the thread is interrupted during sleep, the interrupt signal will be automatically cleared. If the interrupt signal is manually added at this time, the interrupt signal can still be caught. In this way, the subsequently executed methods can still detect that an interrupt occurred here and can make corresponding processing, so that the thread can exit normally.

We need to pay attention that in actual development, we should not blindly swallow interrupts. If we do not declare in the method signature, do not resume the interrupt again in the catch block, and do not process it in the catch block, we call this behavior “shielding the interrupt request”. If we blindfolded the interrupt request, the interrupt signal would be completely ignored, eventually causing the thread to fail to stop properly.

Why is using the volatile flag to stop the method wrong #

Let’s take a look at the second question of this lesson. Why is it incorrect to use the volatile flag for stopping a thread?

Incorrect Stopping Methods #

First, let’s look at a few incorrect methods for stopping threads. For example, stop(), suspend(), and resume() methods have been marked as @Deprecated by Java. If these methods are called, the IDE will kindly remind us not to use them anymore. But why can’t we use them anymore? This is because stop() method directly stops the thread, which doesn’t give enough time for the thread to handle the logic of saving data before stopping. The task abruptly stops, resulting in issues with data integrity, among others.

As for suspend() and resume(), their problem lies in the fact that if a thread calls suspend(), it doesn’t release the lock and goes into sleep. However, it may still hold the lock at this point, which can easily lead to deadlock problems because the lock won’t be released until the thread is resumed().

Let’s assume thread A calls suspend() to suspend thread B. Thread B goes into sleep while holding a lock. Now, suppose thread A wants to access the lock held by thread B. But since thread B has gone into sleep without releasing the lock, thread A cannot acquire the lock and gets blocked. As a result, both thread A and thread B cannot proceed further.

Because of such risks, the combination of suspend() and resume() methods has also been deprecated. Now, let’s see why using the volatile flag as a stop flag is also incorrect.

Appropriate Scenarios for Using the volatile Modifier #

public class VolatileCanStop implements Runnable {

    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (!canceled && num <= 1000000) {
                if (num % 10 == 0) {
                    System.out.println(num + " is a multiple of 10.");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileCanStop r = new VolatileCanStop();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(3000);
        r.canceled = true;
    }
}

In what scenarios can we use the volatile modifier to stop a thread properly? As shown in the code above, we define a class called VolatileCanStop that implements the Runnable interface. In the run() method, we have a while loop with two conditions. First, it checks the value of the canceled variable, which is a boolean variable initialized as false and is volatile. When this value becomes true, the while loop exits. The second condition of the while loop is num being less than or equal to 1000000. Inside the while loop, if a number is a multiple of 10, it is printed, and then num is incremented by 1.

Next, we start the thread, and after 3 seconds, we set the value of the boolean flag, canceled, which is declared with the volatile modifier, to true. As a result, the running thread will check that the value of canceled has become true in the next iteration of the while loop. Thus, it no longer satisfies the condition for the while loop and exits, effectively stopping the thread. This demonstrates a situation where the volatile modifier for the stop flag works correctly. However, if we say that a method is correct, it should not only be applicable in one situation, but also in other situations.

Inappropriate Scenarios for Using the volatile Modifier #

Next, let’s use an example of a producer/consumer pattern to demonstrate why the volatile stop flag method is not perfect.

class Producer implements Runnable {
    public volatile boolean canceled = false;
    BlockingQueue storage;
    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }
    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 50 == 0) {
                    storage.put(num);
                    System.out.println(num + " is a multiple of 50 and has been put into the storage.");
                }
                // rest of the code
            }
```java
num++;

}
} catch (InterruptedException e) {

e.printStackTrace();

} finally {

System.out.println("Producer stopped");

}

}

First, a Producer is declared with a boolean variable canceled, which is initialized as false and marked as volatile to stop the thread. In the run() method, the while loop checks if num is less than 100000 and if canceled is marked. Inside the while loop, if num is a multiple of 50, it is put into the storage, which is the communication storage between the producer and the consumer. If num is greater than 100000 or if it is notified to stop, the while loop will be exited and the finally block will be executed, indicating “Producer stopped”.

class Consumer {

BlockingQueue storage;

public Consumer(BlockingQueue storage) {

this.storage = storage;

}

public boolean needMoreNums() {

if (Math.random() > 0.97) {

return false;

}

return true;

}

}

As for the Consumer, it shares the same storage with the producer. Inside the needMoreNums() method, it determines whether to continue using more numbers by generating a random number and comparing it with 0.97. If it is greater than 0.97, it will not continue using numbers.

public static void main(String[] args) throws InterruptedException {

ArrayBlockingQueue storage = new ArrayBlockingQueue(8);

Producer producer = new Producer(storage);

Thread producerThread = new Thread(producer);

producerThread.start();

Thread.sleep(500);

Consumer consumer = new Consumer(storage);

while (consumer.needMoreNums()) {

System.out.println(consumer.storage.take() + " consumed");

Thread.sleep(100);

}

System.out.println("Consumer doesn't need more data.");

// Once the consumer doesn't need more data, we should stop the producer as well, but in reality it doesn't stop

producer.canceled = true;

System.out.println(producer.canceled);

}

}

Now let’s see the main function. First, a storage BlockingQueue with a capacity of 8 is created. Then, a producer is created and put into a thread which is then started. After a 500ms sleep, a consumer is created. The while loop checks if the consumer needs more numbers. Each time a number is consumed, it sleeps for 100ms. This kind of logic could be encountered in real production scenarios.

When the consumer no longer needs data, the canceled flag is set to true. In theory, the producer should exit the while loop and print “Producer stopped”. However, the result is not what we expect. Even though canceled is set to true, the producer doesn’t stop because it gets blocked when executing storage.put(num). It can’t enter the next iteration of the while loop to check the value of canceled until it is woken up. Therefore, using volatile in this case cannot stop the producer. On the contrary, if we use the interrupt statement to interrupt the thread, it can sense the interrupt signal and respond accordingly.

Conclusion #

In summary, we have learned how to stop a thread correctly and why using volatile to stop a thread is incorrect.

If we are asked in an interview, “Do you know how to stop a thread correctly?”, I believe you can answer it perfectly. First of all, it is recommended to use interrupt to request an interruption instead of forcefully stopping the thread. This can avoid data corruption and give the thread time to finish its cleanup work.

If we are the authors of the sub-method and encounter an InterruptedException, how should we handle it?

We can declare the exception in the method so that the top-level method can perceive and catch the exception, or we can re-declare the interruption in the catch block, so that the next iteration can also sense the interruption. So, to stop a thread correctly, it requires cooperation between the stopper, the stopped thread, and the authors of the sub-method. Everyone should write code according to certain standards to stop the thread correctly.

Finally, let’s review the methods that are not recommended, such as stop(), suspend(), and resume(), which have been deprecated due to major security risks such as deadlock. And the volatile method is not comprehensive enough to stop a thread in certain special cases, such as when a thread is blocked for a long time, it cannot sense the interruption in a timely manner. Therefore, volatile is not a comprehensive method for stopping a thread.