The cornerstone of concurrent programming understood by Tencent Architects -- the working principle of Thread class

1. Opening remarks

When it comes to concurrent programming, the first impression in your mind may be Thread, multithreading, JUC, Thread pool, ThreadLocal and so on. Indeed, concurrent programming is an indispensable part of Java programming. Mastering the core technology of concurrent programming will be a sharp weapon in job interviews. Today I'm going to talk to you about the working principle of Thread class, the cornerstone of concurrent programming.

In fact, when I recall the core API of Thread class and the corresponding Thread state transition relationship, I always feel that my impression is somewhat vague, so I have this article. The core topic of this paper is Thread class, which extends many topics, such as process and Thread, Thread state and life cycle, the usage of Thread API and so on.

2. Process and thread

First of all, it is necessary to introduce processes and threads, as well as the differences and relationships between them.

Process is the basic unit for the operating system to allocate resources. For example, when we start a main, we start a JVM process.

Thread is a unit smaller than the process dimension. It is the basic unit of CPU allocation (because the running thread is really occupied). For example, after starting a main method, its thread belongs to a thread of the JVM process, which is called the main thread. A process can have one or more threads. Each thread in the same process shares the memory space of the process.

The differences between processes and threads are as follows:

  • Process is the smallest unit of resources allocated by the operating system, and thread is the smallest unit of CPU allocation (program execution)
  • A process consists of one or more threads. Threads are different execution routes of code in a process
  • Processes are independent of each other, but each thread in the same process shares the memory space of the program
  • Scheduling and switching: thread context switching is much faster than process context switching

3. Thread common API s

Since the author knew little about the core API of Thread class and its principle, the main content of this section is to introduce the usage, significance and impact on Thread state of the core API of Thread class.

3.1 create thread task

Before calling the API of Thread class, you need to create a Thread object, which is very simple. You only need new Thread(). But in fact, if you only create a Thread through new without doing anything else, the Thread will not execute any business logic.

Therefore, we need to specify the business logic to be executed by threads through other means. Then the question comes: how many forms do we create thread tasks?

Generally speaking, we think there are three forms: Inheriting Thread class, implementing Runnable interface and implementing Callable interface, which are described in detail below.

3.1.1 inherit Thread class

Create a class that inherits the Thread class and overrides the run method, which specifies the business logic executed by the Thread. In this way, you can directly instantiate this class when creating a Thread in the future. After starting the Thread, the program will automatically execute the overridden run method logic.

public class CreateThreadByThread extends Thread {

    @Override
    public void run() {
        System.out.println("CreateThreadByRunnable#run, custom business logic ");
    }

    public static void main(String[] args) {
        Thread thread = new CreateThreadByThread();
        thread.start();
    }
}

// output
CreateThreadByRunnable#run, custom business logic

3.1.2 implementation of Runnable interface

Create a class, implement the Runnable interface and override the run method, then create a Thread class, and pass the object implementing the Runnable interface as an entry into the constructor of the Thread class. After starting the Thread object, the program will execute the run method of the Runnable object.

