1.为何说只有1种实现线程的方法?

135 阅读7分钟

1.为何说只有1种实现线程的方法?

  1. 常见4种

    1. Runnable

    2. Thread

    3. Callable

      Callable 和与之相关的 Future、FutureTask,它们可以把线程执行的结果作为返回值返回,如代码所 示,实现了 Callable 接口,并且给它的泛型设置成 Integer,然后它会返回一个随机数。 但是,无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行 的,而不是说它们本身就是线程。它们可以放到线程池中执行,如代码所示, submit() 方法把任务放 到线程池中,并由线程池创建线程,不管用什么方法,最终都是靠线程来执行的,而子线程的创建方式 仍脱离不了最开始讲的两种基本方式,也就是实现 Runnable 接口和继承 Thread 类。

    4. 线程池创建线程:通过线程池创建线程。线程池确 实实现了多线程,比如我们给线程池的线程数量设置成 10,那么就会有 10 个子线程来为我们工作

    对于线程池而言,本质上是通过线程工厂创建线程的,默认采用 DefaultThreadFactory ,它会给线程 池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。但是无论 怎么设置这些属性,最终它还是通过 new Thread() 创建线程的 ,只不过这里的构造函数传入的参数要 多一些,由此可以看出通过线程池创建线程并没有脱离最开始的那两种基本的创建方式,因为本质上还 是通过 new Thread() 实现的。 在面试中,如果你只是知道这种方式可以创建线程但不了解其背后的实现原理,就会在面试的过程中举 步维艰,想更好的表现自己却给自己挖了 “坑”。 所以我们在回答线程实现的问题时,描述完前两种方式,可以进一步引申说 “我还知道线程池和 Callable 也是可以创建线程的,但是它们本质上也是通过前两种基本方式实现的线程创建。” 这样的回 答会成为面试中的加分项。然后面试官大概率会追问线程池的构成及原理,这部分内容会在后面的课时 中详细分析。

  2. 其他创建方式

    1. 定时器Timer

      1. class TimerThread extends Thread {
        }
        

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

      2. 其他方法

        *描述:匿名内部类创建线程
        */
        new Thread(new Runnable() {
            @Override
            public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
                }).start();
              }
        }
        
      3. 或许你还会说,我还知道一些其他方式,比如匿名内部类或 lambda 表达式方式,实际上,匿名内部类 或 lambda 表达式创建线程,它们仅仅是在语法层面上实现了线程,并不能把它归结于实现多线程的方 式,如匿名内部类实现线程的代码所示,它仅仅是用一个匿名内部类把需要传入的 Runnable 给实例出 来。

        new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
        

        我们再来看下 lambda 表达式方式。如代码所示,最终它们依然符合最开始所说的那两种实现线程的方 式。

  3. 实现线程只有一种方式

    1. 关于这个问题,我们先不聚焦为什么说创建线程只有一种方式,先认为有两种创建线程的方式,而其他 的创建方式,比如线程池或是定时器,它们仅仅是在 new Thread() 外做了一层封装,如果我们把这些 都叫作一种新的方式,那么创建线程的方式便会千变万化、层出不穷,比如 JDK 更新了,它可能会多出 几个类,会把 new Thread() 重新封装,表面上看又会是一种新的实现线程的方式,透过现象看本质, 打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的。 接下来,我们进行更深层次的探讨,为什么说这两种方式本质上是一种呢?

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

      首先,启动线程需要调用 start() 方法,而 start() 方法最终还会调用 run() 方法,我们先来看看第一种方 式中 run() 方法究竟是怎么实现的,可以看出 run() 方法的代码非常短小精悍,第 1 行代码 if (target != null) ,判断 target 是否等于 null,如果不等于 null,就执行第 2 行代码 target.run(),而 target 实 际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给 Thread 类的对象。

    3. 然后,我们来看第二种方式,也就是继承 Thread 方式,实际上,继承 Thread 类之后,会把上述的 run() 方法重写,重写后 run() 方法里直接就是所需要执行的任务,但它最终还是需要调用 thread.start() 方法来启动线程,而 start() 方法最终也会调用这个已经被重写的 run() 方法来执行它的 任务,这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创 建线程的唯一方式。 我们上面已经了解了两种创建线程方式本质上是一样的,它们的不同点仅仅在于实现线程运行内容的不 同,那么运行内容来自于哪里呢?

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

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

下面我们来对刚才说的两种实现线程内容的方式进行对比,也就是为什么说实现 Runnable 接口比继承 Thread 类实现线程要好?好在哪里呢?

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

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

第三点好处在于 Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法 再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做 到了,相当于限制了代码未来的可拓展性。 综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。