Multithreading (1) | Talk about Thread and Runnable

Get into the habit of writing together! This is the first day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

The use of multi-threading should also be regarded as a difficulty that can never be bypassed in the process of Java language development. This series focuses on some API usages in multithreading. Note that the concept of multithreading will not be introduced here. If you do not know much about basic concepts such as multithreading, it is recommended to understand some concepts before learning. This article will start directly with the creation of multithreading.

1. Thread class and Runnable interface

The Thread class represents the abstraction of the thread. Since the startup and execution of the thread must deal with the underlying operating system, there are many native methods in the Thread class. This class also contains a lot of operations for threads. For example, we can create a thread through the construction method, we can start the thread through the start method, and there are commonly used methods such as interrupt, sleep, join, and yield.

Runnable itself is an interface, which has a method run with no return value. This interface abstracts the thread task, that is, what the thread is to do. It only represents the task, it does not have the ability to start the thread, so it must be used in the Thread class. The start method can start a thread.

Thread itself also implements the Runnable interface, and this class also has a run method, so we can specify our thread tasks by inheriting the Thread class and overriding the run method. In the parameterized construction method of Thread, we can also specify thread tasks by passing in a Runnable externally. Next, we will demonstrate two ways of multi-threading.

There are three steps to start multithreading:

  • Create a thread (Thread class and its subclasses)
  • Specify the task (Thread's run or Runnable's run)
  • Start the thread (Thread's start)

Second, the code case

2.1 Inheriting Thread

The Thread class itself already implements the Runnable interface, so we can inject thread tasks when overriding the run method by inheriting Thread.

image.png

package com.lsqingfeng.action.knowledge.multithread.thread;

/**
 * @className: ThreadDemo2
 * @description:
 * @author: sh.Liu
 * @date: 2022-04-06 13:48
 */
public class ThreadDemo2 extends Thread{

    public void run(){
        System.out.println(Thread.currentThread().getName() + ": I am Thread Task");
    }


    public static void main(String[] args) throws InterruptedException {
        ThreadDemo2 t1 = new ThreadDemo2();
        ThreadDemo2 t2 = new ThreadDemo2();

        t1.start();
        t2.start();

//        t1.join();
//        t2.join();

        System.out.println("执行结束");
    }
}
复制代码

上面的代码中,使用ThreadDemo2 继承了Thread类,所以ThreadDemo2代表一个线程,这个类中重新了run方法,就指定了这个线程的任务,然后通过调用start() 方法,将两个线程对象启动了。所以在当前程序中,共有三个线程,一个是t1线程,一个是t2线程,还有一个主线程。执行结果如下:

执行结束 
Thread-1: I am Thread Task 
Thread-0: I am Thread Task
复制代码

如果想让主线程在其他线程执行完之后在打印执行结束,可以使用join方法。

当然继承一个类并重写里面的方法,我们也可以使用匿名内部类的方式,来简化我们的代码。

new Thread(){
    public void run(){
        System.out.println("我是Thread类的子类中run方法的实现");
    }
}.start();
复制代码

如果这里有看不懂的同学,说明你对于匿名内部类这部分的知识理解的不够,那么就赶快去补充一下吧。 还有要注意,由于Thread类中的run 方法并不属于函数式接口,所以这里不能使用lambda表达式。

在这种方式下, Thread的子类对象中,即包含了线程对象,又充当了线程任务,相当于我们多线程的前两个步骤都是在一个类中完成的。接下来我们来看第二种。

2.2 实现Runnable

在Thread的构造方法中,我们是可以传入Runnable接口的,也就是我们可以通过构方法的方式传入线程任务。

image.png

还可以在传入任务的同时传入线程名称:

image.png

Runnable本身是一个接口,我们必须要给定他的实现才行。

package com.lsqingfeng.action.knowledge.multithread.thread;

/**
 * @className: ThreadDemo3
 * @description:
 * @author: sh.Liu
 * @date: 2022-04-06 14:05
 */
public class ThreadDemo3 implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "I am Thread Task");
    }

    public static void main(String[] args) {
        // r1 是一个线程任务,线程任务无法启动线程,必须依赖Thread
        Runnable r1 = new ThreadDemo3();

        // 创建一个线程,并传入这个线程的任务r1和线程名称
        Thread t1 = new Thread(r1, "thread-01");

        // 再创建一个线程
        Thread t2 = new Thread(r1, "th-02");

        // 分别启动: ,线程启动的时候会自动执行线程任务中的run方法
        t1.start();
        t2.start();

    }
}
复制代码

执行结果:

thread-01I am Thread Task

th-02I am Thread Task

