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

TodoCoder大约 8 分钟Java并发Java并发

        线程实现有百种千种,我独终你那一种。                       -- 王霸炖鸡

前言

  在程序开发中,并发编程是所有程序员必须掌握的技能,而多线程是并发编程中基础中的基础,要想进一步的写出高性能的Java程序,必须要先实现多线程,才可以继续后续的一系列开发,所以今天我们来从并发编程的基础如何实现线程开始聊起。

  尽管线程的实现很基础,看似很简单,但实际上却暗藏玄机。因为在我们的开发中,不只是仅仅会实现线程就万事大吉,我们还要考虑以下问题:

  1. 实现线程是否对后续的开发有扩展的支持。
  2. 实现线程的开销是否大于收益。
  3. 如何合理的选出一种实现方式。

要想解答以上问题,我们需要先看以下两个问题:

  1. 实现线程的方式到底有几种?
  2. 线程实现的本质是什么?为什么说线程只有一种实现?

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

大部分人会说有 2 种、3 种或是 4 种,少有人说有 1 种。我们先看大家熟知的2种方式

1. 实现 Runnable 接口

public class ThreadImplement {
​
    static class RunnableThread implements Runnable {
        @Override
        public void run() {
            System.out.println("实现Runnable接口实现线程");
        }
    }
    public static void main(String[] args) {
        RunnableThread runnableThread = new RunnableThread();
        new Thread(runnableThread).start();
    }
}

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

2. 继承 Thread 类

public class ThreadImplement {
​
    static class ThreadExtends extends Thread {
        @Override
        public void run() {
            System.out.println("继承 Thread 类实现线程");
        }
    }
    public static void main(String[] args) {
        new ThreadExtends().start();
    }
}

第 2 种方式是通过继承Thread类,如代码所示,ThreadExtends继承Thread类,并重写run()方法来实现线程功能。

3. 线程池创建线程

  有了前两种基本的创建方式,那么为什么说还有第 3 种或第 4 种方式呢?我们先来看看第3种线程池方式的实现。

public class ThreadImplement {
​
    private static final ExecutorService executorService = new ThreadPoolExecutor(
            10,10,60L, TimeUnit.SECONDS, 
            new LinkedBlockingQueue(), 
            Executors.defaultThreadFactory());
​
    public static void main(String[] args) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程池创建线程");
            }
        });
    }
}

这里创建了一个核心线程数为10,最大线程数也为10的线程池,给线程池传入一个Runnable实例,以此来运行。最后一个参数为 DefaultThreadFactory实例,这是个默认的线程工厂实例。其实线程池实现本质上是通过线程工厂创建的Thread实例来运行 Runnable实例来实现的,只不过这里线程池参数会多点。

在面试中,如果不了解线程池实现原理,答出这个就会给自己挖了“坑”。

所以我们在回答线程实现的问题时,描述完前两种方式,可以进一步引申说“我还知道线程池和Callable 也是可以创建线程的,但是它们本质上也是通过前两种基本方式实现的线程创建。”这样的回答会成为面试中的加分项。然后面试官大概率会追问线程池的构成及原理,这部分内容会在后面的文章中详细分析。

4. Callable 创建线程

public class ThreadImplement {
  
    static class CallableThread implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "Callable 创建线程";
        }
    }
    public static void main(String[] args) throws Exception {
        CallableThread callable = new CallableThread();
        FutureTask<String> stringFutureTask = new FutureTask<>(callable);
        new Thread(stringFutureTask).start();
        String r = stringFutureTask.get();
        System.out.println(r);
    }
}

通过有返回值的 Callable 创建线程,Runnable 创建线程是无返回值的,而 Callable 和与之相关的 Future、FutureTask,它们可以把线程执行的结果作为返回值返回,如代码所示,实现了 Callable 接口,然后把 callable包装成一个FutureTask,通过Thread 来启动线程,并获取结果。

无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行的,而不是说它们本身就是线程。它们可以传到Thread中,也可以在线程池中执行。

我们看一下FutureTask的父类

011.png 可以看出,FutureTask也是实现的Runnable接口,Thread.start 启动线程的时候,触发的是Runnable的run()方法,进而触发Callable的call()方法。这里涉及到一个设计模式,适配器模式,FutureTask中的 RunnableAdapter类把 Callable和Runnable进行适配来达到 Runnable.run()调用Callable.call()的功能。详细内容我们这里先不讨论。

其实第3、4种方式本质也是实现Runnable接口。

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

  经过上面的讨论,我们可以得知,第2、3、4种的本质都是通过实现Runnable接口来触发run()方法来实现多线程功能的。那Thread和Runnable两种的本质是什么呢?

我们知道这两种方式都是通过Thread.start()方法来启动线程的。我们看一下Thread源码。

public class Thread implements Runnable {
    private Runnable target;
    private Thread(Runnable target) {
        //这里做了逻辑简化
        this.target = target;
    }
    private Thread() {
        //这里做了逻辑简化
        this.target = null;
    }
    ...
    /**
    * 使线程开始执行;Java虚拟机调用该线程的run方法。 
    */
    public synchronized void start() {
            ...
            try {
                start0();
            } finally {
                ...
            }
        }
     private native void start0();
​
     @Override
     public void run() {
         if (target != null) {
            target.run();
         }
     }
     ...
 )

说明:start0()方法是 native方法,触发start0()方法后底层会启动线程并在线程中调用 run()方法。

这个是简化过的代码,但主要逻辑都在,先看第1种方式:

RunnableThread runnableThread = new RunnableThread();
new Thread(runnableThread).start();

这个调用start()方法的时候,target != null ,调用的是 RunnableThread中的run()方法。

第2种方式:

public class ThreadImplement {
​
    static class ThreadExtends extends Thread {
        @Override
        public void run() {
            System.out.println("继承 Thread 类实现线程");
        }
    }
    public static void main(String[] args) {
        new ThreadExtends().start();
    }
}

这个调用start()方法的时候,由于Thread 中的run()方法被 ThreadExtends 重写,其实调用的是 ThreadExtends中的run()方法。

这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。只不过运行的内容来自两个地方。所以实现线程的形式有很多种,本质上只有一种。

那么这两种哪种实现比较好呢?为什么?

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

原因

  1. 从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。
  2. 在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少。
  3. Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。

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

最后

  至于开篇提的几个问题,Runnable方式解耦且易于扩展,如果需要频繁创建线程的话,并且想控制资源的大小可以用线程池的方式,如果想获取线程中的返回值可以用Callable的方式等。

其实讨论完这些后线程的创建方式后怎么使用合理,相信你心中自有答案了。

感谢各位看到这里,如果觉得可以,记得收藏一下哦,感谢!!

// TODO Coding ...