Java实现多线程有几种方式?相信有工作经验的朋友在面试时都遇到过这个问题?但是网上的说法不一;有说两种、有说三种、也有说四种的。那么到底有几种呢?本文我们来讨论一下这个问题。
1.官方说法
对于这个问题网上说法不一,说几种的都有;所以我们看一下Oracle的官文档
如上图所见 Oracle官方在Thread类的描述中提到,实现线程的方式有两种一种是继承Thread、一种是实现Rnnable接口;Oracle的官方文档相信我们Java开发者都是认同的。这应该是属于业界的权威资料。所以这个回答可以说是标准答案了。
那么这两种方式有什么特点呢?在实践中我们应该是使用哪种方式呢?来看看它们的特性
1.2 继承Thread(不推荐)
代码1:继承Thread实现多线程
public class ThreadImpl extends Thread{
@Override
public void run() {
System.out.println("继承Thread实现多线程...");
}
public static void main(String[] args) {
ThreadImpl thread = new ThreadImpl();
thread.start();
}
}
- 优点
- 编码简单
- 缺点
- 扩展性差,Java只支持单继承,继承了Thread就不能继承其他类
1.3 实现Runnable(推荐)
代码2:Runnable实现多线程
public class RunnableImpl {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable接口实现的多线程");
}
}
- 优点
- 线程声明周期和业务逻辑解耦
- 结合线程池可节约系统资源消耗,提高性能
- 拓展性好,实现了Runnable接口还可以实现其他接口或继承其他类。
- 缺点
- 相比于继承Thread编码稍显复杂
综上所述,如无特殊要求;一般情况下推荐实现Runnable,其良好的拓展性和解耦特性;可以适应绝大多数场景。如果你只是写一个小demo或者有特殊要求可以使用继承Thread,否则都是推荐使用实现Runnable。
2.本质区别
以上都是从宏观代码层面上的区别和特性,想要知道这两种方式的最本质的区别还是得去看相关的源码
代码3: java.lang.Thread.java
public class Thread implements Runnable {
//...
private Runnable target;
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//...
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
以上是java.lang.Thread的源码节选,我省略掉了部分内容,仅仅保留了对本文主题有参考价值的部分。
可见Thread也是实现了Runnable中的run()方法,而Thread提供的有参构造Thread(Runnable target)恰恰就是上文代码2:Runnable实现多线程中使用的方法;该构造传入一个Runnable实现类赋值给Thread中的属性target。
线程启动后执行run()方法,如果target不为空,就执行传进来的target中的run()方法。也就是本文的例子中MyRunnable中的run()方法。
再看上文代码1。
代码1:继承Thread实现多线程
public class ThreadImpl extends Thread{
@Override
public void run() {
System.out.println("继承Thread实现多线程...");
}
public static void main(String[] args) {
ThreadImpl thread = new ThreadImpl();
thread.start();
}
}
ThreadImpl继承Thread类,重写了其中的run()方法。因此Thread中判断target的代码被我们重写的覆盖;当线程启动后执行的代码就是我们重写后的run()方法。
3.实现方式究竟有几种?
回到标题,那么实现多线程的方式究竟有几种呢?如果面试中遇到这个问题,我们可以这么回答:
一般来说有两种;一种是继承Thread一种是实现Runnable。这是Oracle官方文档中的描述。如果你对Java多线程技术了解的比较多,可以说还有线程池等方式可以实现多线程。然后话锋一转:不管是哪种方式,其实都是实现Runnable接口这一种方式,在上文2.本质区别
中;笔者从源码层面简单的分析出Thread类其实也是实现了Runnable。所以上文说的两种实现方式可以看作都是实现了Runnable,只是实现方式有所不同。事实上包括线程池等实现线程池的方式,其实从本质上也是实现了Runnable接口。
因此在从这个角度讲我们可以说实现多线程的方式本质上只有一种,但是可以有不同的设计或实现思路。
4.两种方式同时使用会发生什么?
public class TogetherThread {
public static void main(String[] args) {
// <A> Runnable 匿名内部类
Thread thread = new Thread(() -> System.out.println("Runnable中的run()")) {
// <B> 重写Thread 中的 run() 方法
@Override
public void run() {
System.out.println("Thread中的run()");
}
};
thread.start();
}
}
以上代码中,<A>
使用匿名内部类实现了Runnable接口传入Thread有参构造器中,然后<B>
又重写了Thread的run()方法。朋友们可以自己先自己想一想运行结果应该是什么?
看到这里有的朋友可能要说了:<A>
处匿名内部类实现了Runnable接口传入Thread有参构造器那Thread中的target变量就不为null了线程启动后就直接执行target中的run()方法了也就是匿名内部类中的run()方法。ok,先让我们看看答案:
答案显而易见,这样想的朋友猜错了。
这个问题我们应该从从面向对象的角度来分析,虽然我们在<A>
处构造了一个匿名内部类传入了target属性,但是<B>
处直接覆盖了Thread中的run()方法。即使传入了target;也无法执行其中的run()方法。
因此线程启动后执行的是<B>
处Thread被重写的方法
5.线程池、FutureTask、Timer、匿名内部类、Lambda表达式
事实上,不管是线程池、Callable、FutureTask还是定时器Timer;本质上都是对Thread或者Runnable做的包装。底层的源码实现还是Thread和Runnable
而匿名内部类和Lambda表达式都只是语法的另一种表现形式本质上还是实现了Runnable
本人能力有限,本文只是我个人的技术分享。如有错误或不实之处;恳请大家不吝赐教。可以在公众号留言讨论;本人定虚心学习。