首页 > 其他分享 >创建线程的三种方式

创建线程的三种方式

时间:2023-10-09 19:32:05浏览次数:43  
标签:Runnable run Thread 创建 任务 三种 线程 方法

「有点收获」三种基本方法创建线程 (qq.com)

创建线程的三种方法

线程英译是 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 方法代码的意思就是说,如果 target != 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("开始执行");
        for (int i = 0; i < 100; i++) {
        	System.out.print(i+"-");
        }
    }
};

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

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

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

标签:Runnable,run,Thread,创建,任务,三种,线程,方法
From: https://blog.51cto.com/coderge/7780491

相关文章

  • 2023-02-09-使用simple_term_menu创建更好的python菜单
    +++title="用simple_term_menu创建更好的python菜单"description=""date=2023-02-09T16:25:24+08:00featured=falsecomment=truetoc=truereward=truecategories=[""]tags=["python","menu"]seri......
  • Java21上手体验-分代ZGC和虚拟线程 | 京东云技术团队
    一、导语几天前Oracle刚刚发布了Java21,由于这是最新的LTS版本,引起了大家的关注。我也第一时间在个人项目中进行了升级体验。一探究竟,和大家分享。二、Java21更新内容介绍官方release公告:https://jdk.java.net/21/release-notes开源中国介绍:https://my.oschina.net/waylau/blog/10......
  • Jmeter之三种参数化
    参数化三种方式: 1.Jmeter之参数化txt例子:http://47.107.168.87:8080/futureloan/mvc/api/member/login 五条用例一条正常4条一场,可以发现异常的用例会提示对应的msg但是数据比较多的时候就很麻烦,这时候需要添加配置元件->csvdatasetconfig到D盘建立个文件:输入的......
  • 学习笔记-设计模式-创建型模式-单例模式
    单例模式一个类只有一个实例,并提供一个全局访问此实例的点,哪怕多线程同时访问。单例模式主要解决了一个全局使用的类被频繁的创建和消费的问题。单例模式的案例场景数据库的连接池不会反复创建spring中一个单例模式bean的生成和使用在我们平常的代码中需要设置全局的一些属......
  • metersphere接口调试时有返回响应体,但是创建接口单独测试时无响应体
    在用metersphere做接口测试时,单独做接口调试有响应体,但是放到接口中无响应体是环境配置 做接口测试时我用的都是这个mock环境,但是接口实际上调用的不是该地址,因此我在项目环境中重新建了环境就好了,需要注意的是在项目环境中配置地址时一定要点确定,不能只点更新 2、环境......
  • 1、为何说只有 1 种实现线程的方法?
    1、为何说只有1种实现线程的方法?为什么说本质只有一种实现线程的方式?实现Runnable接口究竟比继承Thread类实现线程好在哪里?目录1、为何说只有1种实现线程的方法?实现多线程的多种方式1、通过实现Runnable接口的方式实现多线程2、继承Thread类3、通过线程池创建线......
  • 设计模式 (1): 5 种创建型模式 (结合代码详解)
    目录1单例模式饿汉单例懒汉单例双重检验单例静态内部类单例破坏单例(反射、反序列化)枚举类单例2工厂模式简单工厂模式工厂方法模式3抽象工厂模式代码实现对比三种工厂模式如何选择工厂方法和抽象工厂?4建造者模式5原型模式1单例模式需考虑的问题:是否线程安全是否延......
  • 虚拟环境搭建、luffy后台项目创建,目录调整、封装logger、封装全局异常、封装Response
    虚拟环境搭建#1虚拟环境作用多个项目,自己有自己的环境,装的模块属于自己的#2使用pycharm创建-一般放在项目路径下:venv文件夹-lib文件夹---》site-package--》虚拟环境装的模块,都会放在这里-scripts--》python,pip命令#3本地创建......
  • 万字长文详解Java线程池面试题
    王有志,一个分享硬核Java技术的互金摸鱼侠加入Java人的提桶跑路群:共同富裕的Java人今天是《面霸的自我修养》第6篇文章,我们一起来看看面试中会问到哪些关于线程池的问题吧。数据来源:大部分来自于各机构(Java之父,Java继父,某灵,某泡,某客)以及各博主整理文档;小部分来自于......
  • luffy后台项目创建
    1.项目创建1.使用命令创建luffy项目2.创建虚拟环境mkvirtualenv-ppython310luffy3.安装djangopipinstalldjango==3.1.124.命令创建项目django-adminstartprojectluffy_api5.pycharm创建 2.目录调整"""├──luffyapi├......