[High concurrency series] 4. Core principles and basic operations of threads

Today, I will summarize the core principles and basic operations of threads in Java.

1. The core principle of thread

Since modern operating systems provide powerful thread management capabilities, Java entrusts thread scheduling work to the scheduling process of the operating system without independent thread management and scheduling.

1. Thread scheduling model

First of all, it is necessary to figure out the relationship between thread scheduling and CPU time slice.

What is a CPU time slice? Because the CPU calculation frequency is very high, even up to 1 billion calculations per second, if we segment the CPU time from the millisecond dimension, then each small segment is a CPU time slice.

Since the time slice is very short and the threads are switched quickly, it looks like many threads are "executing at the same time" or "executing concurrently". At present, the mainstream thread scheduling method of the operating system is based on the CPU time slice. That is to say, a thread can execute an instruction (execution state) only if it gets a CPU time slice, and a thread that does not get a CPU time slice will wait for the system to allocate the next CPU time slice (ready state).

There are currently two main thread scheduling models, namely time-sharing scheduling and preemptive scheduling.

The time-sharing scheduling model means that the operating system will allocate CPU time slices equally, and all threads will occupy the CPU in turn, which means "all beings are equal".

The preemptive scheduling model refers to that the operating system allocates CPU time slices according to thread priorities. Threads with high priority are assigned CPU time slices first. If all ready threads have the same priority, a thread will be randomly selected.

At present, most operating systems adopt a preemptive scheduling model, because Java's thread management and scheduling are entrusted to the operating system, so Java's thread scheduling is also a preemptive scheduling model.

2. Thread priority

There is an instance attribute (private int priority;) and two instance methods (getPriority(), setPriority()) in the Java thread Thread class, which are used for operations related to thread priority.

private int            priority;

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

public final void setPriority(int newPriority) {
    ......
}

public final int getPriority() {
    return priority;
}

The minimum value of the thread priority is 1, and the maximum value is 10. The priority attribute of the Thread instance defaults to level 5, and the corresponding class constant is NORM_PRIORITY.

Since Java uses a preemptive scheduling model for thread scheduling, the higher the priority attribute of the thread instance, the higher the probability of obtaining CPU time slices!

3. Thread life cycle

There are six states in the life cycle of threads in Java, including the execution state and ready state mentioned above.

In the Thread class, there is an instance attribute (private int threadStatus;) and an instance method (public Thread.State getState();) which are used to save and obtain the state of the thread. Among them, Thread.State is an internal enumeration class, which represents the six states of Java threads by defining six enumeration constants, as follows:

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

Next, the 6 states of the Java thread and the conditions for entering each state will be introduced in detail.

3.1 New state (NWE)

After the thread is created, but the thread is not started through the start() method, the thread is in the new state at this time.

The four ways of creating threads introduced in the first two articles, but the first three are essentially created through new Thread(), which creates different target execution target instances (such as Runnable instances).

3.2 Runnable state (RUNABLE)

In Java, the ready state (READY) and the execution state (RUNNING) are collectively referred to as the runnable state. After the start() of the thread is called, the thread enters the ready state. If the thread obtains the CPU time slice, it will execute the business logic in run() and enter the execution state.

Please note that the ready state can only indicate that the thread is eligible to run. As for when to enter the execution state, it depends on the scheduling situation of the operating system.

So, what conditions can make the thread enter the ready state?

  • 1. After calling the thread's start() method.
  • 2. The CPU time slice allocated by the current thread is exhausted.
  • 3. The thread sleep operation ends.
  • 4. The thread coalescing operation ends.
  • 5. The current thread yields CPU execution rights.
  • 6. Wait for user input to complete.
  • 7. The thread competes for the object lock.

3.3 Blocked state (BLOCKED)

The thread is in the blocked state and will not occupy the CPU. The following situations will cause the thread to enter the blocked state:

  • Waiting to acquire a lock.
  • io blocking (disk io, network io, etc.).

A thread waits to acquire a lock, and the lock is held by other threads, the thread enters the blocked state, and only when other threads release the lock and the thread scheduler allows the thread to hold the lock, the thread exits the blocked state.

As for io blocking, after a thread performs a blocking io operation, if it does not meet the io operation conditions, it will enter the blocking state. A simple example is when a thread waits for user input to complete before continuing.

3.4 Waiting indefinitely (WAITING)

The thread is in an indefinite waiting state and needs to be explicitly woken up by other threads before it can enter the ready state.

There are three ways to wait indefinitely and be woken up:

  • The thread instance method join() is called, and the corresponding wake-up method is: the execution of the merged thread is completed.
  • Object.wait() method, the corresponding wakeup method is: call Object.notify() or Object.notifyAll().
  • LockSupport.park() method, the corresponding wakeup method is to call LockSupport.unpark(…).

3.5 Timed waiting (TIMED_WAITING)

