在程序开发中,并发编程是所有程序员必须掌握的技能,而多线程是并发编程中基础中的基础,要想进一步的写出高性能的Java程序,必须要先实现多线程,才可以继续后续的一系列开发,所以我们本节从并发编程的基础如何实现线程开始讲起。
尽管线程的实现很基础,看似很简单,但实际上却暗藏玄机。因为在我们的开发中,不只是仅仅会实现线程就万事大吉,我们还要考虑以下问题:
- 实现线程是否对后续的开发有扩展的支持。
- 实现线程的开销是否大于收益。
- 如何合理的选出一种实现方式。
要想解答以上问题,我们需要先看以下两个问题:
- 实现线程的方式到底有几种?
- 线程实现的本质是什么?为什么说线程只有一种实现?
实现线程的方式到底有几种?
大部分人会说有 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的父类
可以看出,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 类实现线程要好
原因
- 从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。
- 在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少。
- Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。
至于开篇提的几个问题,Runnable方式解耦且易于扩展,如果需要频繁创建线程的话,并且想控制资源的大小可以用线程池的方式,如果想获取线程中的返回值可以用Callable的方式等。
其实讨论完这些后线程的创建方式后怎么使用合理,相信你心中自有答案了。
// TODO Coding …