Home  >  Article  >  Java  >  Detailed analysis of multi-threaded programming methods in JAVA (with examples)

Detailed analysis of multi-threaded programming methods in JAVA (with examples)

王林
王林forward
2019-08-30 11:46:251979browse

1. Program, process, thread

A program is an ordered collection of instructions, which can also be understood popularly as several lines of code. It itself has no meaning of running. It is just a static entity. It may be a simple text file, or it may be an executable file generated after compilation.
 In a narrow sense, a process is an instance of a running program; in a broad sense, a process is a running activity of a program with certain independent functions on a certain data collection. The process is the basic unit of resource allocation by the operating system.
Thread is the smallest unit in a process that can be executed independently. It is also the basic unit for independent scheduling and dispatch by the processor. A process can contain multiple threads, each thread performs its own tasks, and all threads in the same process share resources in the process, such as memory space, file handles, etc.

2. Introduction to multi-threaded programming

1. What is multi-threaded programming

Multiple Thread programming technology is an important feature of the Java language. The meaning of multi-threaded programming is to divide program tasks into several parallel sub-tasks and assign these sub-tasks to multiple threads for execution.
Multi-threaded programming is a programming paradigm with threads as the basic abstract unit. However, multi-threaded programming is not just as simple as using multiple threads for programming, but also has its own problems that need to be solved. Multithreaded programming and object-oriented programming are compatible, that is, we can implement multi-threaded programming based on object-oriented programming. In fact, a thread in the Java platform is an object.

2. Why should we use multi-threaded programming?

Nowadays computers often have multi-processor cores, and each thread can only run on one processor at the same time. superior. If only a single thread is used for development, the resources of the multi-core processor cannot be fully utilized to improve the execution efficiency of the program. When using multi-threading for programming, different threads can run on different processors. In this way, not only the utilization of computer resources is greatly improved, but also the execution efficiency of the program is improved.

3. Introduction to JAVA Thread API


The java.lang.Thread class is the implementation of threads by the Java platform. An instance of the Thread class or its subclass is a thread.

1. Creation, startup, and running of threads

In the Java platform, creating a thread is an example of creating a Thread class (or its subclass). Each thread has a task to perform. The thread's task processing logic can be directly implemented in the run method of the Thread class or called through this method. Therefore, the run method is equivalent to the entry method of the thread's task processing logic. It should be directly called by the Java virtual machine when running the corresponding thread. It should not be called by application code.

 Running a thread actually means letting the Java virtual machine execute the run method of the thread, so that the task processing logic code can be executed. If a thread is not started, its run method will never be executed. To do this, you first need to start the thread. The start method of the Thread class is used to start the corresponding thread. The essence of starting a thread is to request the virtual machine to run the corresponding thread, and when this thread can run is determined by the thread scheduler (the thread scheduler is part of the operating system). Therefore, calling the thread's start method does not mean that the thread has started running. The thread may start running immediately, may be run later, or may never run.

  Two ways to create threads are introduced below (actually there are other ways, which will be introduced in detail in subsequent articles). Before that, let’s take a look at the source code of the run method of the Thread class:

// Code 1-1@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

This run method is defined in the interface Runnable. It does not accept parameters and has no return value. In fact, there is only one method in the Runnable interface, so this interface is a functional interface, which means that we can use lambda expressions where Runnable is needed. The Thread class implements this interface, so it must implement this method. target is a field in the Thread class, and its type is also Runnable. The target field indicates what this thread needs to execute, and the run method of the Thread class only executes the run method of the target.
We just mentioned that the Java virtual machine automatically calls the run method of the thread. However, the run method of the Thread class has been defined, and we have no way to put the code we need to execute in the run method of the Thread class. Therefore, we can consider other ways to affect the behavior of the run method. The first is to inherit the Thread class and override the run method, so that the JVM will call our overridden run method instead of the run method of the Thread class when running the thread; the second method is to pass the code we want to execute to Thread The target method of the class, and the Thread class happens to have several constructors that can directly assign values ​​to the target. In this way, the JVM still executes the code we passed when calling the run method.
 In the Java platform, each thread can have its own default name. Of course, we can also give our thread a name when constructing an instance of the Thread class. This name makes it easier for us to distinguish different threads.
The following code uses the above two methods to create two threads. The task they have to perform is very simple - print a line of welcome message and include their own names.

public class WelcomeApp {
    public static void main(String[] args) {
        Thread thread1 = new WelcomeThread();
        Thread thread2 = new Thread(() -> System.out.println("2. Welcome, I'm " + Thread.currentThread().getName()));
        thread1.start();
        thread2.start();
    }
}class WelcomeThread extends Thread {
    @Override
    public void run() {
        System.out.println("1. Welcome, I'm " + Thread.currentThread().getName());
    }
}

The following is the output of this program when it is run:

1. Welcome, I'm Thread-0
2. Welcome, I'm Thread-1

Run this program multiple times, we can find that the output of this program may also be:

2. Welcome, I'm Thread-1
1. Welcome, I'm Thread-0

