因为我是在学习java多线程相关的知识所以写的文章也是由浅入深比较没有深度,这一篇是线程相关比较基础的使用,大神勿喷,请绕行。
目录
● 线程的三种创建方式
● 如何安全的终止线程
● 深入理解run()和start()
● 线程各种状态切换
线程的三种新建方式
一、继承Thread类重写run()方法
/**
* 继承Thread
*/
private static class UseThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("I am extends Thread.");
}
}
这种方式创建线程,直接在main方法中进行调用即可
UseThread useThread = new UseThread();
useThread.start();
二、实现Runnable接口,重写run()
/**
* 实现Runnable接口
*/
private static class UseRun implements Runnable {
@Override
public void run() {
System.out.println("I am implements Runnable.");
}
}
注意:Runnable接口,它仅仅只是一个接口,它不能代表线程,java中能代表线程的只有Thread类,不过Thread类实现了Runnable接口,所以通过这种方式创建的线程在启动的时候就相比继承Thread类有些不同。需要先将线程创建出来,然后传入实现了Runnable的对象。
UseRun useRun = new UseRun();
new Thread(useRun).start();
三、实现Callable接口,重写call()
这种方式创建线程跟实现Runnable接口的区别在于这种方式可以允许有返回值,上面提到的两种新建线程的方式run()都是不允许有返回值的。而实现Callable接口重写的call()可以有返回值。其返回值类型就是我们实现Callable接口时传入的泛型。
/**
* 实现Callable接口,允许有返回值
*/
private static class UseCall implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("I am implements Callable.");
return "callResult";
}
}
注意:同我们上面提到的类似,Runnable不能表示线程,同样Callable接口也不能表示线程。它们都可以理解成任务。Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
因为Future只是一个接口,所以无法直接用来创建对象使用,因此就有了FutureTask。
FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值。FutureTask是Future接口的唯一一个实现类。
新建一个FutureTask对象有两种方式:
所以,使用实现Callable接口新建线程它的启动方式为
UseCall useCall = new UseCall();
FutureTask<String> futureTask = new FutureTask<>(useCall);
new Thread(futureTask).start();
如何安全的终止线程
终止
线程自然终止:要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
手动终止:暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。其原因主要是:以suspend()为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不正确状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
安全的终止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为java中的线程时协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread。interrupted()会同时将中断标识位改写为false。下面我们通过代码来理解一下isInterrupted()和Thread.interrupted()的区别。
public class EndThread {
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "while before interrput flag = " + Thread.currentThread().isInterrupted());
/**
* isInterrupted()方法和Thread.interrupted()方法都是用来判断线程是否被中断,
* 但是Thread.interrupted()方法相对于isInterrupted会同时将中断标识位改为false
*/
// 代码①
// while (!Thread.interrupted()) {
// System.out.println(threadName + " is running");
// System.out.println(threadName + " inner Thread.interrupted() flag = " + Thread.currentThread().isInterrupted());
// }
// System.out.println(threadName + " after Thread.interrupted() flag = " + Thread.currentThread().isInterrupted());
//代码 ②
while (!isInterrupted()) {
System.out.println(threadName + " is running");
System.out.println(threadName + " inner isInterrupted() flag = " + Thread.currentThread().isInterrupted());
}
System.out.println(threadName + " after Thread.interrupted() flag = " + Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("endThread");
endThread.start();
Thread.sleep(5);
endThread.interrupt();
}
}
当前代码中代码①是被注释了,代码②会被执行,使用的是
isInterrupted()方法,运行结果如下:
多次运行发现,图中①处的值不是确定的,可能为false也可能为true,②处的值是确定的一定为true。结合代码和运行结果我们可以分析到,在while循环中线程可能会被中断并且跳出while循环后中断标识位没有被重新赋值。现在我们把代码中①处的注释放开,同时把代码②处加上注释,我们再次运行,结果为:
多次运行发现图中①处的值不是确定的,可能为false也可能为true,②处的值是确定的一定为false。结合代码和运行结果我们可以分析到,在while循环中线程可能会被中断并且跳出while循环后中断标识位一定会被重新赋值(从true被赋值为false)。
这就验证了我们上边说的结论:使用静态方法Thread。interrupted()来判断线程是否被中断会将中断标识位改写为false。
如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait),则在线程检查中断标识时如果发现中断标识为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标识清除,即重新设置为false。下面我们看如下代码来证明一下此结论如下所示:
public class HasInterruptException {
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss_SSS");
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (!isInterrupted()) {
try {
System.out.println("UseThread::" + format.format(new Date()));
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println(threadName + " catch interrupt flag is " + isInterrupted() + " at " + (format.format(new Date())));
// interrupt(); 注释①处
e.printStackTrace();
}
System.out.println(threadName + " interrupt flag is " + isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new UseThread("HasInterruptException");
thread.start();
System.out.println("Main:" + format.format(new Date()));
Thread.sleep(500);
System.out.println("Main begin interrupt threadexercise:" + format.format(new Date()));
thread.interrupt();
}
}
如上代码直接运行会发现,即使我们在阻塞方法Thread.sleep(3000);处抛出了InterruptedException异常,但是我们的程序并没有因此而终止运行而是以3s为事件周期继续执行try/catch外的输出代码,并且输出的值是:HasInterruptException interrupt flag is false。这就说明在抛出异常后会立即将线程的中断标识位清除,即重新设置为false。现在我们把代码中的 “注释①”处的代码放开重新运行程序,我们可以发现运行结果是抛出异常后打印的内容为HasInterruptException interrupt flag is true。所以,至此我们应该不难发现我们已经验证了上边结论的正确性,即:处于阻塞状态的线程,在检查中断标识位为true,会抛出异常,并且会立即将线程的中断标识位清除,重新设置为false。
在开发中我们可能会自定义一个取消标识位来终止线程的运行。在此我们是不建议这样做的,因为run()方法里有阻塞调用时会无法很快检测到取消标识位,线程必须从阻塞调用返回后,才会检查这个取消标识。这种情况下,使用中断会更好,因为,一、一般的阻塞方法如sleep()等本身就支持中断的检查;二、检查中断位的状态和检查取消标志位的状态没有什么区别,用中断位的状态还可以避免声明取消标识位,减少资源的消耗。另外需要注意的是:处于死锁状态的线程无法被中断。
理解run()方法和start()方法
Thread类是java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有与操作系统中真正的线程产生什么关系。只有执行了start()方法后,才实现了真正意义上的启动线程。
start()方法让一个线程进入就绪队列等待分配CPU,分到CPU后才调用实现的run()方法,start()方法不能重复调用。因为一个线程只能启动一次。
run()方法时业务逻辑实现的地方,本质上和任意一个类中的任意一个成员方法没有任何区别,可以重复执行,可以被单独调用。关于这个大家可以自行撸码进行验证。
线程各种状态切换
关于线程中各种状态的定义大家可以参考java线程状态定义。在此我只对线程状态的切换以图的形式进行表示。
这是我根据自己对线程状态的理解绘制的简易的线程状态图,网上可能会有很多不同于此的线程状态切换图的表示,但是我认为大致都是相似的。
至此,java线程学习系列的第二篇也就结束了,下一篇我将会具体介绍线程中的其他方法以及线程间的共享和协作等内容。希望各路大神多多看完指教,小生在此不胜感激。
相关文章阅读