【并发那些事 】创建线程的三种方式

创建线程可以说是并发知识中最基础的操作了,JDK 提供的创建线程的方式,如果不包括通过线程池的话,目前有三种形式,它们分别是通过继承 Thread 类,通过实现 Runable 接口,通过 FutureTask。如下图所示

下面整理了一下 3 种方法的具体使用与异同。

创建线程的 3 种方法

1. 继承 Thread

  1. 创建一个类继承 Thread 并覆盖 run 方法
class MyThread extends Thread {
    @Override
    public void run() {
        String threadName = getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("线程[" + threadName + "]运行开始,i = " + i + " time = " + new Date());
        }
    }
}
  1. 创建并启动线程
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();

        myThread1.start();
        myThread2.start();

        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("线程[" + threadName + "]运行开始,i = " + i + " time = " + new Date());
        }

整体流程如下:

这里步骤比较简单和清晰

  1. 创建一个类,这类要继承 Thread
  2. 覆盖 Thread 的 run 方法,并在此方法中实现你的多线程任务
  3. 创建这个类的实例
  4. 调用它的 start() 方法(这里要注意,新手容易直接调用 run 方法,那样只是普通调用,而不多线程调用)

2. 实现 Runable

  1. 创建一个类实现 Runable 接口,并覆盖 run 方法
class MyRunable implements Runnable {
    
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("线程[" + threadName + "]运行开始,i = " + i + " time = " + new Date());
        }
    }
}
  1. 创建类的实现,并将它传给 Thread 的构造函数来创建 Thread 
MyRunable myRunable = new MyRunable();

new Thread(myRunable).start();
new Thread(myRunable).start();

String threadName = Thread.currentThread().getName();
for (int i = 0; i < 20; i++) {
    System.out.println("线程[" + threadName + "]运行开始,i = " + i + " time = " + new Date());
}

整体流程如下:

具体步骤如下:

  1. 创建一个实现了 Runable 接口的类
  2. 覆盖 run 方法,并在此方法中实现你的多线程任务
  3. 创建 Runable 接口实现类的实例
  4. 通过把上步得到的 Runable 接口实现类的实例,传给  Thread 的有参构造函数来创建 Thread 的实例
  5. 调用上步得来的 Thread 实例的 start() 方法(这里要注意,新手容易直接调用 run 方法,那样只是普通调用,而不多线程调用)

3. 通过 FutureTask

  1. 创建一个实现了 Callable 接口的类,并实现call 方法 (T 代表你想要的返回值类型)
class MyCallerTask implements Callable {
    
    @Override
    public String call() throws Exception {
        System.out.println("执行任务开始");
        Thread.sleep(3000);
        System.out.println("执行任务结束");
        return "hello";
    }
}
  1. 创建并启动线程
        // 创建异步任务
        FutureTask futureTask = new FutureTask<>(new MyCallerTask());
        // 启动线程
        new Thread(futureTask).start();
        System.out.println("其它操作");
        try {
            // 等待任务执行完,并获得任务执行完的结果
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

整体流程如下:

具体步骤如下:

  1. 创建一个实现了 Callable 接口的类,这里 T 的类型就是你线程任务想要返回的类型
  2. 覆盖 call 方法,并在此方法中实现你的多线程任务
  3. 创建 Callable 接口实现类的实例
  4. 通过把上步得到的 Callable 接口实现类的实例,传给  FutureTask 的有参构造函数来创建 FutureTask 的实例
  5. 通过把上步得到的 FutureTask 实例,传给  Thread 的有参构造函数来创建 Thread 的实例
  6. 调用上步得来的 Thread 实例的 start() 方法(这里要注意,新手容易直接调用 run 方法,那样只是普通调用,而不多线程调用)
  7. 如果你还想获得返回值,需要再调用 FutureTask 实例的 get() 方法(这里要注意,get()会阻塞线程)

3种方法的优缺点

通过上述的演示代码,可以看出这 3 种方法,其实各有优缺点

复杂程度

通过代码量与逻辑可以明显感觉出来,第一种直接继承 Thread 最方便,并且其它两种到最后,还是要依赖创建 Thread 才能实现。所以从方便及难易程度来看,可以得到如下结论:

可扩展性

通过演示代码可以看出,只有第一种是通过继承,其它两种是通过实现接口的形式。我们都知道 JAVA 是不允许多继承,但是可以多实现。所以如果使用了第一种方法,就无法再继承别的类了。另外第一种把线程与线程任务冗余在了一起,不利于后期的维护。所以可以得到如下结论:

是否有返回值

从代码中可以很容易看出,只有通过 FutureTask 的方式才有返回值,另外两种均没有,所以得出如下结论

该用哪种方式创建线程

如果要用到返回值,那不用想,肯定只能使用 FutureTask 的方法。如果对于返回值没有要求,那Thread 与  Runable 均可,不过,考虑到可扩展性,最好使用 Runable 的形式。不过,话说回来,如果在真正项目中使用,综合考虑,一般还是最推荐通过线程池去创建。

你可能感兴趣的:(java)