在上面的代码中,我们就是把Runnable的子类(实现类)就是作为线程的任务来看待,创建线程还是使用的Thread, 只是把线程任务通过构造方法传入了进来,还是调用Thread类中的start方法启动线程。这里再次强调,Runnable是无法启动线程的,它里面只有一个run方法。只有Thread类及其子类才可以启动线程,线程启动后会自动调用线程任务中的run()方法。

上面的这种方式我们也可以使用匿名内部类来进行简化:

 // 匿名内部类:
Runnable r2 = new Runnable() {
    @Override
    public void run() {
        System.out.println("我是新的线程任务");
    }
};
new Thread(r2,"th-03").start();

// 写到一起:
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("我是新的线程任务2");
    }
},"th-04").start();

// 使用lambda表达式简化:,因为Runnable中只有一个需要实现的接口,属于函数式接口
new Thread(()->{
    System.out.println("我是新的线程任务3");
},"th-05").start();
复制代码

这两种方式,一般都认为第二种方式更优雅一些,符合单一职责的设计原则,线程就是线程,任务就是任务,各司其职,更加清晰一些。

三、源码分析

上面我们提到了两种实现多线程的方式,主要就是线程任务的传递方式不同,一种是在Thread子类中直接重写,一种是通过构造方法的方式传入。那如果我的线程对象通过两种方式同时传入了线程任务,哪一个线程任务会执行到呢?我们先来写个案例。我先写得清晰一点:

package com.lsqingfeng.action.knowledge.multithread.thread;

/**
 * @className: ThreadDemo4
 * @description:
 * @author: sh.Liu
 * @date: 2022-04-06 14:36
 */
public class ThreadDemo4 extends Thread{

    public ThreadDemo4(){

    }

    public ThreadDemo4(Runnable r){
        super(r);
    }

    public void run(){
        System.out.println("Thread 类中的run方法");
    }

    public static void main(String[] args) {
        // 先总结前面的方式:
        // 方式1 通过Thread子类来启动线程
        new ThreadDemo4().start();

        // 方式2: new Thread,传入Runnable接口:
        new Thread(new RunnableDemo()).start();


        // 两种混用:由于显示调用,所以必须在子类中把构造方法定义出来
        new ThreadDemo4(new RunnableDemo()).start();

    }

}

class RunnableDemo implements Runnable{

    @Override
    public void run() {
        System.out.println("Runnable中的run方法");
    }
}
复制代码

打印结果:

Thread 类中的run方法
Runnable中的run方法
Thread 类中的run方法

通过结果我们可以看出,当我们把两种方法混用的时候,线程任务是Thread的子类中的run方法生效了。 这是为什么呢? 这就需要了解一下源码了。

首先我们要了解: Thread类本身实现了Runnable接口,所以类中本身就有一个run方法: run方法的具体源码如下:

public
class Thread implements Runnable {
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

}
复制代码

那target是个啥呢: target是Thread类中的一个成员变量Runnable:

image.png

也就是说Thread这个类不光实现了Runnable 接口,还引入了Runnable 作为成员变量,我们通过有参构造方法传入的Runnable其实最后就是赋值到了target变量中。

image.png

调用了init方法:

image.png

在init方法中完成了对于target的赋值。

当我们通过调用start() 方法启动线程的时候,线程底层会自动调用run方法。这部分在代码中没有体现,是调用的 native方法,看不到源码。

所以当我们使用第二种方式启动线程的时候:

// 方式2: new Thread,传入Runnable接口:
new Thread(new RunnableDemo()).start();
复制代码

相当于我们完成了target的赋值,所以再调用run方法的时候:

 @Override
public void run() {
    if (target != null) {
        target.run();
    }
}
复制代码

由于target已经不为空了,所以调用的是外部传入的Runnable中的run方法。

当我们自己创建Thread的子类的时候,我们自己重写了run方法,所在在调用的时候,调用的是子类自己的run方法,上面判断target的代码不会被执行到(多态的知识点)。这也就解释了为什么第三种情况打印的是 Thread类中run方法。

And this question is easy to understand if you analyze it carefully, but sometimes the interview will combine anonymous inner classes to ask questions, such as:

public static void main(String[] args) {
      new Thread(
              ()->System.out.println("aaa")
      ){
          public void run(){
              System.out.println("bbbb");
          }
      }.start();

}
复制代码

Ask what is the result of the execution of this code: The answer is bbbb, if it is not clear, study anonymous inner classes well, and then read the above a few more times, you should be able to understand.

Finally, let's look at the start method:

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();
复制代码

The start method mainly sets several states, and then calls the native method start0() to enable multithreading. Opening a thread is an interface of the operating system, so it is necessary to call the native method implementation. So when someone asks the difference between the start() and run() methods, make it clear that start() will start the thread and automatically call run(), and calling the run() method alone will not start a new thread.

Well, I will talk about so much today's content first, and I may talk about thread pools next time.

Guess you like

Origin juejin.im/post/7083667845623742501