专栏首页飞天小牛肉「有点收获」三种基本方法创建线程

「有点收获」三种基本方法创建线程

挺基础的知识,一开始不是很愿意写,毕竟这种简单的知识大家不一定愿意看,而且容易写的大众化,不过还好梳理一遍下来还算是有点收获,比如我看了 Thread 类重写的 run 方法,才明白为什么可以把任务(Runnable)和线程本身(Thread)分开来。

创建线程的三种方法

线程英译是 Thread,这也是 Java 中线程对应的类名,在 java.lang 包下。

注意下它实现了 Runnable 接口,下文会详细解释。

线程与任务合并 — 直接继承 Thread 类

线程创建出来自然是需要执行一些特定的任务的,一个线程需要执行的任务、或者说需要做的事情就在 Thread 类的 run 方法里面定义。

这个 run 方法是哪里来的呢?

事实上,它并不是 Thread 类自己的。Thread 实现了 Runnable 接口,run 方法正是在这个接口中被定义为了抽象方法,而 Thread 实现了这个方法。

所以,我们把这个 Runnable 接口称为任务类可能更好理解。

如下,就是通过集成 Thread 类创建一个自定义线程 Thread1 的示例:

// 自定义线程对象
class Thread1 extends Thread {
    @Override
 public void run() {
  // 线程需要执行的任务
  ......
   }
}

// 创建线程对象
Thread1 t1 = new Thread1();

看这里,Thread 类提供了一个构造函数,可以为某个线程指定名字:

所以,我们可以这样:

// 创建线程对象
Thread1 t1 = new Thread1("t1");

这样,控制台打印的时候就比较明了,一眼就能知道是哪个线程输出的。

当然了,一般来说,我们写的代码都是下面这种匿名内部类简化版本的:

// 创建线程对象
Thread t1 = new Thread("t1") {
 @Override
 // run 方法内实现了要执行的任务
 public void run() {
  // 线程需要执行的任务
     ......
  }
};

线程与任务分离 — Thread + 实现 Runnable 接口

假如有多个线程,这些线程执行的任务都是一样的,那按照上述方法一的话我们岂不是就得写很多重复代码?

所以,我们考虑把线程执行的任务与线程本身分离开来。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程需要执行的任务
     ......
    }
}

// 创建任务类对象
MyRunnable runnable = new MyRunnable();
// 创建线程对象
Thread t2 = new Thread(runnable);

除了避免了重复代码,使用实现 Runnable 接口的方式也比方法一的单继承 Thread 类更具灵活性,毕竟一个类只能继承一个父类,如果这个类本身已经继承了其它类,就不能使用第一种方法了。另外,用这种方式,也更容易与线程池等高级 API 相结合。

因此,一般来说,更推荐使用这种方式去创建线程。也就是说,不推荐直接操作线程对象,推荐操作任务对象。

上述代码使用匿名内部类的简化版本如下:

// 创建任务类对象
Runnable runnable = new Runnable() {
    public void run(){
        // 要执行的任务
        ......
    }
};

// 创建线程对象
Thread t2 = new Thread(runnable);

同样的,我们也可以为其指定线程名字:

Thread t2 = new Thread(runnable, "t2");

以上两个 Thread 的构造函数如图所示:

可以发现,Thread 类的构造函数无一例外全部调用了 init 方法,这个方法到底做了啥?我们点进去看看:

它将构造函数传进来的 Runnable 对象传给了一个成员变量 target。

target 就是 Thread 类中定义的 Runnable 对象,代表着需要执行的任务(What will be run)。

这个变量的存在,就是我们能够把任务(Runnable)和线程本身(Thread)分开的原因所在。看下面这段代码:

没错,这就是 Thread 类默认实现的 run 方法。

在使用第一种方法创建线程的时候,我们定义了一个 Thread 子类并重写了其父类的 run 方法,所以这个父类实现的 run 方法不会被执行,执行的是我们自定义的子类中的 run 方法。

而在使用第二种方法创建线程的时候,我们并没有在 Thread 子类中重写 run 方法,所以父类默认实现的 run 方法就会被执行。

而这段 run 方法代码的意思就是说,如果 taget != null,也就是说如果 Thread 构造函数中传入了 Runnable 对象,那就执行这个 Runnable 对象的 run 方法。