This shows that , although thread1 is started before thread2, this does not mean that thread1 will be run before thread2.
  No matter which method is used to create a thread, once the execution of the thread's run method (called by the JVM) ends, the running of the corresponding thread will also end. Of course, the execution end of the run method includes normal end (the run method returns normally) and termination caused by exceptions thrown in the code. The resources (such as memory space) occupied by the thread that has finished running will be recycled by the JVM like other Java objects.
 Threads are "disposable items". We cannot re-run a thread that has finished running by re-calling the start method. In fact, the start method can only be called once. Calling the start method of the same Thread instance multiple times will cause it to throw an IllegalThreadStateException exception.

2. Thread attributes

Thread attributes include thread number, name, category and priority. The details are as shown in the following table:

Detailed analysis of multi-threaded programming methods in JAVA (with examples)

The concepts of daemon threads and user threads are mentioned above. Here is a brief explanation of them. According to whether the thread will prevent the Java virtual machine from stopping normally, we can divide the threads in Java into daemon threads (Daemon Thread) and user threads (User Thread, also called non-daemon threads). The thread's daemon attribute is used to indicate whether the corresponding thread is a daemon thread. User threads will prevent the Java virtual machine from stopping normally. That is, a Java virtual machine can stop normally only when all its user threads have finished running (that is, the Thread.run() call has not ended). The daemon thread will not affect the normal stop of the Java virtual machine. Even if the daemon thread is running in the application, it will not affect the normal stop of the Java virtual machine. Therefore, daemon threads are usually used to perform tasks that are not very important, such as monitoring the running status of other threads.
 Of course, if the Java virtual machine is forcibly stopped, such as using the kill command to forcefully terminate a Java virtual machine process under the Linux system, then even the user thread cannot prevent the Java virtual machine from stopping.

3. Common methods of Thread class

Detailed analysis of multi-threaded programming methods in JAVA (with examples)

#Any piece of code in Java is always executed in a certain thread. The thread executing the current code is called the current thread, and Thread.currentThread() can return the current thread. Since the same piece of code may be executed by different threads, the current thread is relative, that is, the return value of Thread.currentThread() may correspond to different threads (objects) when the code is actually running.
The function of the join method is equivalent to the thread that executes the method and the thread scheduler says: "I have to pause first, and I can not continue until the other thread finishes running."
The function of the yield static method is equivalent to the execution The thread of this method says to the thread scheduler: "I'm not in a hurry now. If someone else needs the processor resources, let them use it first. Of course, if no one else wants to use it, I don't mind continuing to occupy it."
  sleep The role of a static method is equivalent to the thread executing the method saying to the thread scheduler: "I want to take a nap, wake me up after a while and continue working."

4、Thread类中的废弃方法

Detailed analysis of multi-threaded programming methods in JAVA (with examples)

虽然这些方法并没有相应的替代品,但是可以使用其他办法来实现,我们会在后续文章中学习这部分内容。

四、无处不在的线程

Java平台本身就是一个多线程的平台。除了Java开发人员自己创建和使用的线程,Java平台中其他由Java虚拟机创建、使用的线程也随处可见。当然,这些线程也是各自有其处理任务。
  Java虚拟机启动的时候会创建一个主线程(main线程),该线程负责执行Java程序的入口方法(main方法)。下面的程序打印出主线程的名称:

public class MainThreadDemo {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

  该程序会输出“main”,这说明main方法是由一个名为“main”的线程调用的,这个线程就是主线程,它是由JVM创建并启动的。
  在多线程编程中,弄清楚一段代码具体是由哪个(或者哪种)线程去负责执行的这点很重要,这关系到性能、线程安全等问题。本系列的后续文章会体现这点。
  Java 虚拟机垃圾回收器(Garbage Collector)负责对Java程序中不再使用的内存空间进行回收,而这个回收的动作实际上也是通过专门的线程(垃圾回收线程)实现的,这些线程由Java虚拟机自行创建。
  为了提高Java代码的执行效率,Java虚拟机中的JIT(Just In Time)编译器会动态地将Java字节码编译为Java虚拟机宿主机处理器可直接执行的机器码。这个动态编译的过程实际上是由Java虚拟机创建的专门的线程负责执行的。
  Java平台中的线程随处可见,这些线程各自都有其处理任务。

五、线程的层次关系

Java平台中的线程不是孤立的,线程与线程之间总是存在一些联系。假设线程A所执行的代码创建了线程B, 那么,习惯上我们称线程B为线程A的子线程,相应地线程A就被称为线程B的父线程。例如, Code 1-2中的线程thread1和thread2是main线程的子线程,main线程是它们的父线程。子线程所执行的代码还可以创建其他线程,因此一个子线程也可以是其他线程的父线程。所以,父线程、子线程是一个相对的称呼。理解线程的层次关系有助于我们理解Java应用程序的结构,也有助于我们后续阐述其他概念。
  在Java平台中,一个线程是否是一个守护线程默认取决于其父线程:默认情况下父线程是守护线程,则子线程也是守护线程;父线程是用户线程,则子线程也是用户线程。另外,父线程在创建子线程后启动子线程之前可以调用该线程的setDaemon方法,将相应的线程设置为守护线程(或者用户线程)。
  一个线程的优先级默认值为该线程的父线程的优先级,即如果我们没有设置或者更改一个线程的优先级,那么这个线程的优先级的值与父线程的优先级的值相等。
  不过,Java平台中并没有API用于获取一个线程的父线程,或者获取一个线程的所有子线程。并且,父线程和子线程之间的生命周期也没有必然的联系。比如父线程运行结束后,子线程可以继续运行,子线程运行结束也不妨碍其父线程继续运行。

六、线程的生命周期状态