If the thread is not awakened within the specified time, the thread that is waiting for a limited time will be automatically awakened by the system, and then enter the ready state.

There are also three ways to wait for a time limit and be woken up:

  • The thread sleep(time) method is called, and the corresponding wake-up method is: the end of the sleep time.
  • Object.wait(time) method, the corresponding wakeup method is: call Object.notify() or Object.notifyAll() to wake up actively, or end within a time limit.
  • LockSupport.parkNanos(time) or parkUntil(time) method, the corresponding wake-up method is: the thread calls the supporting LockSupport.unpark(Thread) method to end, or the thread stop time limit ends.

3.6 Dead state (TERMINATED)

The thread executes the task and enters the death state normally. Or an exception occurs during the execution of the task, but the exception is not handled, which will also cause the thread to die.

4. Daemon thread

A daemon thread is also called a background thread, which refers to a thread that provides some general service in the background while the program process is running. For example, every time a JVM process is started, a series of GC (garbage collection) threads will run in the background. These GC threads are daemon threads and provide garbage collection services in the background.

An instance property (daemon) and two instance methods (setDaemon, isDaemon) are provided in the Java Thread class to operate the daemon thread. The instance attribute daemon saves the daemon state of a thread instance, and the default is false, which means that the thread is a user thread by default. The set method can set the daemon thread (true) or the user thread (false), while the is method is used to determine the thread type.

From the perspective of daemon threads, Java threads can be divided into user threads and daemon threads. The essential difference between the two lies in the direction of termination of the Java virtual machine process.

User threads and JVM processes are in an active relationship. If all user threads are terminated, the JVM virtual machine process will also be terminated; daemon threads and JVM processes are in a passive relationship. If the JVM process is terminated, all daemon threads will also be terminated. As shown below:

For a better understanding, it can be explained from another angle. The daemon thread can be regarded as a service provider, and the user thread can be regarded as a consumer.

Only when all user threads are terminated, that is, there are no consumers, the services provided by the daemon threads are meaningless, and they can all be terminated. In other words, as long as there is still a user thread, the daemon thread is still necessary!

Please note:

  • If the thread is a daemon thread, you must call setDaemon(true) of the thread instance before calling the start() method of the thread instance, and set its daemon instance attribute value to true, otherwise JVM will throw InterruptedException.
  • The daemon thread has the risk of being terminated by the JVM. Avoid using daemon threads to access system resources (database connections, file handles, etc.), otherwise system resources may be irreparably damaged.
  • Create a thread in a daemon thread, and the new thread is also a daemon thread. After creation, the new thread can be tuned to be a user thread if it is explicitly set to be a user thread by calling setDaemon(false).

Second, the basic operation of the thread

The common operations of Java threads are basically in the Thread class, including some static methods and thread instance methods.

1. set/get thread name

Setting and getting the thread name is the most basic operation of Java thread. We can initialize and set the thread name through the Thread constructor, or call the instance method setName(…) to set it, and get the thread name through the getName() method.

//入参带线程名称的 Thread 构造器
public Thread(String name){......}
public Thread(Runnable target, String name) {......}
public Thread(ThreadGroup group, String name) {......}
public Thread(ThreadGroup group, Runnable target, String name) {......}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {......}

public final synchronized void setName(String name) {
    ......
}

public final String getName() {
    return name;
}

In practice, there are some specifications to know about thread names:

  • Avoid setting the same thread name for different thread objects;
  • If the thread name is not set, the system will automatically set the thread name, usually in the form of Thread-number;
  • If the Thread.currentThread() static method is called to obtain the name of the currently executing thread, the thread is the current thread, indicating the Java thread currently executing code logic;
  • When creating a thread or thread pool, you need to name the thread name with a specified meaning, which is convenient for error reporting and troubleshooting!

2. Thread sleep

Thread sleep refers to letting the thread currently executing the task sleep, allowing the CPU to perform other tasks. From the perspective of thread state, the thread changes from the execution state to the blocking state.

The Java Thread class provides a set of static methods for thread sleep, as follows:

public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos)
    throws InterruptedException {
    ......
}

Please note:

  • Using the sleep() method needs to catch interrupt exceptions, and the time unit of the input parameter is milliseconds or nanoseconds.
  • After the thread sleep time expires, the thread may not execute immediately, because the CPU may be performing other tasks, and the thread at this time will enter the ready state and wait for the next CPU time slice to be allocated.

3. Thread interruption

Thread interruption refers to interrupting and terminating a thread that is executing a task.

The stop() method (marked obsolete) is provided in the Java Thread class for terminating a running thread, why is it deprecated?

Because the stop() method is a simple, crude and dangerous operation. For example, when the washing machine at home is doing laundry, the water heater is boiling water, or other electrical appliances are working, suddenly the switch is pulled off, and all electrical appliances will be forcibly terminated. Isn't it very unhappy! And wouldn’t it be better to change to a more elegant way of power outage, for example, if I want to have a power outage in half an hour, I just send out a notification or call to mark the state of power outage, and all the electrical appliances at home have completed the scheduled work and then do the power outage Time to prepare.

