写博客主要是记录平常所学习的知识,如有错误之处,还望指正......
附:(本文章主要讲解线程的实现方式和线程的停止/复位以及线程六种状态之间的转换)
在之前的面试中,我相信一定有小伙伴遇到面试官问实现线程有几种方式,大多数小伙伴都说的是两种,三种,四种,只有极少人会说一种,那么今天我们来说一下,实现线程的方式到底有几种?
先来总结一下常见的几种线程实现方式
1.继承Thread类
2.实现 Runnable 接口
3.带返回值的Callable/Future
这种带有返回值创建线程的方式,本质上还是由线程池去创建的线程,也就是说无论是Callable和Future他们都是一个任务,是通过submit方法去执行的,而不是说他们本身就是线程,而通过线程池创建的子线程都是继承Thread类和实现Runnable接口的.
4.还有线程池创建线程,但是本质上还是通过线程工厂去创建线程的,默认采用DefaultThradFactory,他会给线程池创建的线程设置一些默认值,比如:线程的名称,是否守护线程,以及线程的优先级等等。但是无论怎么去设置,他最终还是去通过new Thread() 这种方式去创建线程的。
接下来 我们在去探讨为何说实现线程只有一种方式
关于这个问题,我认为透过现象看本质,会发现线程池是在 new Thread() 外做了一层封装,但是打开封装后,会发现他们最终都是基于Runnable和Thread去实现的,下面我们来研究这两种不同的实现方式
首先我们在启动线程start()方法的时候,他会去执行run()方法里面的代码,先看第一行代码if (target != null) ,判断target是否为空,不为空则执行target.run(),而你会发现target本质上就是一个Runnable
而在Runnable接口上也有说明Runnable由类Thread实现,大家可自行查看
接下来我们在说继承Thread类,继承Thread类会把run方法重写,而重写的run方法就是我们需要执行的任务,最终还是通过thread.start()方法去启动线程,而start()方法最终也是调用这个被重写run(),这个时候我们就可以明白,无论实现Runnable接口还是继承Thread类都是构造一个Thread类,也就是new Thread()创建线程的唯一方式。
总结:想要执行线程里面的内容有两种方式,就是通过实现Runnable接口和继承Thread类重写run方法,但是本质上实现线程只有一种方式那就是new Thread();
那么就有人问了,那我到底用继承Thread类好还是实现Runnable接口好一点了?我建议大家实现Runnable接口好一点
1.在一些情况下,继承Thread类,每一次执行任务都需要新建一个独立的线程,然后执行完任务之后被销毁,如果还想再继续执行任务那么必须在新建一个线程,而我们使用Runnable接口可以直接把任务传进线程池,使用一些固定的线程去完成任务,不需要多次新建销毁线程,降低了我们的性能开销
2.大家都知道Java语言不支持类的双继承,我们的类继承Thread类,那么后续就不能再继承其他的类了,相当于限制了代码的可拓展性,不过具体还是要结合应用场景,这里只是做一个参考。
2. 线程的停止与复位
大家都知道线程的终止有几种很常见的方式比如 stop命令,suspend,resume,但是这些命令都是过期的命令,不建议使用,那么我们如何用优雅的方式去停止线程了?而线程给我们提供一个叫 interrupt() 方法去停止线程,他和stop命令不同的是,stop命令是强制停止线程会造成一些数据上的混乱还有对业务的影响,而interrupt()命令他是以一种通知的方法去告诉你可以停止线程了,它可以选择立即停止,也可以选择一段时间停止,也可以压根不停止,这就取决于当前线程了。
在上面的说明中,我们通过了interrupt()方法去优雅的中断线程,但是线程中还提供了interrupted()方法对中断的线程进行复位,比如在上面的代码中我们通过interrupt方法对线程进行了中断,而我们又可以通过interrupted方法对线程进行复位,还有一种复位方法就是抛出 InterruptedException 异常,在抛出异常之前 JVM会把interrupt方法的一个标识会清除掉,将会返回false。
下面我们看一下线程中断的一个原理:
thread.interrupt()实际上就是设置一个状态标识为true,并且通过ParkEvent的unpark()方法来唤醒线程,在调用park方法之前会先判断线程的中断状态,如果为ture,则会清除当前线程的中断标识,默认为false。
那么在处于阻塞状态的线程,像sleep(),wait(),join()这种,我们这个时候去中断线程,他们会感受到吗?答案是会感受到并且抛出InterruptedException 异常去清除interrupt方法的一个中断标识,但是什么去中断这个是由线程本身去决定的,上面也有讲到的。
线程六种状态之间的转换:
众所周知线程也有属于他自己的生命周期: New(新建状态),Runnable(可运行状态),Blocked(被阻塞状态),Waiting(等待状态),Timed_waiting(计时等待),Terminated(被终止状态),如果我们想要查看当前线程的状态可以用 getState()方法,并且任何线程在任何时刻只会处于一种状态,下面我们来分析一下线程各个状态之间的转换。
New(新建状态):简单来说当你new Thread()一个线程,他没有调用start方法,也就是没有运行run方法里面的代码他就处于新建状态
Runnable(可运行状态):当我们调用start()会将线程状态转换为Runnable状态,而Ruannle状态对应操作系统线程状态中的Running和Ready状态,Java中处于Runnable状态的线程可能正在执行也有可能处于等待分配CPU资源,也就是说如果一个正在运行的线程是Runnable状态,当他任务执行到一半时,执行该线程的CPU被调度去做其他事情,导致当前线程暂时不运行,他的状态依然不变,还是处于Runnable状态,因为他有可能随时回来继续执行任务。
Blocked(被阻塞状态):这个是指被 synchronized 修饰的代码块或者方法没有抢占到monitor锁,会处于阻塞状态,如果抢占到monitor锁的话,会回到Runnable可运行状态。
Waiting(等待状态):这个是指当前线程调用了 wait(),LockSuppot.park(),join(), 刚刚说到Blocked(被阻塞状态)是没有被synchronized 修饰的代码块或者方法没有抢占到monitor锁,但是我们还是ReentrantLock锁,如果该线程没有抢占到锁就会进入wait状态,而ReentrantLock本质就是通过LockSuppot.park()去执行的。他和Blocked的区别在于Blocked是等待其他线程释放monitor锁才会被阻塞,而wait是在等待某个条件,比如join()的线程执行完,或者通过notify/notifyall();
Timed_waiting(计时等待):这个是指设置了时间参数的sleep(long millis),Object.wait(long timeout),join(long millis),LockSuppot.parkNanos(long nanos),LockSuppot.parkUntil(long deadline)才会进入计时等待状态。
上面这三个等待状态统称为阻塞状态,下面我们来总结一下:
Blocked状态需要进入到Runnable状态要抢占到monitor锁,而wait状态需要进入到Runnable状态有点特殊,因为wait是不限时的,所以需要LockSuppot.unpark()或者线程被中断才会进入Runnable状态,而处于wait状态的线程调用notify/notifyall方法时会进入到Blocked状态,因为我们进入Runnable状态必须要抢占到monitor锁,直到该线程抢占到monitor锁才会进入Runnable状态。同样在Timed_waiting也是这个道理,当然如果Timed_waiting的超时时间到了并且能直接获取到monitor锁,会直接恢复到Runnable状态,不需要在进入Blocked状态。
Terminated(被终止状态):当前线程要进入该状态有两种可能,第一是run方法执行完毕,正常退出,第二是出现一个没有捕获的异常,终止了run方法的运行,最终导致意外的终止
注意:线程是不能直接进入Blocked状态的,必须要先进入Runnable状态,线程的周期不可逆,一旦当前线程进入Runnable状态是不能回到New状态的,所以一个线程只会有一次New状态和Terminated状态。
好啦,本文到这里就结束了,有错误的地方希望大家能指正出来,谢谢大家的支持。