Part of this article is excerpted from The Art of Concurrent Programming in Java

The thread profile

1. What are threads?

When running a program, modern operating systems create a process for it. Multiple threads can be created in a process. The smallest unit of scheduling in modern operating systems is threads, also known as lightweight processes. Each of these threads has its own counters, stacks, local variables and other attributes, and has access to shared memory variables. The processor switches between these threads at high speed, giving the user the impression that they are executing simultaneously

2. Why multithreading?

There are several reasons for using multithreading:

  • More processor cores

    By using multithreading technology, the processing time of a program can be significantly reduced by distributing the computational logic across multiple processor cores

  • Faster response times

    Sometimes we write more complex code (mainly business logic) and can use multi-threading techniques to delegate operations with inconsistent data to other threads (message queues can also be used). The advantage of this is that the thread responding to the user’s request can process it as quickly as possible, shortening the response time

  • A better programming model

    Java already provides a good programming model for multithreaded programming, and developers only need to build the appropriate model according to the needs of the problem

Thread priority

Modern operating systems basically schedule running threads in the form of time division. The operating system will divide time slices into time slices, and the threads are allocated to several time slices. When the time slices are used up, thread scheduling takes place and waits for the next allocation. The number of time slices allocated by a thread determines how much processor resources it uses, and thread priority is a thread attribute that determines how much or how little processor resources a thread needs to allocate

In Java threads, the priority is controlled by an integer member variable priority, which ranges from 1 to 10. The setPriority(int) method can be used to change the priority during thread construction. The default priority is 5. Threads with higher priority allocate more time slices than threads with lower priority. However, thread planning varies between JVMS and operating systems, and some operating systems even ignore thread priority Settings

public class Priority {

    private static volatile boolean notStart = true;
    private static volatile boolean notEnd = true;

    public static void main(String[] args) throws Exception {
        List<Job> jobs = new ArrayList<Job>();
        for (int i = 0; i < 10; i++) {
            int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
            Job job = new Job(priority);
            jobs.add(job);
            Thread thread = new Thread(job, "Thread:" + i);
            thread.setPriority(priority);
            thread.start();
        }
        notStart = false;
        TimeUnit.SECONDS.sleep(10);
        notEnd = false;
        for (Job job : jobs) {
            System.out.println("Job Priority : " + job.priority + ", Count : "+ job.jobCount); }}static class Job implements Runnable {
        private int priority;
        private long jobCount;

        public Job(int priority) {
            this.priority = priority;
        }

        @Override
        public void run(a) {
            while (notStart) {
                Thread.yield();
            }
            while(notEnd) { Thread.yield(); jobCount++; }}}}Copy the code

When you run the example, the corresponding output on your author’s machine is as follows

In Win10 + JDK11, we can see that thread priority is in effect

Thread state

A Java thread can be in six different states during its lifetime, as shown in the table below, and a thread can only be in one of these states at any given moment

The name of the state instructions
NEW In the initial state, the thread is built, but the start() method has not yet been called
RUNNABLE Running state, which Java threads loosely refer to as “running” in the operating system
BLOCKED Blocked state, indicating that the thread is blocked on the lock
WAITING Wait state, which indicates that the thread enters the wait state, which indicates that the current thread needs to wait for other threads to do some specific action (notification or interrupt)
TIME_WAITING A timeout wait state that, unlike WAITING, can return at a specified time
TERMINATED Terminated status: indicates that the current thread has finished executing

Threads are not fixed in one state during their life cycle, but switch between different states as the code executes

Daemon thread

Daemon threads are support threads that are used for background scheduling and support work in applications. This means that when a Java virtual machine has no Daemon threads, the Java virtual machine will exit. Threads can be set to Daemon threads by calling thread.setdaemon (true)

Two things to note when using Daemon threads:

  • Daemon properties need to be set before starting the thread, not after
  • When building Daemon threads, you cannot rely on the contents of the finally block to ensure that the logic for cleaning up resources is executed or turned off. Because the finally block in the Daemon thread does not necessarily execute when the Java VIRTUAL machine exits

Start and terminate threads

1. Construct a thread

Before running a thread, a thread object must be constructed. When a thread object is constructed, it needs to provide information about its properties, such as the thread group it belongs to, whether it is a Daemon thread, and so on

2. Start the thread

After the thread object is initialized, the start() method is called to start the thread

3. Understand interruptions

Interrupts can be understood as an identifier bit property of a thread that identifies whether a running thread has been interrupted by another thread. Interrupts are like being greeted by other threads, which can interrupt the thread by calling its interrupt() method

A thread responds by checking whether or not it has been interrupted, using isInterrupted() to determine whether or not it has been interrupted, or by calling the static method Tread.interrupted() to reset the interrupt identifier of the current thread. If the thread is terminated and has been interrupted, isInterrupted() returns false

Many methods that declare to throw InterruptedException require the Java virtual machine to clear the thread’s interrupt identifier before throwing an exception. Calling isInterrupted() returns false

In the following example, create two threads: SleepThread and BusyThread. The former is always sleeping and the latter is always running

public class Interrupted {

    public static void main(String[] args) throws InterruptedException {
        // Sleepthreads are constantly trying to sleep
        Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
        sleepThread.setDaemon(true);
        // busyThread runs continuously
        Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();
        // Sleep for 5 seconds to allow sleepThreads and busyThreads to run fully
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
        // Prevent sleepThread and busyThreaad from exiting immediately
        SleepUtils.second(2);
    }

    static class SleepRunner implements Runnable {

        @Override
        public void run(a) {
            while (true) {
                SleepUtils.second(10); }}}static class BusyRunner implements Runnable {

        @Override
        public void run(a) {
            while (true) {}}}}Copy the code

The output is as follows

As you can see, the thread SleepThread that threw InterruptedException has its interrupt flag bit cleared, while the thread BusyThread that has been busy has its interrupt flag bit not

4. Safely terminate the thread

The interrupt operation mentioned above is a convenient way to interact with threads and is suitable for canceling or stopping tasks. In addition to interrupting, you can use a Boolean variable to control whether you need to stop the task and terminate the thread

In the following example, we create a thread, CountThread, that keeps adding variables while the main thread tries to interrupt and stop it

public class Shutdown {

    public static void main(String[] args) throws InterruptedException {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        // The main thread interrupts the CountThread so that the CountThread can sense the interruption and terminate
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        // After sleeping for one second, the main thread interrupts Runner Two so that CountThread ends up sensing that on is false
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {

        private long i;
        private volatile boolean on = true;

        @Override
        public void run(a) {
            while(on && ! Thread.currentThread().isInterrupted()) { i++; } System.out.println("Count i = " + i);
        }

        public void cancel(a) {
            on = false; }}}Copy the code

The main thread terminates CountThread with an interrupt and cancel(). This approach, by identifying bits or interrupting operations, gives the thread a chance to clean up resources upon termination rather than arbitrarily stopping the thread, which is safer and more elegant