天天看点

Java创建线程的方式到底有几种?创建线程

文章目录

  • 创建线程
    • 创建线程的两种方式(本质)
      • 继承Thread类
      • 实现Runnable接口(推荐)
    • 其他创建线程的方式(表面)
      • 通过线程池
      • 通过Callable 和 FutureTask
      • 通过定时器
    • 总结
      • 最准确的描述
      • 两者的本质区别
      • 优缺点
    • 两种方法同时使用会怎样

创建线程

创建线程的本质上只有继承Thread类 和 实现Runnable接口两种方式,其他方式如通过线程池创建线程、通过Callable 和 FutureTask创建线程、通过定时器创建线程等,其本质还是通过上述两种方式进行创建线程,他们都只不过是包装了new Thread( )。

多线程的实现方式,在代码中写法千变万化,但是其本质万变不离其宗。

创建线程的两种方式(本质)

继承Thread类

public class ThreadStyle extends Thread{
   //重写Thread类的run方法
   @Override
   public void run() {
      System.out.println("通过继承Thread类实现线程");
   }

   public static void main(String[] args) {
      /*
      因为继承Thread类之后重写了Thread类的run方法,所以这里调用的是继承Thread类的子类
      重写的run方法,这里即ThreadStyle中的run方法
       */
      //直接创建继承Thread的子类的实例,然后通过实例对象调用start方法开启线程
      ThreadStyle threadStyle = new ThreadStyle();
      threadStyle.start();
   }
}
           

实现Runnable接口(推荐)

public class RunnableStyle implements Runnable{
   //实现Runnable接口中的run方法
   @Override
   public void run() {
      System.out.println("通过Runnable接口实现线程");
   }

   public static void main(String[] args) {
      /*
         @Override
         public void run() {
            if (target != null) {
               target.run();
            }
         }
         所以实现Runnable接口的方式实现线程,最终调用的目标对象的run方法,
         这里即new RunnableStyle()对象的run方法,即上面的run方法
       */
      //传入Runnable接口的实现类对象作为target参数值,然后创建一个Thread对象
      Thread thread = new Thread(new RunnableStyle());
      thread.start();
   }
}
           

其他创建线程的方式(表面)

通过线程池

/**
 * 通过线程池的方式创建线程
 * 本质还是通过继承Thread类和实现Runnable接口两种方式
 */
public class ThreadPool5 {

   public static void main(String[] args) {
      /**
       * 深入源码可以看出,线程池创建线程的本质还是通过new Thread的方法
      public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
       */
      ExecutorService executorService = Executors.newCachedThreadPool();

      for (int i = 0; i < 1000; i++){
         executorService.submit(new Tasktest(){

         });
      }
   }
}

class Tasktest implements Runnable{

   @Override
   public void run() {
      //线程休眠
      try {
         Thread.sleep(500);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      //打印当前线程的名字
      System.out.println(Thread.currentThread().getName());
   }
}
           

通过Callable 和 FutureTask

Java创建线程的方式到底有几种?创建线程
  • Thread类实现了Runnable接口,且Thread类也由Runnable接口组成(聚合关系);
  • RunnableFuture 继承了 Runnable 和 Future 两个接口;
  • FutureTask 实现了RunnableFuture 接口,并且由Thread类组成(聚合关系),由Callable 组成(聚合关系);
Java创建线程的方式到底有几种?创建线程
  • RunnableFuture 接口继承了 Runnable接口和Future接口;
  • FutureTask 实现了 RunnableFuture 接口;
  • FutureTask 由Callable 组成

聚合关系:强调整体与部分的关系,整体由部分构成,比如一个部门有多个员工组成;

与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分依然存在;

例如: 部门撤销了,员工依然在;

组合关系:与聚合关系一样,表示整体由部分构成,比如公司有多个部门组成;

但是组合关系是一种强依赖的特殊聚合关系,如果整体不在了,则部门也不在了;

例如:公司不在了,部门也将不在了;

聚合关系:用一条带空心菱形箭头的直线表示;

组合关系:用一条带实心菱形箭头的直线表示;

参考:

五分钟读懂UML类图

看懂UML类图和时序图

类图

通过上面的类图分析,可以知道:通过Callable 和 FutureTask创建线程,实质上底层还是通过继承Thread 和 实现Runnable接口这两种方式创建线程。

主要步骤:

