实现线程的方式到底有几种?

这篇文章主要讲解实现线程的方式到底有几种?以及实现 Runnable 接口究竟比继承 Thread 类实现线程好在哪里?编程

实现线程是并发编程中基础中的基础,由于咱们必需要先实现多线程,才能够继续后续的一系列操做。因此本文就先从并发编程的基础如何实现线程开始讲起。多线程

实现线程的方式到底有几种?咱们接下来看看它们具体指什么?架构

实现 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() 方法。相信上面这两种方式你必定很是熟悉,而且常常在工做中使用它们。dom

线程池建立线程

那么为何说还有第 3 种或第 4 种方式呢?咱们先来看看第 3 种方式:经过线程池建立线程。线程池确实实现了多线程,好比咱们给线程池的线程数量设置成 10,那么就会有 10 个子线程来为咱们工做,接下来,咱们深刻解析线程池中的源码,来看看线程池是怎么实现线程的?ide

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;
    }
}

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

除此以外,Callable 也是能够建立线程的,可是本质上也是经过前两种基本方式实现的线程建立。oop

有返回值的 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 和与之相关的 Future、FutureTask,它们能够把线程执行的结果做为返回值返回,如代码所示,实现了 Callable 接口,而且给它的泛型设置成 Integer,而后它会返回一个随机数。性能

可是,不管是 Callable 仍是 FutureTask,它们首先和 Runnable 同样,都是一个任务,是须要被执行的,而不是说它们自己就是线程。它们能够放到线程池中执行,如代码所示, submit() 方法把任务放到线程池中,并由线程池建立线程,无论用什么方法,最终都是靠线程来执行的,而子线程的建立方式仍脱离不了最开始讲的两种基本方式,也就是实现 Runnable 接口和继承 Thread 类。学习

除了上述经常使用的实现线程的方式还有如下方式:

定时器 Timer

class TimerThread extends Thread {

    boolean newTasksMayBeScheduled = true;

    private TaskQueue queue;

    TimerThread(TaskQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            mainLoop();
        } finally {
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();
            }
        }
    }

    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break;

                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) {
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else {
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired)
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }
}

定时器也能够实现线程,若是新建一个 Timer,令其每隔 10 秒或设置两个小时以后,执行一些任务,那么这时它确实也建立了线程并执行了任务,但若是咱们深刻分析定时器的源码会发现,本质上它仍是会有一个继承自 Thread 类的 TimerThread,因此定时器建立线程最后又绕回到最开始说的两种方式。

匿名内部类建立线程

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}).start();

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

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

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

实现线程只有一种方式

咱们先不认为建立线程只有一种方式,先认为有两种建立线程的方式,而其余的建立方式,好比线程池或是定时器,它们仅仅是在 new Thread() 外作了一层封装,若是咱们把这些都叫做一种新的方式,那么建立线程的方式便会变幻无穷、层出不穷,好比 JDK 更新了,它可能会多出几个类,会把 new Thread() 从新封装,表面上看又会是一种新的实现线程的方式,透过现象看本质,打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的。

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

首先,启动线程须要调用 start() 方法,而 start() 方法最终还会调用 run() 方法,咱们先来看看第一种方式中 run() 方法到底是怎么实现的:

@Override
public void run() {
    if (target != null) {
        target.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 接口比继承 Thread 类实现线程要好?好在哪里呢?

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

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

第三点好处在于 Java 语言不支持双继承,若是咱们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其余的类,这样一来,若是将来这个类须要继承其余类实现一些功能上的拓展,它就没有办法作到了,至关于限制了代码将来的可拓展性。

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

总结

本文主要学习了经过 Runnable 接口和继承 Thread 类等几种方式建立线程,又详细分析了为何说本质上只有一种实现线程的方式,以及实现 Runnable 接口究竟比继承 Thread 类实现线程好在哪里?看完本文相信你必定对建立线程有了更深刻的理解。

相关文章
相关标签/搜索