public class CreateThreadByRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("CreateThreadByRunnable#run, custom business logic ");
    }

    public static void main(String[] args) {
        Runnable runnable = new CreateThreadByRunnable();
        // Pass the runnable object as an input parameter into the Thread class constructor
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Although one of the above two forms inherits the Thread class and the other implements the Runnable interface, there is no essential difference if you look at the source code.

First, let's look at the similar form of inheriting Thread, which needs to override the run method. Let's see what the default content of Thread#run is:

// Thread#run
@Override
public void run() {
  if (target != null) {
    target.run();
  }
}

// Thread#target
/* What will be run. */
private Runnable target;

In fact, the Thread#run method is a run method that overrides the Runnable interface. Its logic is to execute its run method when the private member variable of Runnable type is not empty.

In the form of implementing the Runnable interface, after creating the Runnable type object, we need to pass it as an input parameter into the constructor of the Thread class.

// Thread#Thread(java.lang.Runnable)
public Thread(Runnable target) {
  init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
  // Omit other codes
  
  this.target = target;

  // Omit other codes
}

It can be seen that in the overload construction method similar to Thread, the passed in Runnable object is assigned to the target private member variable of Thread class. Then contact the Thread#run method we just mentioned: when the private member variable of Runnable type is not empty, execute its run method.

Isn't this just a change of skin?

Therefore, there is no essential difference between the form of implementing the Runnable interface and the form of inheriting the Thread class. They are based on overriding the run method to change the tasks that the Thread needs to perform.

3.1.3 implementation of Callable interface

The Callable interface is jdk1 5, which is more powerful than Runnable. The biggest feature is that Callable allows return values. Secondly, it supports generics. At the same time, it allows throwing exceptions to be caught by outer code. The following is an example of implementing Callable interface to create threads:

public class CreateThreadByCallable implements Callable<Integer> {

    public static void main(String[] args) {
        CreateThreadByCallable callable = new CreateThreadByCallable();
        FutureTask<Integer> future = new FutureTask<>(callable);
        // Create thread and start
        Thread thread = new Thread(future);
        thread.start();

        Integer integer = null;
        try {
            integer = future.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("FutureTask Return content: " + integer);

    }

    @Override
    public Integer call() throws Exception {
        System.out.println("CreateThreadByCallable#call, custom business logic, return 1 ");
        return 1;
    }
}

It is worth noting that we need to use the Callale interface based on the FutureTask class. The return value, exception and genericity are all features provided by the FutureTask class.

When going deep into the constructor of FutureTask and its internal methods, the author found some new things.

// FutureTask#FutureTask(java.util.concurrent.Callable<V>)
public FutureTask(Callable<V> callable) {
  if (callable == null)
    throw new NullPointerException();
  this.callable = callable;
  this.state = NEW;
}

First, in the constructor of FutureTask, the Callable object is assigned to the Callable type private member variable of FutureTask class. Then continue to construct the Thread object. The author found that the Thread overload construction method we used is consistent with the scenario of implementing the Runnable interface, that is, FutureTask implements the Runnable interface. Open the source code and see if it is.

// The FutureTask class implements the RunnableFuture interface
public class FutureTask<V> implements RunnableFuture<V> {}

// The RunnableFuture interface inherits from the RunnableFuture interface
public interface RunnableFuture<V> extends Runnable, Future<V> {}

Therefore, the FutureTask object we constructed is passed into the Thread class as a Runnable type. When the Thread starts, it will execute the run method inside FutureTask. Let's take a look at the FutureTask#run method.

// java.util.concurrent.FutureTask#run
public void run() {
  if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
    return;
  try {
    Callable<V> c = callable;
    if (c != null && state == NEW) {
      V result;
      boolean ran;
      try {
        // Execute the call method of the callable property to get the return value
        result = c.call();
        ran = true;
      } catch (Throwable ex) {
        result = null;
        ran = false;
        setException(ex);
      }
      if (ran)
        // If the execution is completed, assign the return value to the outcome attribute (returned in the FutureTask#get method)
        set(result);
    }
  } finally {
    runner = null;
    int s = state;
    if (s >= INTERRUPTING)
      handlePossibleCancellationInterrupt(s);
  }
}

// java.util.concurrent.FutureTask#set
protected void set(V v) {
  if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
    outcome = v;
    UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
    finishCompletion();
  }
}

You can see that the Callable#call method is actually executed in the FutureTask#run method. Therefore, in the form of implementing the Callable interface, the final execution content of Thread is still the Thread#run method, but this run method is overwritten by the run method of FutureTask class, and calling the Callable#call method is the internal predefined logic of FutureTask#run.

3.1.4 method of creating thread

Through the above three forms of creating thread tasks and the exploration of their source code, we can know that no matter which form is finally implemented in the form of covering Thread#run.

Here's an aside: how many ways to create threads?

This problem is often misinterpreted on the network. Most views believe that threads can be created by inheriting the Thread class, implementing the Runnable interface and implementing the Callable interface. In fact, what I described above is "the form of creating Thread tasks". What I want to highlight is not the method of creating threads, but the method of creating threads to execute tasks.

In the above example code, we can know that even through the latter two forms (i.e. implementing Runnable interface and Callable interface), we finally need new Thread to create a thread, but we change the behavior of the thread by passing in Runnable object.

Therefore, there is only one way to create a thread: new Thread().

3.2 start

Thread#start can be said to be the most commonly used method of thread class. This method is used to make the thread start execution.

After calling the start method of the newly created thread, the thread will change from NEW state to RUNNABLE state, and the CPU will allocate the time slice to the thread at an appropriate time to truly execute the thread's business method.

It should be noted that a thread can only call the start method once, otherwise an IllegalThreadStateException will be thrown. The reason is that in the Thread#start method, the thread state will be judged first.

// java.lang.Thread#start
public synchronized void start() {
	// If the thread state is not NEW, an exception is thrown
  // A zero status value corresponds to state "NEW". (the status corresponding to 0 value is NEW)
  if (threadStatus != 0)
    throw new IllegalThreadStateException();

  // Omit other logic
}

You can see that when the state of a thread is not NEW, calling its start method again will throw an IllegalThreadStateException. In other words, once the thread completes execution, it cannot be restarted.

3.3 join

The Thread#join method is used to wait for the thread to complete execution. In jdk1 This method has three overloaded methods in 8, and the other two overloaded methods with parameters set the timeout time:

The meaning of Thread#join method can be described as follows: call the join method of thread B in thread A, and thread A will wait for thread B to execute. In this process, the state of thread A will change from RUNNABLE to WAITING. After thread B is executed, the state of thread A will change to RUNNABLE. This conclusion can be verified by the following example:

public static void main(String[] args) throws InterruptedException {
  Thread thread1 = new Thread(() -> {
    System.out.println("thread1 is running");
    try {
      // In order to observe the effect, set the sleep time longer
      Thread.sleep(50000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("thread1 is over");
  });

  thread1.start();
  thread1.join();
}

Run the program, first output thread1 is running, and then thread1 thread enters the sleep method. After sleep, output thread1 is over. In the process of thread1 thread sleep, open the jconsole tool, and you can observe that thread1.0 is called The main thread state of the join method is WAITING.  

The Thread#join method mentioned above. If it is an overloaded method with timeout set, the thread state of calling a thread object join method will change to TIMED_WAITING.

In addition, there is another question worth thinking about: after the current thread calls the join method of other threads, if other threads try to obtain the lock held by the current thread, will it succeed? Let's do an experiment.

private static String str = "123";

private static void testJoinLock() throws InterruptedException {
  // The main thread occupies str resources first
  synchronized (str) {
    Thread thread1 = new Thread(() -> {
      System.out.println("thread1 is running");
      // The child thread attempts to occupy str resources
      synchronized (str) {
        System.out.println("thread1 is get str lock");
      }
      System.out.println("thread1 is over");
    });

    thread1.start();
    thread1.join();
  }
}

First declare a shared resource STR variable. The main thread first adds a synchronization lock to the variable, and then instantiates a child thread. The child thread also tries to add a synchronization lock to str. Run the program and observe that the final output is thread1 is running, and the program has not been terminated.

When guessing that the child thread may be blocked, open jconsole, as shown in the following figure:

Sure enough, it is observed that the state of Thread-0 thread is BLOCKED and the resource owner is main, that is, the thread is BLOCKED by the main thread.

Therefore, when thread A enters the WAITING state by calling the join method of thread B, it will not release the lock resources it already holds.

3.4 yield

The Thread#yield method is used to release the time slice and let the CPU select the thread for execution again. The underlying meaning of this sentence is that the CPU may select the thread that abandoned the time slice before to execute.

It is worth noting that the Thread#yield method does not release the lock resources already held.

3.5 interrupt

The Thread#interrupt method is used to interrupt the thread. The method calling the thread will request to terminate the current thread. It should be noted that this method only sends a termination information to the current thread and sets the interrupt flag bit. Whether to terminate or not is handled by the thread itself.

There are two similar methods: thread #interrupted and thread #isinterrupted.

The Thread#interrupt method is used to check whether the thread is interrupted and clear the interrupt flag bit.

The Thread#isInterrupted method checks whether the thread is interrupted, but does not clear the interrupt flag bit.

It is worth noting that when the Thread#interrupt method of a thread is called, if the current thread is in timed_ When in the WAITING or WAITING state (such as calling Object#wait, Thread#join, Thread#sleep or the thread corresponding to the overloaded method), an InterruptedException exception will be thrown, causing the thread to enter the TERMINATED state directly.

public static void main(String[] args) {
  Thread thread1 = new Thread(() -> {
    System.out.println("thread1 is running");
    try {
      // In order to observe the effect, set the sleep time longer
      Thread.sleep(50000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("thread1 is over");
  });

  thread1.start();
  // Interrupt thread
  thread1.interrupt();
}

Execute this method, and the output content is:

thread1 is running
thread1 is over
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at io.walkers.planes.pandora.jdk.thread.usage.InterruptMethod.lambda$main$0(InterruptMethod.java:16)
	at java.lang.Thread.run(Thread.java:748)

As you can see, the program threw an InterruptedException exception.

4. Thread status

In Java language, Thread is abstracted into Thread class, and there is a State enumeration class in Thread class, which describes various states of Thread.

// java.lang.Thread.State
public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

The comments on the JDK source code describe the thread state as follows:

  • NEW: the thread that is not started is in this state
  • RUNNABLE: the thread executing in the JVM is in this state
  • BLOCKED: a thread that is BLOCKED while waiting for a lock is in this state
  • WAITING: a thread is WAITING indefinitely for another thread to perform a specific operation, and it is in this state
  • TIMED_WAITING: This is the state of a thread waiting for another thread to perform an operation within the specified waiting time
  • TERMINATED: the exited thread is in this state

Next, I will use code to simulate threads in various states, and give examples of how threads enter this state.

4.1 NEW

The unstarted Thread is in the new state, which is easy to simulate. When I create a new Thread object, the Thread is in the new state. The simulation code is as follows:

public static void main(String[] args) {
  Thread thread = new Thread();
  System.out.println("Thread state is: " + thread.getState());
}

// Program output content
Thread state is: NEW

Therefore, after creating a thread object, the state of the thread is the NEW state.

4.2 RUNNABLE

The thread executing in the JVM is in the RUNNABLE state, that is, after calling the start method of a thread, wait for the CPU to allocate the time slice to the thread. When the thread is officially executed, it is in the RUNNABLE state. The simulation code is as follows:

public static void main(String[] args) {
  Thread thread = new Thread(() -> System.out.println("Thread state is: " + Thread.currentThread().getState()));
  thread.start();
}

// Program output content
Thread state is: RUNNABLE

Therefore, after calling the start method of a thread, if no exception is thrown, the thread will enter the RUNNABLE state.

It should be noted here that if a thread in non NEW state calls its start method, it will throw an IllegalThreadStateException. The reason is as follows:

// java.lang.Thread#start
public synchronized void start() {
	// If the thread state is not NEW, an exception is thrown
  // A zero status value corresponds to state "NEW". (the status corresponding to 0 value is NEW)
  if (threadStatus != 0)
    throw new IllegalThreadStateException();

  // Omit other logic
}

In addition, there are several situations that will enter the RUNNABLE state:

  • The thread in the BLOCKED state enters the RUNNABLE state due to successful lock acquisition
  • Waiting / timed is entered due to calling sleep and join methods_ A thread in waiting state will enter the RUNNABLE state when it exceeds the timeout, normally waits for the end, or calls the object#notify and object#notifyall methods
  • A thread in the RUNNABLE state re enters the RUNNABLE state by calling the yield method

4.3 BLOCKED

The thread is BLOCKED due to waiting for a lock and will be in the BLOCKED state. If you want to simulate the thread in this state, you need to introduce shared resources and a second thread. Thread 1 starts and occupies the lock resources first, and then starts thread 2. When thread 2 attempts to obtain the lock resources, it finds that the shared resources have been occupied by thread 1, so it enters the BLOCKED state. The simulation code is as follows:

public class StateBlocked {
		// shared resource
    private static String str = "lock";

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (str) {
                System.out.println("Thread1 get lock");
                // Prevent thread thread1 from releasing str lock resources
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        // Ensure that thread1 gets the lock resource first
        Thread.sleep(1000);

        Thread thread2 = new Thread(() -> {
            synchronized (str) {
                System.out.println("Thread1 get lock");
            }
        });
        thread2.start();
        // Ensure that thread2 enters the synchronized code block
        Thread.sleep(1000);
        System.out.println("Thread2 state is: " + thread2.getState());
    }
}

The output results of the above simulation code are as follows:

Thread1 get lock
Thread2 state is: BLOCKED
Thread1 get lock

Therefore, when a thread fails to obtain a lock due to entering the synchronization block, its state is BLOCKED.

4.4 WAITING

When a thread is waiting indefinitely for another thread to perform a specific operation, it is in the waiting state. Start a thread A, call the Thread#join method in another thread B, and thread B will wait for the thread A to finish, when the thread B is WAITING state, the simulation code is as follows:

public static void main(String[] args) throws InterruptedException {
  // Lock resource
  String str = "lock";
  Thread thread = new Thread(() -> {
    // sleep 100s is to have enough time to view the thread status
    try {
      Thread.sleep(100000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  });
  thread.start();
  // The main thread waits for the thread thread to finish executing
  thread.join();
}

After executing this method, we need to open jconsole, find the corresponding process and view its thread status, as shown in the following figure:

You can see that the status of the main thread is WAITING.

Therefore, when a thread's join method is called (no timeout is specified), the caller will enter the WAITING state.

In addition, when a thread's Object#wait is called (no timeout is specified), the caller will also enter the WAITING state.

4.5 TIMED_WAITING

The thread WAITING for another thread to perform an operation within the specified WAITING time is TIMED_WAITING status. TIMED_ The only difference between the WAITING state and the WAITING state is that the former specifies the timeout time. We can simulate timed by slightly changing the code in the previous step_ WAITING status, simulation code is as follows:

public static void main(String[] args) throws InterruptedException {
  // Lock resource
  String str = "lock";
  Thread thread = new Thread(() -> {
    // sleep 100s is to have enough time to view the thread status
    try {
      Thread.sleep(100000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  });
  thread.start();
  // The main thread waits for the thread thread to finish executing. The specified timeout is 10s
  thread.join(10000);
}

After executing this method, open jconsole, find the corresponding process and view its thread status, as shown in the following figure:

You can see that the status of the main thread is TIMED_WAITING.

Therefore, when a thread's join method is called (specifying the timeout), the caller will enter TIMED_WAITING status

In addition, when a thread's Object#wait (specified timeout) is called, the caller will also enter the WAITING state.

4.5 TERMINATED

The exited thread is in the TERMINATED state, which is the state in which the thread ends naturally. It is very easy to simulate. The code is as follows:

public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread();
  thread.start();
  // Wait for the thread to finish executing
  thread.join();
  System.out.println("Thread state is: " + thread.getState());
}

Therefore, when a thread ends normally, it will enter the TERMINATED state.

In addition, when a thread throws an exception and exits, it will also enter the TERMINATED state, such as waiting / timed_ The thread in waiting state calls the Thread#interrupt method and exits.

5. Thread state transition diagram

The above thread state transition relationship is summarized as follows:

 

6. Summary

This paper first introduces the relationship and difference between process and Thread, then describes the common API of Thread class and the form of creating Thread to execute tasks, then explains the Thread state in detail and gives examples, and finally summarizes the Thread state transition diagram.

Several small problems are summarized:

  • Relationship and difference between process and thread
  • What are the forms of creating thread tasks?
  • How many ways to create threads?
  • What are the thread states?
  • Describe the transition rules of thread state

Keywords: Java JDK Programmer Concurrent Programming

Added by evildobbi on Sun, 23 Jan 2022 03:53:55 +0200