  • (1)创建Callable接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值;
  • (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象包装了该Callable对象的call() 方法的返回值;
  • (3)使用FutureTask对象作为Thread对象的target创建并启动线程;
  • (4)调用FutureTask对象的get() 方法来获得子线程执行结束后的返回值;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 通过Callable和FutureTask创建线程
 * 万变不离其宗
 */
public class CallableandFutureTask implements Callable<Integer> {

   public static void main(String[] args) {
      //创建Callable接口实现类的实例
      CallableandFutureTask callableandFutureTask = new CallableandFutureTask();
      //使用FutureTask包装Callable对象(其包装了Callable对象的call()方法的返回值)
      FutureTask<Integer> futureTask = new FutureTask<>(callableandFutureTask);
      for (int i = 0; i < 100; i++){
         System.out.println(Thread.currentThread().getName()+" 的循环变量i的值 "+i);
         if (i == 20){//i等于20的时候开始执行子线程
            //将FutureTask类的实例作为target传入Thread类中,创建并启动线程(类似实现Runnable接口方式创建线程)
            new Thread(futureTask,"有返回值的线程").start();
         }
      }

      try {
         //调用FutureTask对象的get()方法来获取子线程执行结束后的返回值
         System.out.println("子线程的返回值:"+ futureTask.get());
      } catch (InterruptedException e) {
         e.printStackTrace();
      } catch (ExecutionException e) {
         e.printStackTrace();
      }
   }

   // 实现Callable接口的call方法
   @Override
   public Integer call() throws Exception {
      int i = 0;
      for (; i < 100; i++){
         System.out.println(Thread.currentThread().getName()+" "+i);
      }
      return i;
   }
}
           

参考:

java创建线程的三种方式及其对比

通过定时器

通过定时器创建线程,本质其实还是通过继承Thread类 和 Runnable接口两种方式创建线程

import java.util.Timer;
import java.util.TimerTask;

public class DemoTimerTask {

   public static void main(String[] args) {
      Timer timer = new Timer();
      /*
      用于定时按周期做任务
      第一个参数是task: 表示执行的任务
      第二个参数是delay:表示初始化延时,即初始化延迟多少时间开始执行;
      第三个参数是period:表示每个多少时间执行一次任务(周期)
      注意:这里的period表示的是相邻两个任务开始之间的时间,因此执行时间不会延后
      总结起来就是:启动后过了delay时间之后,开始以period为间隔执行task任务

      还需注意schedule和scheduleAtFixedRate的区别
       */
      timer.scheduleAtFixedRate(new TimerTask() {
         @Override
         public void run() {
            System.out.println(Thread.currentThread().getName());
         }
      },5000,1000);
   }
}
           

说明一下schedule方法和scheduleAtFixedRate方法的区别

  • scheduleAtFixedRate:每次执行时间为上一次任务开始起向后推一个period间隔,也就是说下次开始执行时间相对于上一次任务开始的时间间隔,因此执行时间不会延后,但是存在任务并发执行的问题。(period:相邻两次任务开始之间的时间间隔)
  • schedule:每次执行时间为上一次任务结束后推一个period间隔,也就是说下次开始执行时间相对于上一次任务结束的时间间隔,因此执行时间会不断延后。(period:上次任务结束之后开始计时,即上次任务结束到下次任务开始之间的间隔)
timer.schedule(new TimerTask() {
   @Override
   public void run() {
      System.out.println(Thread.currentThread().getName());
   }
},5000,1000);
           

参考:

Java定时任务调度详解

总结

最准确的描述

  • (1)按Oracle文档来说,创建线程我们通常可以分为两类:继承Thread类 和 实现Runnable接口。
  • (2)准确地讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元(实现类里面的run方法)有两种方式:
    • 实现Runnable接口的 run 方法,并把Runnable实例传给 Thread 类;
    • 重写Thread的 run 方法(继承 Thread 类);

两者的本质区别

  • 继承Thread类方式:继承Thread类的子类必须重写Thread类中的run方法,所以最终调用的也是重写之后的run方法;
  • 实现Runnable接口方式:实现Runnable接口中的run方法,然后将Runnable接口的实现类对象传入Thread类,所以最终调用的是Runnable接口的实现类中的run方法;

优缺点

实现Runnable接口方式相比继承Thread类的优点:

  • (1)更有利于代码解耦合;
  • (2)能够继承其他类,可拓展性更好;
  • (3)更容易共享资源(变量);
继承Thread类的子类的内部变量不会直接共享
  • (4)损耗小;

当要新建一个任务时,如果是继承Thread类的方式,则需要new一个类的对象,但是这样做的话损耗比较大,这样我们每次都需要去新建一个线程,执行完之后还需要去销毁。但是如果我们采用实现Runnable接口的方式,传入target,传入实现Runnable接口的类的实例的方法,这样我们就可以反复地利用同一个线程。比如,线程池就是这样做的。

总结:

继承Thread类的方式,线程不可复用;

实现Runnable接口的方式,线程可以复用;

继承Thread类方式也有几个小小的好处,但相对于其缺点来说,其优点不值一提:

  • (1)在 run() 方法内部获取当前线程可以直接用 this,而无须用 Thread.currentThread( ) 方法;
  • (2)继承Thread类的子类的内部变量不会直接共享,少数不需要共享变量的场景下使用起来会更加方便;

两种方法同时使用会怎样

public class BothRunnableThread {

   public static void main(String[] args) {
      //使用匿名内部类(两个:一个是Runnable类,一个是Thread类)
      new Thread(new Runnable() {
         //实现Runnable接口的run方法
         @Override
         public void run() {
            System.out.println("来自Runnable接口的实现类的run方法!");
         }
      }){
         //重写父类Thread类的run方法
         @Override
         public void run() {
            System.out.println("来自继承Thread的子类重写之后的run方法");
         }
      }.start();
   }
}
           

输出结果:

来自继承Thread的子类重写之后的run方法
           

因为继承Thread类的子类重写了run方法,调用的时候重写的run方法会覆盖Runnable接口实现类中实现的run方法,所以最终调用的是重写的run方法。