01 创建线程的方式到底有多少种


I’m Captain Jack Sparrow. The original. The only!                               --《加勒比海盗》

img

       实现线程是并发编程的过程中基础中的基础,因为只有先实现多线程,才能够进行后续一系列的基于线程的种种操作。实现线程的方式看似简单,实则暗藏玄机。在此抛出一个最最简单的问题,实现线程的方式到底有多少种呢?2种?3种?还是更多?正确的答案可能会让很多老手也虎躯一震,本质上Java种实现线程的方式仅有1种,仅此一家,如假包换。

1. 常见的创建线程方式

       先来说小盆友都知道的2种方式:

1.1 实现Runnable接口

       第1种常用的实现方式为通过实现Runnable接口实现多线程,如下代码所示,类Thread01通过实现Runnable接口,覆写run()方法。之后只需将Thread01的实例对象作为参数传给Thread类就可实现多线程了。

public class Thread01 implements Runnable {
    @Override
    public void run() {
        System.out.println('实现Runnable接口');
    }
}

1.2 继承 Thread 类

       第 2 种方式是继承 Thread 类,如下代码所示,类Thread02继承 Thread 类,并重写了其 run() 方法。

public class Thread02 extends Thread {
    @Override
    public void run() {
        System.out.println('继承Thread类');
    }
}

       接下来再看看其他的一些创建线程的方式。

1.3 线程池创建线程

       线程池确实实现了多线程,比如将线程池的线程数量设置成 20,则会有 20 个子线程可供使用,深入解析线程池中的源码,来看看线程池是怎么实现线程的?

static class DefaultThreadFactory implements ThreadFactory {
    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;
    }
}

       可以看出线程池是通过线程工厂创建线程的,默认采用 DefaultThreadFactory ,它会给线程池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。但是无论怎么设置这些属性,最终它还是通过 new Thread() 创建线程的 ,只不过这里的构造函数传入的参数要多一些。

1.4 有返回值的 Callable 创建线程

       Runnable 创建线程是无返回值的,如果需要获取线程的返回值,则可使用 Callable 或 Future、FutureTask,它们可以把线程执行的结果作为返回值返回,如代码所示,实现了 Callable 接口,并且给它的泛型设置成 Integer,然后它会返回一个随机数。

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());

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

2. 其他创建方式

2.1 定时器 Timer

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

class TimerThread extends Thread {
//具体实现
}

2.2 其他方法

       其他方式,比如匿名内部类或 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();

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

       在论述实现线程的方式仅有一种之前,先探讨一下如上所列出的各种线程实现方式。可以看出线程池或是定时器,仅是对 new Thread() 做了一些封装,如果我们把所有这种仅仅是做了封装都称之为一种实现方式,那线程的实现方式将会无穷无尽了。透过现象看本质,打开封装后,会发现这种种方式底层都是基于 Runnable 接口或继承 Thread 类实现的。

       接下来,进行更深层次的探讨,为什么说这两种方式本质上是一种呢?

       其实很简单,启动线程的方法的入口只有一个就是调用Thread类的start()方法,在执行start()方法后,会调用一个start0()的native方法,最终是由这个native方法再去触发调用run()方法,完成任务。这个run()方法是Runnable接口中定义的唯一的方法,Thread类也是实现了Runnable接口并覆写了该方法,覆写方式如下:

@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() 方法再进行重写,重写后 run() 方法里直接就是所需要执行的任务,但它最终还是需要调用 thread.start() 方法来启动线程,而 start() 方法最终也会调用这个已经被重写的 run() 方法来执行它的任务。

       至此可以彻底看清楚了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。实现Runnable接口和继承Thread类2种创建线程方式本质上是一样的,不同点在于**实现线程运行内容的不同,**要么来自于 target,要么来自于重写的 run() 方法。

       总结一下:本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行。在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。

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

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

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

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

       最后, Java 语言不支持多继承,如果我们的类一旦继承了 Thread 类,后续就不能再继承其他的类,如果未来这个类需要继承其他类实现一些功能上的拓展则不能做到,限制了代码未来的可拓展性。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

见贤不思齐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值