线程与任务分离 — Thread + 实现 Callable 接口

虽然 Runnable 挺不错的,但是仍然有个缺点,那就是没办法获取任务的执行结果,因为它的 run 方法返回值是 void。

这样,对于需要获取任务执行结果的线程来说,Callable 就成为了一个完美的选择。

Callable 和 Runnable 基本差不多:

和 Runnbale 比起来,Callable 不过就是把 run 改成了 call。当然,最重要的是!和 void run 不同,这个 call 方法是拥有返回值的,而且能够抛出异常。

这样,一个很自然的想法,就是把 Callable 作为任务对象传给 Thread,然后 Thread 重写 call 方法就完事儿。

But,遗憾的是,Thread 类的构造函数里并不接收 Callable 类型的参数。

所以,我们需要把 Callable 包装一下,包装成 Runnable 类型,这样就能传给 Thread 构造函数了。

为此,FutureTask 成为了最好的选择。

可以看到 FutureTask 间接继承了 Runnable 接口,因此它也可以看作是一个 Runnable 对象,可以作为参数传入 Thread 类的构造函数。

另外,FutureTask 还间接继承了 Future 接口,并且,这个 Future 接口定义了可以获取 call() 返回值的方法 get:

看下面这段代码,使用 Callable 定义一个任务对象,然后把 Callable 包装成 FutureTask,然后把 FutureTask 传给 Thread 构造函数,从而创建出一个线程对象。

另外,Callable 和 FutureTask 的泛型填的就是 Callable 任务返回的结果类型(就是 call 方法的返回类型)。

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 要执行的任务
        ......
        return 100;
    }
}
// 将 Callable 包装成 FutureTask,FutureTask也是一种Runnable
MyCallable callable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(callable);
// 创建线程对象
Thread t3 = new Thread(task);

当线程运行起来后,可以通过 FutureTask 的 get 方法获取任务运行结果:

Integer result = task.get();

不过,需要注意的是,get 方法会阻塞住当前调用这个方法的线程。比如说我们在主线程中调用了 get 方法去获取 t3 线程的任务运行结果,那么只有这个 call 方法成功返回了,主线程才能够继续往下执行。

换句话说,如果 call 方法一直得不到结果,那么主线程也就一直无法向下运行。

启动线程

OK,综上,我们已经把线程成功创建出来了,那么怎么把它启动起来呢?

以第一种创建线程的方法为例:

// 创建线程
Thread t1 = new Thread("t1") {
 @Override
 // run 方法内实现了要执行的任务
 public void run() {
  // 线程需要执行的任务
     ......
  }
};

// 启动线程
t1.start();

这里涉及一道经典的面试题,即为什么使用 start 启动线程,而不使用 run 方法启动线程

使用 run 方法启动线程看起来好像并没啥问题,对吧,run 方法内定义了要执行的任务,调用 run 方法不就执行了这个任务了?

这确实没错,任务确实能够被正确执行,但是并不是以多线程的方式,当我们使用 t1.run() 的时候,程序仍然是在创建 t1 线程的 main 线程下运行的,并没有创建出一个新的 t1 线程。

举个例子:

// 创建线程
Thread t1 = new Thread("t1") {
    @Override
    public void run() {
      // 线程需要执行的任务
      System.out.println("开始执行");
      FileReader.read(文件地址); // 读文件
    }
};

t1.run();
System.out.println("执行完毕");

如果使用 run 方法启动线程,"执行完毕" 这句话需要在文件读取完毕后才能够输出,也就是说读文件这个操作仍然是同步的。假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 CPU 什么都做不了,其它代码都得暂停。

而如果使用 start 方法启动线程,"执行完毕" 这句话在文件读取完毕之前就会被很快地输出,因为多线程让方法执行变成了异步的,读取文件这个操作是 t1 线程在做,而 main 线程并没有被阻塞。

