《Java多线程面试题》系列-创建线程的三种方法及其区别
1. 创建线程的三种方法及其区别
1.1 继承Thread类
首先,定义Thread类的子类并重写run()方法:
package com.zwwhnly.springbootaction.javabase.thread;
public class MyFirstThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.printf("[MyFirstThread]输出:%d,当前线程名称:%s\n",
i, getName());
}
}
}
然后,创建该子类的实例并调用start()方法启动线程:
package com.zwwhnly.springbootaction.javabase.thread;
public class ThreadTest {
public static void main(String[] args) {
System.out.println("主线程开始执行,当前线程名称:" +
Thread.currentThread().getName());
Thread firstThread = new MyFirstThread();
firstThread.start();
System.out.println("主线程执行结束,当前线程名称:" +
Thread.currentThread().getName());
}
}
运行结果如下所示:
主线程开始执行,当前线程名称:main
主线程执行结束,当前线程名称:main
[MyFirstThread]输出:0,当前线程名称:Thread-0
[MyFirstThread]输出:1,当前线程名称:Thread-0
[MyFirstThread]输出:2,当前线程名称:Thread-0
[MyFirstThread]输出:3,当前线程名称:Thread-0
[MyFirstThread]输出:4,当前线程名称:Thread-0
从运行结果可以看出以下2个问题:
- 程序中存在2个线程,分别为主线程main和自定义的线程Thread-0。
- 调用
firstThread.start();,run()方法体中的代码并没有立即执行,而是异步执行的。
查看Thread类的源码,可以发现Thread类实现了接口Runnable:
public class Thread implements Runnable {
// 省略其它代码
}
这里是重点,面试常问!
1.2 实现Runnable接口(推荐)
首先,定义Runnable接口的实现类并实现run()方法:
package com.zwwhnly.springbootaction.javabase.thread;
public class MySecondThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.printf("[MySecondThread]输出:%d,当前线程名称:%s\n",
i, Thread.currentThread().getName());
}
}
}
然后,调用Thread类的构造函数创建Thread实例并调用start()方法启动线程:
package com.zwwhnly.springbootaction.javabase.thread;
public class ThreadTest {
public static void main(String[] args) {
Runnable target = new MySecondThread();
Thread secondThread = new Thread(target);
secondThread.start();
}
}
运行结果如下所示:
主线程开始执行,当前线程名称:main
主线程执行结束,当前线程名称:main
[MySecondThread]输出:0,当前线程名称:Thread-0
[MySecondThread]输出:1,当前线程名称:Thread-0
[MySecondThread]输出:2,当前线程名称:Thread-0
[MySecondThread]输出:3,当前线程名称:Thread-0
[MySecondThread]输出:4,当前线程名称:Thread-0
可以看出,使用这种方式和继承Thread类的运行结果是一样的。
1.3 实现Callable接口
首先,定义Callable接口的实现类并实现call()方法:
package com.zwwhnly.springbootaction.javabase.thread;
import java.util.Random;
import java.util.concurrent.Callable;
public class MyThirdThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(6 * 1000);
return new Random().nextInt();
}
}
然后,调用FutureTask类的构造函数创建FutureTask实例:
Callable<Integer> callable = new MyThirdThread();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
最后,调用Thread类的构造函数创建Thread实例并调用start()方法启动线程:
package com.zwwhnly.springbootaction.javabase.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
public static void main(String[] args) {
System.out.println("主线程开始执行,当前线程名称:" +
Thread.currentThread().getName());
Callable<Integer> callable = new MyThirdThread();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
try {
System.out.println("futureTask.isDone() return:" + futureTask.isDone());
System.out.println(futureTask.get());
System.out.println("futureTask.isDone() return:" + futureTask.isDone());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("主线程执行结束,当前线程名称:" +
Thread.currentThread().getName());
}
}
运行结果如下所示:
主线程开始执行,当前线程名称:main
futureTask.isDone() return:false
-1193053528
futureTask.isDone() return:true
主线程执行结束,当前线程名称:main
可以发现,使用Callable接口这种方式,我们可以通过futureTask.get()获取到线程的执行结果,而之前的2种方式,都是没有返回值的。
注意事项:调用
futureTask.get()获取线程的执行结果时,主线程会阻塞直到获取到结果。
阻塞效果如下图所示:

