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

【 2 种线程实现方式的源码】

实现 Runnable 接口

public class RunnableThread implements Runnable {

   

    @Override

    public void run() {

        System.out.println('用实现Runnable接口实现线程');

    }

}

第 1 种方式是通过实现 Runnable 接口实现多线程

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

   

继承 Thread 类

public class ExtendsThread extends Thread {

   

    @Override

    public void run() {

        System.out.println('Thread类实现线程');

    }

}

2 种方式是继承 Thread

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

相信上面这两种方式你一定非常熟悉,并且经常在工作中使用它们。

   

线程池创建线程

线程池确实实现了多线程,比如我们给线程池的线程数量设置成 10,那么就会有 10 个子线程来为我们工作,接下来,我们深入解析线程池中的源码,来看看线程池是怎么实现线程的?

static class DefaultThreadFactory implements ThreadFactory { 

    DefaultThreadFactory() {

        SecurityManager s = System.getSecurityManager();

        group = (!= 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;

    }

}

   

对于线程池而言,本质上是通过线程工厂创建线程的。

默认采用 DefaultThreadFactory ,它会给线程池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。

但是无论怎么设置这些属性,最终它还是通过 new Thread() 创建线程的 ,只不过这里的构造函数传入的参数要多一些,由此可以看出通过线程池创建线程并没有脱离最开始的那两种基本的创建方式,因为本质上还是通过 new Thread() 实现的。

   

有返回值的 Callable 创建线程

class CallableTask implements Callable<Integer> {

   

    @Override

    public Integer call() throws Exception {

        return new Random().nextInt();

    }

}

   

//创建线程池

ExecutorService service = Executors.newFixedThreadPool(10);

//提交任务,并用 Future提交返回结果

Future<Integer> future = service.submit(new CallableTask());

   

4 种线程创建方式是通过有返回值的 Callable 创建线程

Runnable 创建线程是无返回值的,而 Callable 和与之相关的 FutureFutureTask,它们可以把线程执行的结果作为返回值返回,如代码所示,实现了 Callable 接口,并且给它的泛型设置成 Integer,然后它会返回一个随机数。

   

但是,无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行的而不是说它们本身就是线程

它们可以放到线程池中执行,如代码所示, submit() 方法把任务放到线程池中,并由线程池创建线程,不管用什么方法,最终都是靠线程来执行的,而子线程的创建方式仍脱离不了最开始讲的两种基本方式,也就是实现 Runnable 接口和继承 Thread

   

【其他创建方式】

讲到这里你可能会说,我还知道一些其他的实现线程的方式。

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

   

定时器 Timer

class TimerThread extends Thread {

//具体实现

}

   

或许你还会说,我还知道一些其他方式,比如匿名内部类或 lambda 表达式方式

实际上,匿名内部类或 lambda 表达式创建线程,它们仅仅是在语法层面上实现了线程,并不能把它归结于实现多线程的方式,如匿名内部类实现线程的代码所示,它仅仅是用一个匿名内部类把需要传入的 Runnable 给实例出来。

/**

 *描述:匿名内部类创建线程

 */

new Thread(new Runnable() {

    @Override

    public void run() {

        System.out.println(Thread.currentThread().getName());

    }

}).start();

   

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

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

}

   

【实现线程只有一种方式】

关于这个问题,我们先认为有两种创建线程的方式,而其他的创建方式,打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的

   

为什么说这两种方式本质上是一种呢?

@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类的对象

   

然后,我们来看第二种方式,也就是继承 Thread 方式.

实际上,继承 Thread 类之后,会把上述的 run() 方法重写,重写后 run() 方法里直接就是所需要执行的任务,但它最终还是需要调用 thread.start() 方法来启动线程,而 start() 方法最终也会调用这个已经被重写的 run() 方法来执行它的任务,这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread ,这是创建线程的唯一方式。

   

两种创建线程方式本质上是一样的,它们的不同点仅仅在于实现线程运行内容的不同

   

运行内容主要来自于两个地方,要么来自于 target,要么来自于重写的 run() 方法。

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

   

在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。

   

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

首先,我们从代码的架构考虑。

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

   

第二点就是在某些情况下可以提高性能

使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多。

如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。

   

第三点好处在于 Java 语言不支持双继承。

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

   

posted @ 2020-06-30 16:07  流年的夏天  阅读(368)  评论(1编辑  收藏  举报