1. 线程创建方式
我看网上很多文章,对此答案结论不一,有说两种的也有说三种的,那么到底是几种呢?我们不妨去源码中找找答案。进入Thread类,看下类描述注释
发现其实它早已描述好了,只有两种方式
- 一是继承
Thread类,重写run方法,调用start方法开启线程 - 二是实现
Runnable接口,实现run方法,调用start方法开启线程
所以后面如果还有人问,到底有几种创建方式,你就可以大胆的告诉它:就两种,源码里有!
另外,稍微提一下,Thread也实现了Runnable接口,这涉及代理模式相关的知识,具体内容可以关注下后面的文章。ok,如果你不理解,也不要因为这段话而苦恼,当我啥也没说,我撤回!
2. 线程创建方式1——继承Thread类
细分的话,其实是三步
- 继承
Thread类 - 重写
run方法 - 调用
start方法开启线程
多说无益,看代码就很好理解
public class ThreadDemo {
public static void main(String[] args) {
CuteThread cuteThread = new CuteThread();
cuteThread.start(); // 调用start方法开启线程
// 这里是main线程
for (int i = 0; i < 1000; i++) {
System.out.println("我是main线程,我不可爱");
}
}
}
// 这个是新线程
class CuteThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("我是可爱的线程");
}
}
}
在这个例子中,我手动创建了一个线程,连同main线程总共就是两个线程。在这两个线程中,我都使用了for循环,分别执行了不同的输出语句,我们可以看下结果。
由于main线程打印内容非常多,所以就不全部展示了。只看关键的部分,在前期,main线程一直在输出,到了某一个时刻,我手动创建的
CuteThread抢到了时间片,执行了run方法里面的内容,由于CPU执行速度过快,所以5次for循环一瞬间就执行完了,而后main线程继续抢到时间片,继续运行。
这就是第一种线程创建方式,继承Thread类。同时也初步感受了一下“并发”:我们在程序执行的这段时间内,是运行了两个线程,但在某一个时刻,只有一个线程在工作。
3. 线程创建方式2——实现Runnable接口
在看过第一种创建方式后,第二种创建方式就很好理解了 细分的话,可以说是四步
- 实现
Runnable接口 - 实现
run方法 - 创建
Thread类,将实现了Runnable接口的对象作为构造函数的参数传入 - 调用
Thread的start方法开启线程
直接看代码
public class ThreadDemo {
public static void main(String[] args) {
new Thread(new CuteThread()).start();
// 这里是main线程
for (int i = 0; i < 500; i++) {
System.out.println("我是main线程,我不可爱");
}
}
}
// 这个是新线程
class CuteThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 500; i++) {
System.out.println("我是可爱的线程");
}
}
}
运行结果和上面代码的结果是类似的,这里就不重复展示了。
4. 调用start和调用run是不一样的
这里还有个容易忽略的问题,就是为什么不调用run方法,而是调用start方法呢?两者有什么区别?
我总结下,其实调用start方法,才是真正开启线程,后面讲解线程状态的时候会说到,这里是进入了就绪状态,准备和其他线程一起争夺时间片。而直接调用run方法,就和调用不是线程类的其他类的某一个方法一样了,并没有真正开启一个线程。不相信的朋友也可以去试一下,把start方法改成run,那所有的程序执行顺序就和调用顺序一样了,run方法的内容执行完后才会执行后面的代码,不会出现“并发”的情况了。
5. 两种创建方式,如何去选择?
一般都会推荐第二种,即实现Runnable接口。
好处有以下2个
- Java是单继承多实现,使用第二种线程创建方式,可以避开这种局限性,保存类的扩展性
- 使用第二种线程创建方式,可以多个线程共享同一个对象,非常适合多个线程来处理同一份资源的情况
关于第二个好处的细节,后面的文章会进行解释。