前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多线程(一) | 聊聊Thread和Runnable

多线程(一) | 聊聊Thread和Runnable

作者头像
一缕82年的清风
发布2022-04-08 17:11:08
5450
发布2022-04-08 17:11:08
举报
文章被收录于专栏:lsqingfenglsqingfeng

多线程的用法应该也算是Java语言开发过程中永远绕不过去的一个难点。本系列着重讲解多线程中的一些API用法。注意这里对于多线程的概念将不展开介绍,如果对于什么是多线程等基本概念不太了解的,建议先了解一些概念后再来学习。本文将直接从多线程的创建开始。

一、Thread类和Runnable接口

Thread类就是代表了线程的抽象,由于线程的启动执行等必然要和底层的操作系统打交道,所有在Thread类中有很多native修饰的本地方法。这个类中也包含了很多对于线程的操作。比如通过构造方法我们可以创建线程,通过start方法我们可以启动线程,还有interrupt, sleep, join,yield等常用的方法。

Runnable本身是一个接口,里边有一个无返回值的方法run, 这个接口抽象的是线程任务,就是这个线程要做什么,他仅仅只代表任务,它没有启动线程的能力,因此必须使用Thread类中的start方法才能够启动一个线程。

而Thread本身也实现了Runnable接口,这个类里也有run方法,所以我们可以通过继承Thread类重写run方法的方式来指定我们的线程任务。 而在Thread的有参构造方法中,我们也可以通过外部传入一个Runnable来指定线程任务。接下来我们就分别演示两种多线程的方式。

启动多线程总共分为三步:

  • 创建线程(Thread类及其子类)
  • 指定任务(Thread的run或者 Runnable的run)
  • 启动线程 (Thread的start)

二、代码案例

2.1 继承Thread

Thread类本身就已经实现了Runnable接口,所以我们可以通过继承Thread的方式,在重写run方法的时候,把线程任务注入进来。

代码语言:javascript
复制
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线程,还有一个主线程。执行结果如下:

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

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

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

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

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

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

2.2 实现Runnable

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

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

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

代码语言:javascript
复制
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()方法。

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

代码语言:javascript
复制
 // 匿名内部类:
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子类中直接重写,一种是通过构造方法的方式传入。那如果我的线程对象通过两种方式同时传入了线程任务,哪一个线程任务会执行到呢?我们先来写个案例。我先写得清晰一点:

代码语言:javascript
复制
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方法的具体源码如下:

代码语言:javascript
复制
public
class Thread implements Runnable {
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

}

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

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

调用了init方法:

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

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

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

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

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

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

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

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

而这个问题如果仔细分析还是比较好理解的,但有时候面试中会结合匿名内部类,来提问,比如:

代码语言:javascript
复制
public static void main(String[] args) {
      new Thread(
              ()->System.out.println("aaa")
      ){
          public void run(){
              System.out.println("bbbb");
          }
      }.start();

}

问这段代码执行的结果是什么: 答案是 bbbb, 如果不清楚的,好好学习匿名内部类,再多读几遍上文,应该就可以理解了。

最后我们再来看看start方法:

代码语言:javascript
复制
    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方法中主要就是设置了几个状态,然后调用了native方法start0(), 开启了多线程。开启线程是操作系统的接口,所以必然要调用native的方法实现。所以当有人再问start() 和run()方法区别的时候,要说清楚,start()才会启动线程并且自动调用run(), 单独调用run()方法不会启动新线程。

好了,今天的内容先讲这么多,下次有可能会聊聊线程池。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-04-07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Thread类和Runnable接口
  • 二、代码案例
    • 2.1 继承Thread
      • 2.2 实现Runnable
      • 三、源码分析
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档