 在Java平台中,一个线程从其创建、启动到其运行结束的整个生命周期可能经历若干状态。如下图所示:

Detailed analysis of multi-threaded programming methods in JAVA (with examples)

The status of the thread can be obtained by calling Thread.getState(). The return value type of Thread.getState() is Thread.State, which is an enumeration type inside the Thread class. The thread states defined by Thread.State include the following:
   NEW: A thread that has been created but not started is in this state. Since a thread instance can only be started once, a thread may be in this state only once.
- RUNNABLE: This state can be regarded as a composite state, which includes two sub-states: READY and RUNNING, but in fact these two states are not defined in Thread.State. The former means that the thread in this state can be scheduled by the thread scheduler to make it in the RUNNING state. The latter indicates that the thread in this state is running, that is, the instructions corresponding to the run method of the corresponding thread object are being executed by the processor. The status of the thread executing Thread.yield() may be converted from RUNNING to READY. Threads in the READY substate are also called active threads.
   BLOCKED: After a thread initiates a blocking I/0 operation, or applies for an exclusive resource (such as a lock) held by other threads, the corresponding thread will be in this state. Threads in the BLOCKED state do not occupy processor resources. When the blocking 1/0 operation is completed, or the thread obtains the resources it requested, the status of the thread can be converted to RUNNABLE.
  WAITING: After a thread executes some specific methods, it will be in this state of waiting for other threads to perform some other specific operations. Methods that can change its execution thread to the WAITING state include: Object.wait(), Thread.join() and LockSupport.park(Object). The corresponding methods that can change the corresponding thread from WAITING to RUNNABLE include: Object.notify()/notifyAll() and LockSupport.unpark(Object)).
   TIMED_WAITING: This state is similar to WAITING. The difference is that the thread in this state does not wait indefinitely for other threads to perform specific operations, but is in a waiting state with a time limit. When other threads do not perform the specific operations expected by the thread within the specified time, the status of the thread is automatically converted to RUNNABLE.
  TERMINATED: The thread that has finished executing is in this state. Since a thread instance can only be started once, a thread can only be in this state once. The run method returns normally or terminates early due to throwing an exception, which will cause the corresponding thread to be in this state.
 A thread can only be in the NEW state and the TERMINATED state once in its entire life cycle.

7. Advantages of multi-threaded programming

Multi-threaded programming has the following advantages:

Improve the efficiency of the system Throughput rate: Multi-threaded programming allows multiple concurrent (that is, simultaneous) operations in one process. For example, while one thread is waiting for an I/0 operation, other threads can still perform their operations.

Improve responsiveness: When using multi-threaded programming, for GUI software (such as desktop applications), a slow operation (such as downloading a large file from the server) will not cause The interface of the software appears to be "frozen" and cannot respond to other user operations; for web applications, the slow processing of one request will not affect the processing of other requests.

Make full use of multi-core processor resources: Nowadays, multi-core processor devices are becoming more and more popular, and even consumer devices such as mobile phones commonly use multi-core processors. Implementing proper multi-threading programming helps us to fully utilize the multi-core processor resources of the device and avoid wasting resources.

Multi-threaded programming also has its own problems and risks, including the following aspects:

Thread safety issues. When multiple threads share data, if corresponding concurrent access control measures are not taken, data consistency problems may occur, such as reading dirty data (expired data) and lost updates (updates made by some threads are deleted by other threads). Updates made by threads overwrite), etc.

Thread activity problem. The entire life cycle of a thread from its creation to the end of its execution will go through various states. From the perspective of a single thread, the RUNNABLE state is the desired state. But in fact, improper code writing may cause some threads to be in a state of waiting for other threads to release the lock (BLOCKED state). This situation is called a deadlock (Deadlock). Of course, problems may also occur with a thread that is always busy. It may face a livelock problem, that is, a thread has been trying an operation but cannot make progress. In addition, threads are a scarce computing resource, and the number of processors a system has is always very small compared to the number of threads present in the system. In some cases, the problem of thread starvation (Starvation) may occur, that is, some threads can never get a chance to be executed by the processor and are always in the READY substate of the RUNNABLE state.

Context switching. When the processor switches from executing one thread to executing another thread, an action required by the operating system is called a context switch. Due to the scarcity of processor resources, context switching can be regarded as an inevitable by-product of multi-threaded programming. It increases system consumption and is not conducive to system throughput.

To learn more about related issues, please visit the PHP Chinese website: JAVA Video Tutorial

The above is the detailed content of Detailed analysis of multi-threaded programming methods in JAVA (with examples). For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:cnblogs.com. If there is any infringement, please contact admin@php.cn delete