【Java面试题】线程创建的三种方式及区别?

三种线程创建方式

  1. 继承Thread类,子类重写run()方法,调用子类的strat()启动线程。
  2. 实现Runnable接口,实现run()方法,调用对象start()启动线程。
  3. 实现Callable接口,实现call()方法,用FutureTask()封装实现类。使用FutureTask对象作为Thread对象调用start()启动线程,调用FutureTask对象的get()方法获取返回值()。

三种方式的优缺点 

采用继承Thread类方式:

  • 优点:编写简单。
  • 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

采用实现Runnable接口方式:

  • 优点:线程类只是实现了Runable接口,还可以继承其他的类。
  • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

Runnable和Callable的区别:

  • Callable规定的方法是call(),Runnable规定的方法是run()。
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值得。
  • Call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

总结:Runnable和Callable功能一样的,都是构造线程执行的任务;其区别可以简单理解为有无返回值的区别,通常Callable使用的比较多

继承Thread类

继承Thread类的话,必须重写run方法,在run方法中定义需要执行的任务。

class MyThread extends Thread{
      private static int num = 0;
      
     public MyThread(){
         num++;
      }
       
      @Override
      public void run() {
         System.out.println("主动创建的第"+num+"个线程");
     }
 }

创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

public class Test {
      public static void main(String[] args)  {
          MyThread thread = new MyThread();
          thread.start();
      }
  }
   
   
  class MyThread extends Thread{
     private static int num = 0;
      
     public MyThread(){
         num++;
     }
      
     @Override
     public void run() {
         System.out.println("主动创建的第"+num+"个线程");
     }
 }

在上面代码中,通过调用start()方法,就会创建一个新的线程了。为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:

public class Test {
     public static void main(String[] args)  {
         System.out.println("主线程ID:"+Thread.currentThread().getId());
         MyThread thread1 = new MyThread("thread1");
         thread1.start();
        MyThread thread2 = new MyThread("thread2");
         thread2.run();
     }
 }
 
  
 class MyThread extends Thread{
     private String name;
      
    public MyThread(String name){
         this.name = name;
     }
     
     @Override
    public void run() {
        System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
    }
 }

运行结果:

主线程ID:1
name:thread2 子线程ID:1
name:thread1 子线程ID:8

从输出结果可以得出以下结论:

1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;

2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。

2.实现Runnable接口 

  1. 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。 
  2. 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。 
  3. 调用线程对象的start()方法来启动该线程。
package Thread;

import java.util.concurrent.*;
//测试类
public class TestThread {
    public static void main(String[] args) throws Exception {
         testImplents();
    }

    public static void testImplents() throws Exception {
        MyThreadImplements myThreadImplements = new MyThreadImplements();
        Thread t1 = new Thread(myThreadImplements);
        Thread t2 = new Thread(myThreadImplements, "my thread -2");
        t1.start();
        t2.start();
    }
}
//线程类
class MyThreadImplements implements Runnable {
    @Override
    public void run() {
        System.out.println("通过实现Runable,线程号:" + Thread.currentThread().getName());
    }
}

3. 使用Callable接口

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

创建并启动有返回值的线程的步骤如下:

  1. 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

下面是一个例子:

public class Main {

   public static void main(String[] args){
 
    MyThread3 th=new MyThread3();

    //使用Lambda表达式创建Callable对象
 
      //使用FutureTask类来包装Callable对象

   FutureTask future=new FutureTask(
 
    (Callable)()->{
 
       return 5;

     }

     );

   new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程
 
     try{

    System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回
     }catch(Exception e){

    ex.printStackTrace();

   }
 
   }
 
 }

start()和run()的区别?

(具体区别可以看【Java面试题】线程中start方法和run方法的区别?

  • start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
  • run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

你可能感兴趣的:(面试题库,多线程,java,jvm,开发语言)