マルチスレッド(1)|スレッドとランナブルについて話す

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加した初日です。クリックしてイベントの詳細をご覧ください

マルチスレッドの使用も、Java言語開発のプロセスで回避できない困難と見なす必要があります。このシリーズは、マルチスレッドでのAPIの使用法に焦点を当てています。マルチスレッドの概念はここでは紹介しません。マルチスレッドなどの基本的な概念についてよく知らない場合は、学習する前にいくつかの概念を理解することをお勧めします。この記事は、マルチスレッドの作成から直接開始します。

1.スレッドクラスとRunnableインターフェイス

Threadクラスは、スレッドの抽象化を表します。スレッドの起動と実行は、基盤となるオペレーティングシステムを処理する必要があるため、Threadクラスには多くのネイティブメソッドがあります。このクラスには、スレッドの多くの操作も含まれています。たとえば、constructionメソッドを使用してスレッドを作成したり、startメソッドを使用してスレッドを開始したりできます。また、interrupt、sleep、join、yieldなどの一般的に使用されるメソッドがあります。

Runnable自体は、戻り値なしで実行されるメソッドを持つインターフェースです。このインターフェースは、スレッドタスク、つまりスレッドが実行することを抽象化します。これはタスクを表すだけであり、スレッドを開始する機能はありません。したがって、Threadクラスで使用する必要があります。startメソッドはスレッドを開始できます。

Thread自体もRunnableインターフェイスを実装し、このクラスにもrunメソッドがあるため、Threadクラスを継承し、runメソッドをオーバーライドすることで、スレッドタスクを指定できます。Threadのパラメーター化された構築方法では、Runnableを外部に渡すことでスレッドタスクを指定することもできます。次に、マルチスレッドの2つの方法を示します。

マルチスレッドを開始するには、次の3つの手順があります。

  • スレッドを作成します(スレッドクラスとそのサブクラス)
  • タスクを指定します(スレッドの実行またはRunnableの実行)
  • スレッドを開始します(スレッドの開始)

第二に、コードケース

2.1スレッドの継承

Threadクラス自体はすでにRunnableインターフェースを実装しているため、Threadを継承してrunメソッドをオーバーライドするときにスレッドタスクを挿入できます。

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方法。

また、この質問は注意深く分析すれば簡単に理解できますが、面接では匿名の内部クラスを組み合わせて次のような質問をすることがあります。

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

}
复制代码

このコードの実行の結果は何であるかを尋ねます。答えはbbbbです。明確でない場合は、匿名の内部クラスをよく調べてから、上記をさらに数回読んで理解できるはずです。

最後に、startメソッドを見てみましょう。

    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();
复制代码

startメソッドは主にいくつかの状態を設定し、ネイティブメソッドstart0()を呼び出してマルチスレッドを有効にします。スレッドを開くことはオペレーティングシステムのインターフェイスであるため、ネイティブメソッドの実装を呼び出す必要があります。したがって、誰かがstart()メソッドとrun()メソッドの違いを尋ねるときは、start()がスレッドを開始して自動的にrun()を呼び出し、run()メソッドを呼び出すだけでは新しいスレッドを開始しないことを明確にします。

さて、最初に今日のコンテンツについてたくさん話しますが、次回はスレッドプールについて話すかもしれません。

おすすめ

転載: juejin.im/post/7083667845623742501