为什么说本质上只有一种实现线程的方式?

为什么说本质上只有一种实现线程的方式?

前言

本章主要讨论两个议题

  • 为什么说本质上只有一种实现线程的方式?
  • 实现 Runnable 接口究竟比继承 Thread 类实现线程好在哪里?

项目环境

1.常见的几种线程实现方式

很多人可能会说实现 Runnable 接口、继承 Thread 类,使用线程池创建或者实现 Callable 接口等等,随随便便也可以说出 3、4 种,怎么会是一种呢?下面我们来看看这些实现方式具体是什么?

1.1 实现 Runnable 接口

static 不是必须的,这里只是为了调用测试方便

    static class RunnableThread implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "->第一种:实现 Runnable 接口实现线程");
        }

    }

调用方法:

    public static void main(String[] args) {
        // 第一种 实现 {@link Runnable} 接口
        new Thread(new RunnableThread()).start();
    }

执行结果:

Thread-0->第一种:实现 Runnable 接口实现线程

第 1 种方式是通过实现 Runnable 接口实现多线程,如代码所示,首先通过 RunnableThread 类实现 Runnable 接口,然后重写 run() 方法,之后只需要把这个实现了 run() 方法的实例传到 Thread 类中就可以实现多线程。

1.2 继承 Thread 类

    static class ExtendsThread extends Thread {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "->第二种:继承 Thread 类型实现线程");
        }

    }

调用方法:

    public static void main(String[] args) {
        // 第二种 继承 {@link Thread}
        new ExtendsThread().start();
    }

执行结果:

Thread-1->第二种:继承 Thread 类型实现线程

第 2 种方式是继承 Thread 类,如代码所示,与第 1 种方式不同的是它没有实现接口,而是继承 Thread 类,并重写 run() 方法。

1.3 使用线程池创建

    private static void threadPoolDemo() {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        executorService.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "->第三种:线程池创建");
        });
    }

执行结果:

pool-1-thread-1->第三种:线程池创建

线程池也是经典的多线程实现,比如示例代码中我们给线程池的线程数量设置成 3,那么就会有 3 个子线程来为我们工作,接下来,我们来看看线程池是怎么实现线程的?

    /**
     * The default thread factory
     */
    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-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;
        }
    }

对于线程池而言,本质上是通过线程工厂(ThreadFactory)创建线程的,默认采用 DefaultThreadFactory ,它会给线程池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。但是无论怎么设置这些属性,最终它还是通过 new Thread() 创建线程的 ,只不过这里的构造函数传入的参数要多一些,由此可以看出通过线程池创建线程并没有脱离最开始的那两种基本的创建方式,因为本质上还是通过 new Thread() 实现的。

1.4 有返回值的 Callable 创建线程

    private static void callableDemo() {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Future<String> future = executorService.submit(new CallableTask());
        try {
            System.out.println(future.get(2, TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class CallableTask implements Callable<String> {

        @Override
        public String call() throws Exception {
            return Thread.currentThread().getName() + "->第四种:实现 Callable 接口";
        }
    }

执行结果:

pool-2-thread-1->第四种:实现 Callable 接口

第 4 种线程创建方式是通过有返回值的 Callable 创建线程,Runnable 创建线程是无返回值的,而 Callable 和与之相关的 Future、FutureTask,它们可以把线程执行的结果作为返回值返回,如代码所示,实现了 Callable 接口。

但是,无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行的,而不是说它们本身就是线程。它们可以放到线程池中执行,如代码所示, submit() 方法把任务放到线程池中,并由线程池创建线程,最终都是靠线程来执行的,而子线程的创建方式仍脱离不了最开始讲的两种基本方式,也就是实现 Runnable 接口和继承 Thread 类。

1.5 其他方式创建线程

Timer

比如,定时器也可以实现线程,如果新建一个 Timer,令其每隔 10 秒或设置两个小时之后,执行一些任务,那么这时它确实也创建了线程并执行了任务,但如果我们深入分析定时器的源码会发现,本质上它还是会有一个继承自 Thread 类的 TimerThread,所以定时器创建线程最后又绕回到最开始说的两种方式(实现 Runnable 接口和继承 Thread 类)。

匿名内部类

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类");
            }
        }).start();

lambda

        new Thread(() -> {
            System.out.println("lambda 语法");
        }).start();

匿名内部类或 lambda 表达式创建线程,它们仅仅是在语法层面上实现了线程,并不能把它归结于实现多线程的方式。

2.为何说实现线程只有一种方式?

以上提到的这几种其实本质上都符合最开始所说的那两种实现线程的方式(实现 Runnable 接口和继承 Thread 类),所以我们只需要搞懂实现 Runnable 接口和继承 Thread 类这两种实现线程方式的本质是否一样就可以了。

首先我们看 Thread#start 方法,start 方法中会调用 start0(); 的方法,这是一个 native 修饰的方法。这一块涉及到 openjdk 源代码的具体实现,大致的实现就是 Java 线程调用 start->start0 的方法,实际上会调用到 JVM_StartThread 方法,而 JVM_StartThread 最终调用的是 Java 线程的 run 方法。我们只需要知道 Thread#start 方法最终还是会调用 run 方法就可以了。

Thread#run 方法源码如下:

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

第 1 行代码 if (target != null) ,判断 target 是否等于 null,如果不等于 null,就执行第 2 行代码 target.run(),而 target 实际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给Thread类的对象。

我们再看第二种方式,即继承 Thread 类的方式,继承 Thread 类之后需要重写 run 方法,而 start 方法最终还是会调用这个被重写之后的 run 方法。

综上所述,本质上创建线程只有一种方式,就是 构造一个 Thread 类,这是创建线程的唯一方式。

而实现 Runnable 接口和继承 Thread 类这两种方式的不同点在于实现 run 方法的方式不同,或者说 实现线程运行内容的不同

运行内容主要来自于两个地方,要么来自于 target(Runnable 方式),要么来自于重写的 run() 方法(Thread方式),在此基础上进行拓展,可以这样描述:本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行。

3.实现 Runnable 接口比继承 Thread 类实现线程要好

为什么说实现 Runnable 接口比继承 Thread 类实现线程要好?好在哪里呢?

  • 首先,我们从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。

  • 第二点就是在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。这一点可以参考《线程池的线程复用原理》

  • 第三点在于 Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。

综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。

4.参考

  • 《Java 并发编程 78 讲》- 徐隆曦
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章