From a program point of view, it is also not possible to terminate the thread running casually, because we don't know what the running threads are doing. This thread may be performing the task of operating the database, and forced termination will cause data inconsistency; this thread may also hold a lock, and forced termination will cause the problem that the lock cannot be released. It is precisely because calling the stop() method to forcibly terminate the thread will cause unpredictable results, so it is not recommended!

So, how to gracefully terminate the running thread? In fact, the principle is the same as that explained in the power failure example, by marking the interrupt state instead of directly interrupting.

The interrupt() method is provided in the Java Thread class, which can be used to terminate threads more gracefully. There are two usage scenarios for this method:

  • If the thread is blocked (for example, calling wait(), sleep(), join() and other methods to cause blocking), calling the interrupt() method will immediately exit the blocking state and throw an InterruptedException exception, and the thread can pass through Catch the exception to do some processing, and then let the thread exit.
  • If the thread is running, the thread is not affected in any way, but the thread's interrupt flag field is set to true. We can check whether we are interrupted by calling the isInterrupted() method at the appropriate position, and perform the exit operation.
public void interrupt() {
    ......
    synchronized(){
        ......
    }
    ......
}

public boolean isInterrupted() {
    ......
}

Therefore, the interrupt() method only changes the interrupt status, it does not interrupt a running thread.

As for whether the thread stops executing, the program needs to monitor the isInterrupted() state of the thread and perform corresponding processing. The demonstration is as follows:

    @Test
    public void testInterrupted() throws InterruptedException {
        Thread thread = new Thread() {
            @SneakyThrows
            public void run() {
                log.info("线程启动了...");
                //一直循环
                while (true) {
                    log.info(String.valueOf(isInterrupted()));
                    Thread.sleep(1000);
                    //如果线程被中断,就退出死循环
                    if (isInterrupted()) {
                        log.info("线程结束了...");
                        return;
                    }
                }
            }
        };
        thread.start();
        Thread.sleep(2000); //等待2秒
        thread.interrupt(); //中断线程
    }

Output result:

4. Thread Merging

Assuming that there are two threads A and B, thread A needs to rely on the execution logic of thread B during the running process, which is thread merging. In order to better understand the process of thread A merging thread B, the drawing is as follows:

A set of instance methods are provided in the Java Thread class for merging operations. Here, the join() method has three overloaded versions, as follows:

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
    throws InterruptedException {
    ......
}

public final synchronized void join(long millis, int nanos)
    throws InterruptedException {
    ......
}

requires attention:

  • When thread A merges with thread B, it needs to use the instance of thread B to call the join() method. At this time, thread A will give up the CPU time slice and enter the waiting state.
  • When thread A is in the waiting state, if it is interrupted, an InterruptedException will be thrown.
  • Thread A can be in an infinite waiting state or a time-limited waiting state, depending on whether the join() method uses a time-limited parameter as an input parameter.
  • The advantage of thread merging is that it is easy to operate, but the disadvantage is that the execution result of thread B is not known.

We know that when a thread calls the wait() method of Object, if there is no wake-up operation, the thread will always be in the waiting state. This is also the case for join() without parameters. If thread B has been executing, thread A will wait for thread B to complete execution before ending the waiting state of thread A. Of course, the join() method with time-limited parameters, that is, thread A that is waiting for a time limit, will end the waiting state and turn into a runnable state regardless of whether thread B has finished executing the logic.

Please note that the waiting state of the thread ending may not be allocated to the CPU time slice immediately, it depends on the specific scheduling situation of the operating system, after all, the speaker is the operating system~

5. Thread concessions

Thread concession means that the thread currently executing the task gives up the operation it is executing, and gives up the right to use the CPU, so that the CPU can execute other threads. From the perspective of thread state, the thread currently executing the task changes from the execution state to the ready state.

A static method yield() is provided in the Java Thread class, which means that the currently executing thread gives up the use of the CPU time slice and suspends the thread, but it does not block the thread, but only turns to the ready state.

public static native void yield();

As for when the thread reoccupied the CPU time slice is uncertain, it depends on the scheduling situation of the system (priority, etc.). It may be allocated a CPU time slice just after giving up, or it may be allocated for a long time after giving up.

Finally~

Here, the scheduling model of Java threads, priority, life cycle, six thread states and entry conditions, daemon threads and user threads, and basic operations of threads, such as thread sleep, interrupt, merge, and concession, are mainly sorted out and summarized. . Java multithreading is a basic and important content. It can be said that it is a must-know and necessary for development. It is necessary to learn and master it.

[Without accumulating silicon, there is no way to reach a thousand miles, let's make progress together~]

Guess you like

Origin blog.csdn.net/qq_29119581/article/details/129311559