1.4 区别
以下是重点,面试常问!
- Java中,类仅支持单继承,如果一个类继承了Thread类,就无法再继承其它类,因此,如果一个类既要继承其它的类,又必须创建为一个线程,就可以使用实现Runable接口的方式。
- 使用实现Runable接口的方式创建的线程可以处理同一资源,实现资源的共享。
- 使用实现Callable接口的方式创建的线程,可以获取到线程执行的返回值、是否执行完成等信息。
关于第2点,可以通过如下示例来理解。
假如我们总共有10张票(共享的资源),为了提升售票的效率,开了3个线程来售卖,代码如下所示:
package com.zwwhnly.springbootaction.javabase.thread;
public class SaleTicketThread implements Runnable {
private int quantity = 10;
@Override
public void run() {
while (quantity > 0) {
System.out.println(quantity-- + " is saled by " +
Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
Runnable runnable = new SaleTicketThread();
Thread saleTicketThread1 = new Thread(runnable);
Thread saleTicketThread2 = new Thread(runnable);
Thread saleTicketThread3 = new Thread(runnable);
saleTicketThread1.start();
saleTicketThread2.start();
saleTicketThread3.start();
}
因为3个线程都是异步执行的,因此每次的运行结果可能是不一样,以下列举2次不同的运行结果。
第1次运行结果:
10 is saled by Thread-0
8 is saled by Thread-0
7 is saled by Thread-0
5 is saled by Thread-0
9 is saled by Thread-1
3 is saled by Thread-1
2 is saled by Thread-1
1 is saled by Thread-1
4 is saled by Thread-0
6 is saled by Thread-2
第2次运行结果:
10 is saled by Thread-0
9 is saled by Thread-0
8 is saled by Thread-0
7 is saled by Thread-0
6 is saled by Thread-0
5 is saled by Thread-0
3 is saled by Thread-0
2 is saled by Thread-0
4 is saled by Thread-2
1 is saled by Thread-1
如果将上面的SaleTicketThread修改成继承Thread类的方式,就变成了3个线程各自拥有10张票,即变成了30张票,而不是3个线程共享10张票。
2. Thread类start()和run()的区别
2.1 示例
因为实现Runnable接口的优势,基本上实现多线程都使用的是该种方式,所以我们将之前定义的MyFirstThread也修改为实现Runnable接口的方式:
package com.zwwhnly.springbootaction.javabase.thread;
public class MyFirstThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.printf("[MyFirstThread]输出:%d,当前线程名称:%s\n",
i, Thread.currentThread().getName());
}
}
}
然后仍然沿用之前定义的MyFirstThread、MySecondThread,我们先看下调用start()的效果:
package com.zwwhnly.springbootaction.javabase.thread;
public class ThreadTest {
public static void main(String[] args) {
System.out.println("主线程开始执行,当前线程名称:" +
Thread.currentThread().getName());
Thread firstThread = new Thread(new MyFirstThread());
Runnable target = new MySecondThread();
Thread secondThread = new Thread(target);
firstThread.start();
secondThread.start();
System.out.println("主线程执行结束,当前线程名称:" +
Thread.currentThread().getName());
}
}
运行结果(注意:多次运行,结果可能不一样):
主线程开始执行,当前线程名称:main
[MyFirstThread]输出:0,当前线程名称:Thread-0
[MyFirstThread]输出:1,当前线程名称:Thread-0
[MySecondThread]输出:0,当前线程名称:Thread-1
主线程执行结束,当前线程名称:main
[MySecondThread]输出:1,当前线程名称:Thread-1
[MySecondThread]输出:2,当前线程名称:Thread-1
[MySecondThread]输出:3,当前线程名称:Thread-1
[MySecondThread]输出:4,当前线程名称:Thread-1
[MyFirstThread]输出:2,当前线程名称:Thread-0
[MyFirstThread]输出:3,当前线程名称:Thread-0
[MyFirstThread]输出:4,当前线程名称:Thread-0
可以看出,调用start()方法后,程序中有3个线程,分别为主线程main、Thread-0、Thread-1,而且执行顺序不是按顺序执行的,存在不确定性。
然后将start()方法修改为run()方法,如下所示:
firstThread.run();
secondThread.run();
此时的运行结果如下所示(多次运行,结果是一样的):
主线程开始执行,当前线程名称:main
[MyFirstThread]输出:0,当前线程名称:main
[MyFirstThread]输出:1,当前线程名称:main
[MyFirstThread]输出:2,当前线程名称:main
[MyFirstThread]输出:3,当前线程名称:main
[MyFirstThread]输出:4,当前线程名称:main
[MySecondThread]输出:0,当前线程名称:main
[MySecondThread]输出:1,当前线程名称:main
[MySecondThread]输出:2,当前线程名称:main
[MySecondThread]输出:3,当前线程名称:main
[MySecondThread]输出:4,当前线程名称:main
主线程执行结束,当前线程名称:main
可以看出,调用run()方法后,程序中只有一个主线程,自定义的2个线程并没有启动,而且执行顺序也是按顺序执行的。
1.2 总结
以下是重点,面试常问!
- run()方法只是一个普通方法,调用之后程序会等待run()方法执行完毕,所以是串行执行,而不是并行执行。
- start()方法会启动一个线程,当线程得到CPU资源后会自动执行run()方法体中的内容,实现真正的并发执行。
3. Runnable和Callable的区别
在文章前面的章节中(1.2 实现Runnable接口 和1.3 实现Callable接口),我们了解了如何使用Runnable、Callable接口来创建线程,现在我们分别看下Runable和Callable接口的定义,其中,Runable接口的定义如下所示:
public interface Runnable {
public abstract void run();
}
Callable接口的定义如下所示:
public interface Callable<V> {
V call() throws Exception;
}
由此可以看出,Runnable和Callable的区别主要有以下几点:
- Runable的执行方法是run(),Callable的执行方法是call()
- call()方法可以抛出异常,run()方法如果有异常只能在内部消化
- 实现Runnable接口的线程没有返回值,实现Callable接口的线程能返回执行结果
- 实现Callable接口的线程,可以和FutureTask一起使用,获取到线程是否完成、线程是否取消、线程执行结果,也可以取消线程的执行。
4. 源码及参考
源码地址:https://github.com/zwwhnly/springboot-action.git,欢迎下载。
Java Thread 的 run() 与 start() 的区别
《Java多线程面试题》系列-创建线程的三种方法及其区别的更多相关文章
- Java中创建线程的三种方法以及区别
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例.Java可以用三种方式来创建线程,如下所示: 1)继承Thread类创建线程 2)实现Runnable接口创建线 ...
- java创建线程的三种方法
这里不会贴代码,只是将创建线程的三种方法做个笼统的介绍,再根据源码添加上自己的分析. 通过三种方法可以创建java线程: 1.继承Thread类. 2.实现Runnable接口. 3.实现Callab ...
- Java并发编程(二)创建线程的三种方法
进程与线程 1. 进程 进程和代码之间的关系就像音乐和乐谱之间的关系一样,演奏结束的时候音乐就不存在了但乐谱还在:程序执行结束的时候进程就消失了但代码还在,而计算机就是代码的演奏家. 2. 线程 线 ...
- python 多线程编程之threading模块(Thread类)创建线程的三种方法
摘录 python核心编程 上节介绍的thread模块,是不支持守护线程的.当主线程退出的时候,所有的子线程都将终止,不管他们是否仍在工作. 本节开始,我们开始介绍python的另外多线程模块thre ...
- java多线程(一)创建线程的四种方式
1. 什么是并发与并行 要想学习多线程,必须先理解什么是并发与并行 并行:指两个或多个事件在同一时刻发生(同时发生). 并发:指两个或多个事件在同一个时间段内发生. 2. 什么是进程.线程 进 ...
- JAVA中创建线程的三种方法及比较
JAVA中创建线程的方式有三种,各有优缺点,具体如下: 一.继承Thread类来创建线程 1.创建一个任务类,继承Thread线程类,因为Thread类已经实现了Runnable接口,然后重写run( ...
- Java中创建线程的三种方式以及区别
在java中如果要创建线程的话,一般有3种方法: 继承Thread类: 实现Runnable接口: 使用Callable和Future创建线程. 1. 继承Thread类 继承Thread类的话,必须 ...
- 27 多线程(一)——创建进程的三种方法、线程锁(同步synchornized与lock)
线程的流程 线程的创建 有三种方法,重点掌握前两种: 继承Thread类 实现Runnable接口(推荐使用:避免单继承的局限性) 实现Callable接口 根据java的思想,要少用继承,多用实现. ...
- java中创建线程的几种方法及区别
1,实现Runnable接口创建线程 特点: A:将代码和数据分开,形成清晰的模型 B:线程体run()方法所在的类可以从其它类中继承一些有用的属性和方法 C:有利于保持程序风格的一致性 2,继承Th ...
随机推荐
- R-plotly|交互式甘特图(Gantt chart)-项目管理/学习计划
本文首发于“生信补给站”微信公众号,https://mp.weixin.qq.com/s/CGz51qOjFSJ4Wx_qOMzjiw 更多关于R语言,ggplot2绘图,生信分析的内容,敬请关注小号 ...
- 单引号、双引号与定界符——PHP
单引号与双引号 单引号和双引号在echo输出时的区别 echo输出时,如果使用单引号,那么echo会把单引号之间的全部内容当成普通字符串输出,不能识别变量和转义字符(单引号串中的内容总被认为是普通字符 ...
- Struts2:搭建原理
记录下,struts2的搭建过程: 1核心jar包: struts-2.1.8\apps\struts2-blank-2.1.8.war 解压后 在struts2-blank-2.1.8\WEB-IN ...
- spring cloud 优雅停机
spring cloud 优雅停机 大部分部署项目如果要停掉项目一般都是用kill -9 来杀进程 但是由于Eureka采用心跳的机制来上下线服务,会导致服务消费者调用已经kill的服务提供者然后出错 ...
- 基于 HTML5 Canvas 的楼宇自控系统
前言 楼宇自控是指楼宇中电力设备,如电梯.水泵.风机.空调等,其主要工作性质是强电驱动.通常这些设备是开放性的工作状态,也就是说没有形成一个闭环回路.只要接通电源,设备就在工作,至于工作状态.进程.能 ...
- 由浅入深:Python 中如何实现自动导入缺失的库?
在写 Python 项目的时候,我们可能经常会遇到导入模块失败的错误:ImportError: No module named 'xxx' 或者 ModuleNotFoundError: No mod ...
- Java 异常(二) 自定义异常
上篇文章介绍了java中异常机制,本文来演示一下自定义异常 上篇文章讲到非运行时异常和运行时异常,下面我们来看一下简单实现代码. 首先,先来看下演示目录 非运行时异常 也称 检查时异常 public ...
- MyBatis(2)-- MyBatis配置mybatis-config.xml
一.properties属性 1.可以在mybatis-config.xml中使用property子元素配置 <properties resource="jdbc.properties ...
- Hadoop环境部署
1.虚拟机克隆 在VM界面点击查看-自定义-库,然后在左边我的计算机下右键点击安装好的第一个系统,然后管理-克隆,选择克隆系统所在的文件路径即可. 2.三台主机名字修改 root用户下: (1)编辑n ...
- Redis(十三)Python客户端redis-py
一.安装redis-py的方法 使用pip install安装redis-py C:\Users\BigJun>pip3 install redis Collecting redis Downl ...