1 前言
本节开始我们来回顾下线程基础相关的东西,最近在复习所以来做一些笔记哈,这节我们来讲讲创建线程的方式。
2 创建分类
Java提供了两种线程的创建方法,第一种是继承Thread类;第二种是实现Runable接口,并将Runnable实例传递给Thread类。详细的可以参考官方文档哈:https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
那么接下来我们就来看看这两种方式的优缺点哈,只有了解好坏我们才能在使用中悠然自得。
2.1 继承Thread类
优点 : 方便传参,可以在子类添加成员变量,通过方法设置参数或构造函数传参,也就是类就是一个线程类,对象创建出来start就完事了。
缺点:
- 单继承的弊端,大家都懂的
- 这种创建方式不便于线程池管理,像野孩子需要自己管理
- 代码写法上可能相对于接口方式要麻烦
- 无法获取线程运行结果
class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } } PrimeThread p = new PrimeThread(143); p.start();
2.2 Runnable 接口
优点 : 此方式可以继承其他类。也可以使用线程池管理,节约资源。创建线程代码的耦合性较低。推荐使用此种方式创建线程。
缺点: 不方便传参,只能使用主线程中用final
修饰的变量。其次是无法获取线程任务的返回结果。
// 第一种:常规写法 class PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } } PrimeRun p = new PrimeRun(143); new Thread(p).start(); // 第二种:lambda写法 new Thread(()->{}).start()
2.3 Callable 接口
此种方式创建线程底层源码也是使用实现Runnable
接口的方式实现的,所以不是一种新的创建线程的方式,只是在实现Runnable
接口方式创建线程的基础上,同时实现了Future
接口,实现有返回值的创建线程。
public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { // 封装一个Callable任务 Callable<String> callTask = () -> { System.out.println("开始执行【Callable】任务"); TimeUnit.SECONDS.sleep(2); // 执行结束后返回结果值 return "我是结果"; }; // 创建异步任务 FutureTask<String> task = new FutureTask<>(callTask); // 启动线程 new Thread(task).start(); // 等待结果 String res = task.get(); System.out.println(res); } }
3 引申问题
3.1 如果同时使用Thread和Runnable两种方式创建线程并启动,会怎样?
public class CreateThread { public static void main(String[] args) { new Thread(new Runnable() { public void run() { System.out.println("通过Runnable接口创建线程"); } }) { @Override public void run() { // 此方法会覆盖掉父类run方法,即父类run方法不再执行(java面向对象中的继承特性) System.out.println("通过Thread类创建线程"); } }.start(); } }
分析:上述代码先通过Runnable方式创建一个线程,并实现接口中的run方法。然后在new Thread的方法体中再次重写Thread的run方法时,父类的run方法将不会再执行,即System.out.println("通过Runnable接口创建线程")此行代码不会被执行了,直接执行重写后的run方法System.out.println("通过Thread类创建线程")。原因是Java面向对象中继承后重写父类的方法时,该父类的方法将不再被执行,直接执行子类重写后的方法,所以结果只打印了“通过Thread类创建线程”此行输出。
3.2 线程的启动为什么不能直接调用run方法,而是调用start方法?
通过调用run()方法,是在主线程中执行任务,所以本质上并没有创建出新的线程,其实就是方法调用。
通过调用start()方法,是在主线程中创建一个子线程去执行任务,这才是创建新线程去执行。通过start()方法启动线程后,并不一定立即执行,而是由线程调度器决定何时运行,可能立刻就会运行,也可能稍后才会运行,也可能一直不运行(饥饿状态)。
两种方式都能成功运行并执行,只是直接调用run()方法,并没有使用新线程去运行任务,程序还是串行执行的,所以这种方式是不符合预期的。
3.3 如果线程连续调用两次start()方法,会怎样?
public class ThreadClass extends Thread { @Override public void run() { System.out.println("运行Thread线程"); } public static void main(String[] args) { ThreadClass threadClass = new ThreadClass(); threadClass.start(); threadClass.start(); } }
原因分析:
查看start()
方法源码:
public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } }
通过源码中发现,之所以抛出java.lang.IllegalThreadStateException,是因为threadStatus不为0;threadStatus为0,表示线程刚初始化完成,还没有启动。若threadStatus不为0,说明线程已经被启动过了。所以第二次调用start()方法时,线程的状态threadStatus已改变,此时会抛出异常。threadStatus是voliate修饰的保证可见性顺序性,遵循happens-before原则,所以第二次启动发现状态不对直接抛异常。
4 小结
本节我们看了下创建线程的两种方式,即继承Thread
类和实现Runnable
接口。其他所有创建线程的方式,底层都是使用这两种方式中的一种实现的,比如通过线程池、通过匿名类、通过lambda
表达式、通过Callable<V>
接口等等,全是通过这两种方式中的一种实现的。有理解不对的地方欢迎指正哈。