本文分享自微信公众号 - 飞天小牛肉(CS-Wiki),作者:飞天小牛肉

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-05-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java线程的三种创建方法

    1、创建Callable接口的实现类, 并实现cal()方法, 该cal()方法将作为线程执行体,并且有返回值。

    背雷管的小青年
  • Java 创建线程有哪几种方法

    2. 实现 Runnable 接口的 run 方法, 然后再用 Thread 类包裹后,调用 start 方法。

    水货程序员
  • Java 还有第三种创建多线程的方式?

    我们在多线程编程中最常用的两种方式:一种是直接继承Thread,另外一种就是实现Runnable接口。这两种方式都有一个缺陷就是:在执行完任务之后无法获取执行结...

    互扯程序
  • java创建子线程为什么要有两种方法?

    马克-to-win:通过以下两种方法创建子线程:1)声明一个Thread类的子类。 2)实现runnable接口。java的官方文档也没强调这二者有什么区别。马...

    马克java社区
  • 并非编程系列之创建线程的方法有多少种?

    并非编程系列之创建线程的方法有多少种?并发多线程的知识是很重要而且比较杂的知识点,所以需要花不少时间用于整理。创建线程的方式是学习并发编程的一个很基础的问题,所...

    SmileNicky
  • 总结java创建文件夹的4种方法及其优缺点-JAVA IO基础总结第三篇

    本文为大家介绍Java IO-创建文件夹的四种方法,及其优缺点的解析。如果您阅读完成,觉得此文对您有帮助,请给我点个赞,您的支持是我不竭的创作动力。

    字母哥博客
  • Swift多线程:使用Thread进行多线程间通讯,协调子线程任务1. Thread的三种建立方式2. Thread的基本使用3. 使用NSCondition实现线程间通讯4. pthread

    stanbai
  • Python黑帽编程 4.1 Sniffer(嗅探器)之数据捕获(上)

    Python黑帽编程 4.1 Sniffer(嗅探器)之数据捕获(上) 网络嗅探,是监听流经本机网卡数据包的一种技术,嗅探器就是利用这种技术进行数据捕获和分析的...

    用户1631416
  • 敖丙在位置上肝了一个月的后端知识点长啥样?

    前段时间敖丙不是在复习嘛,很多小伙伴也想要我的复习路线,以及我自己笔记里面的一些知识点,好了,丙丙花了一个月的时间,整整一个月啊,给大家整理出来了。

    敖丙
  • scala快速入门系列【Actor并发编程】

    本篇作为scala快速入门系列的第三十八篇博客,为大家带来的是关于Actor并发编程的内容。

    大数据梦想家
  • 面试官:Java线程池了解?如果你还回答不好,那还不赶快收藏!

    本文将根据面试中常被问到的 Java线程池 展开抽丝剥茧的解析,这个问题可以说是百分之百会在Java程序员面试中被问到,因为在工作中这个需求实在是太普遍了。Ja...

    捡田螺的小男孩
  • 零基础入门:基于开源WebRTC,从0到1实现实时音视频聊天功能

    本文由微医云技术团队前端工程师张宇航分享,原题“从0到1打造一个 WebRTC 应用”,有修订和改动。

    JackJiang
  • 跟着源码一起学:手把手教你用WebSocket打造Web端IM聊天

    本文作者芋艿,原题“芋道 Spring Boot WebSocket 入门”,本次有修订和改动。

    JackJiang
  • java 中的 Executors 简介与多线程在网站上逐步优化的运用案例

    忽略了自定义的ThreadFactory、callable和unconfigurable相关的方法

    Java知音
  • Java线程池实现原理及其在美团业务中的实践

    本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池的设计思路,最后回归实践,通过案例讲述使用线程池遇到的问题,并给出了一种动态化线程池解决方...

    美团技术团队
  • Java线程池实现原理及其在美团业务中的实践

    随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:Thread...

    李林LiLin
  • iOS开发-RunLoop

    从字面意思来看:跑圈、运动循环 基本用法:保持程序持续运行、处理App中的各种事件(触摸事件、定时器事件、SEL等等) 为什么需要它:节省CPU资源、 提高...

    孙寅
  • 音视频直播技术--Android视频采集(Camera2)

    今天为大家介绍一下如何在 Android 上进行视频采集。在 Android 系统下有两套 API 可以进行视频采集,它们是 Camera 和 Camera2 ...

    音视频_李超
  • 工作两年多,XX 征信 面试,offer已到手

    大家好,我是老田,今天给大家分享的是一位两年多工作经验的小伙伴面试经历,恭喜他成功上岸,收到了offer!本文大部分内容是这位朋友所写,我对一小部分内容进行修正...

    田维常

扫码关注云+社区

领取腾讯云代金券