一个问题
哈喽,我是狗哥。话不多说,金三银四,很多同学马上就要参加春招了。而多线程肯定是面试必问的,开篇之前,问大家一个问题:创建线程到底有几种方式?
- 基础答案(回答错误):两种,继承 Thread 和 实现 Runnable
- 进阶答案(回答错误):多种,继承 Thread 、实现 Runnable、线程池创建、Callable 创建、Timer 创建等等
相信以上答案很多同学都能答出来。但它们都是错误的,其实创建线程的方式只有一种。为什么?狗哥你丫逗我么?横看竖看,至少也得两种呀。别急,放下刀。且听我慢慢分析:
第一种:继承 Thread
首先是继承 Thread,创建线程最经典的方法,这种方法很常见啦。刚入门的时候,狗哥写过不知道多少遍了。它的写法是这样的:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("通过集成 Thread 类实现线程");
}
}
// 如何使用
new MyThread().start()
如代码所示:继承 Thread 类,并重写了其中的 run () 方法,之后直接调用 start() 即可实现多线程。相信上面这种方式你一定非常熟悉,并且经常在工作中使用它们。
第二种:实现 Runnable
也是最常用的方法,写法如下:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("通过实现 Runnable 方式实现线程");
}
}
// 使用
// 1、创建MyRunnable实例
MyRunnable runnable = new MyRunnable();
//2.创建Thread对象
//3.将MyRunnable放入Thread实例中
Thread thread = new Thread(runnable);
//4.通过线程对象操作线程(运行、停止)
thread.start();
如代码所示,这种方法其实是定义一个线程执行的任务(run 方法里面的逻辑)并没有创建线程。它首先通过 MyRunnable类实现 Runnable 接口,然后重写 run () 方法,之后还要把这个实现了 run () 方法的实例传到 Thread 类中才可以实现多线程。
第三种:线程池创建线程
说完这两种在工作中最常用的,我们再说说第三种。在 Java 中,我们创建线程池是这样的:
// 10 是核心线程数量
ExecutorService service = Executors.newFixedThreadPool(10);
点进去 newFixedThreadPool 源码,在 IDEA 中调试,可以发现它的调用链是这样的:
Executors.newFixedThreadPool(10) --> new ThreadPoolExecutor(一堆参数) --> Executors.defaultThreadFactory()
可以发现最终还是调用了 Executors.defaultThreadFactory()
方法,而这个方法的源码是这样的:
static class DefaultThreadFactory implements ThreadFactory {
// 线程池序号
static final AtomicInteger poolNumber = new AtomicInteger(1);
// 线程序号
final AtomicInteger threadNumber = new AtomicInteger(1);
// 线程组
final ThreadGroup group;
// 线程池前缀
final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
/**
* 重点方法
* @param r
* @return
*/
@Override
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 () 创建线程的。所以线程池创建线程并没有脱离以上的两种基本的创建方式。
第四种:Callable 创建
第四种是有返回值的 Callable 创建线程,用法是这样的:
public class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
// 使用方法
// 1、创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 2、提交任务,并用 Future提交返回结果
Future< Integer > future = service.submit(new MyCallable());
}
Callable 与 Runnable 名字还有点像,区别在于 Runnable 是无返回值的。它们的本质都是定义线程要做的任务(call 或 run 方法里面的逻辑),而不是说他们本身就是线程。但无论有无返回值,它们都是需要被线程执行。
如代码所示,它们可以提交到线程池执行,通过 sumbit 方法提交。这时就参考方式三,由线程工厂负责创建线程。当然,还有其他方法执行 Callable 任务。但是不管怎么说,它还是离不开实现 Runnable 接口和继承 Thread 类这两种方式。
第五种:Timer 创建
我们使用 Timer 的方式如下:
public class MyTimer {
public static void main(String[] args) {
timer();
}
/**
* 指定时间 time 执行 schedule(TimerTask task, Date time)
*/
public static void timer() {
Timer timer = new Timer();
// 设定指定的时间time,此处为2000毫秒
timer.schedule(new TimerTask() {
public void run() {
System.out.println("执行定时任务");
}
}, 2000);
}
}
如代码所示,Timer 定时器在两秒之后执行一些任务,它也确实创建了线程,但是深入源码:
private final TimerThread thread = new TimerThread(queue);
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
class TimerThread extends Thread {
// 省略内部方法
}
注意到 TimerThread ,它还是继承于 Thread ,所以 Timer 创建线程最后又绕回到最开始说的两种方式了。
为什么只有一种方式?
有同学可能说,狗哥你这扯半天不还是两种方式么?我答对了呀。。。别急,容我喝口水,下面分析为何说它是一种?
注意到 Thread 类中有一个 run 方法:
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
先看实现 Runnable 方式,它启动线程还是需要调用 start 方法(因为是 Native 方法我们看不到具体逻辑),但是线程要执行任务必须还是要调用 run 方法(不然线程执行的是啥?)。
我们看代码,run 方法非常简单。它判断 target 不为 null 就直接执行 target 的 run 方法。而 target 正是我们实现的 Runnable ,使用 Runnable 接口实现线程时传给 Thread 类的对象。
在看继承 Thread 方式,它调用 thread.start(),最终调用的还是 run 方法(run() 里面是任务)。只不过这个 run() 是我们已经重写的 run() 而不是上面 Runnable(target) 的 run()。
看到这里可算明白了,事实上创建线程本质只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式,不同的只是 run 方法(执行内容)的实现方式。
任务的来源
1、2 两种方式它们的不同点仅仅在于实现线程执行内容的不同,那么运行内容来自于哪里呢?
本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种写法:
- 实现 Runnable 接口从而实现 run() 的方式
- 继承 Thread 类重写 run () 方法的方式
然后把我们想要执行的代码传入,让线程去执行。在此基础上,如果我们还想有更多实现线程的方式,比如线程池、Callable 以及 Timer 定时器,只需要在此基础上进行封装即可。
哪种写法好?
答案是:Runnable 写法。
理由:
- 易于扩展
这个不多说,Java 是单继承。如果使用继承 Thread 的写法。将不利于后续扩展。
- 解耦
用 Runnable 负责定义 run() 方法(执行内容)。这种情况下,它与 Thread 实现了解耦。Thread 负责线程的启动以及相关属性设置。
- 性能
在一些情况下可以提高性能。比如:线程执行的内容很简单,就是打印个日志。如果使用 Thread 实现,那它会从线程创建到销毁都要走一遍,需要多次执行时,还需要多次走这重复的流程,内存开销非常大。
但是我们使用 Runnable 就不一样了。可以把它扔到线程池里面,用固定的线程执行。这样,显然是可以提高效率的。
巨人的肩膀
- Java 并发编程 78 讲 — 徐隆曦
福利
如果看到这里,喜欢这篇文章的话,请帮点个好看。微信搜索一个优秀的废人,关注后回复电子书送你 100+ 本编程电子书 ,不只 Java 哦,详情看下图。回复1024送你一套完整的 java 视频教程。