本篇要讲述的是线程中的启动线程(start),中断线程(interrupt),等待线程(join),以及线程睡眠(sleep)。下面将分别介绍这四种线程操作方式。
启动线程:
当我们创建好一个线程对象或者其子类对象后,我们可以通过调用Thread类中的start方法来启动与该对象所关联的线程,用一个最简单的例子来描述就是:
package com.newway.interruputtest;
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(()->{
System.out.println("线程开始工作");
});
thread.start();
}
}
执行上述代码,可以在控制台看到如下打印:
如果调用start方法的线程本身已经处于运行状态,或者该线程已经运行结束处于死亡状态,那么此时将会抛出IllegalThreadStateException。从源码中也可以看出端倪:
可以从源码中看出,当执行start方法的时候,会先对当前线程的状态进行检测,这里有一个int型的标志量threadStatus,它定义于Thread类中,当且仅当线程最初新建的时候,该标志量初始化值为0,当调用start方法时,如果该标志量不等于0,则会抛出IllegalThreadStateException,如果等于0,就继续执行后续的操作代码,启动thread对象相关联的线程。
从上面的示例中可以看出,当创建完一个线程,连续调用了两次start方法,此时抛出相应异常。
中断线程:
java提供了一种线程可以中断线程的机制。在这里我们要先明确一个概念,这里的线程中断机制并不是说通过该机制可以主动的去终止一个线程的运行,它仅仅起到一个协作的作用。
Java中中断机制的本质是java在每一个线程对象中都设置一个boolean类型的标志量(该标志量并未定义在Thread类里,具体的操作都是由native方法来实现的),该标志量代表了当前线程的中断状态,如果当前线程被中断,该标志量会被设为true,java的中断机制到此为止,至于被中断的线程是继续执行、终止亦或是捕捉中断来进行特殊处理,这些都是由被中断的线程来决定的。
当一个线程因为调用join、wait、sleep等方法造成阻塞的时候,如果阻塞时间过长,那么此时可以调用中断方法,那么被中断线程就会抛出java.lang.InterruptedException异常,从而退出阻塞状态并将中断标志位清空设为false。
wait方法:
join方法:
sleep方法:
此时我们可以通过捕获异常的方式来进行针对处理(当然只捕捉而不做任何处理这种行为是不提倡的),也可以继续向上抛出异常,此时可能需要重新调用interruput方法来将标志位设为true(如果需要的话),因为抛出异常后当前线程的中断状态会被清空。
其实Thread类中早期是存在主动终止线程运行的方法的,有stop方法,还有suspend方法和resume方法,但正是因为他们可以随意的终止线程,这样存在着巨大的安全性隐患,所以如今这些方法都已经被弃用了。比如在A线程中调用B线程的stop方法,但是A线程并不知道B线程的运行状态,如果B线程此时仍然在运行自己的操作代码,结果A线程终止了它,那么B线程的任务就无法完成,假设B线程在做着I/O操作,那么此时便造成了数据的丢失。
Thread类中提供了三个方法来支持该机制:
void interruput():调用该方法后,会中断调用此方法的线程对象所关联的系统线程。其实本质就是将线程中的中断标志位设为true,这也是唯一一个可以将中断标志位置为true的方法。
static boolean interrupted():此方法用于验证线程是否已经中断。同时线程的中断状态会被这个方法清除掉(使用时需谨慎)。也就是说当连续调用两次这个方法的时候,必然会得到false的结果。该方法一般用于当接收到中断信号后程序进行捕捉处理继续运行,之后仍可能接收到中断信号,所以在判断中断标志的同时需要将标志位清空,置为false。
boolean isInterrupted:此方法用于验证线程是否已经中断。该方法同上面的interrupted方法功能类似,但是不会影响线程的中断状态。
下面来说说线程中断的一些应用场景:
- 某应用正在执行某些操作,此时点击取消按钮,例如使用电脑管家在进行全盘扫描,扫描过程中点击了停止扫描按钮。
- 线程阻塞时间过长,已经无需继续等待下去。
- 多线程同时在做一件事情,如果其中某个线程完成,则其它线程就无需继续工作。
- 当应用或服务需要停止时。
- …
下面就结合一些小例子来加深对线程中断的理解:
1. 对于Thread类提供的3个支持中断机制的方法的验证:
package com.newway.interruputtest;
public class InterruputTest1 {
Thread t = null;
public static void main(String[] args) throws Exception {
new InterruputTest1().test();
}
public void test() throws Exception {
try {
t = new Thread(() -> {
System.out.println("------thread t is working------");
for(;;){
System.out.println("thread t isInterruputed ? "+t.isInterrupted());
System.out.println("thread t interruputed ? "+Thread.interrupted());
System.out.println("------thread t invoke method interrupted------");
long time = System.currentTimeMillis();
while(System.currentTimeMillis() - time < 1000){}
}
});
} catch (Exception e) {
e.printStackTrace();
}
t.start();
Thread.sleep(100);
System.out.println("------thread t invoke method interrupt------");
t.interrupt();
}
}
执行上述代码后可以在控制台上看到如下打印:
在本例中,随着方法的执行,我们创建了一个线程t,线程中通过一个死循环来不断的调用interrupted和isInterrupted方法来打印线程t的中断状态。
线程创建运行,可以看出打印中通过两个方法得到的中断状态都为false。
紧接着在主线程中,通过线程t的对象调用了interrupt方法,此时通过两个方法得到的中断状态都为true,证实了interrupt方法确实可以将线程的中断状态设为true。然后程序继续执行,这里体现了中断线程并不会终止线程的运行,因为本例中未对线程中断做任何处理,所以调用interrupt中断后,线程继续执行。
线程t继续执行操作代码,因为在上一个循环末尾处,调用了isInterrupted方法,前面说过,该方法在获取线程中断状态后,会清空调用线程的中断状态,所以当再次循环打印线程中断状态后,两个方法得出的结果都是false。
第一个例子主要是为了描述这三个方法到底是干什么用的,下面就是结合一些小场景来说说怎么用。
2.可中断的阻塞方法与中断机制:
前面我们说过当使用sleep,join,wait等会造成线程阻塞的方法的时候,如果当前线程调用了interrupt方法,发出中断信号,那么此时线程会抛出InterruptedException。
这里会存在两种情况,一种是先调用了sleep等阻塞方法,在阻塞过程中调用了interrupt方法,还有一种则是先行调用了interrupt方法,然后当执行sleep等阻塞方法的时候会立马抛出InterruptedException。下面通过两个小例子来演示这两种情况。
第一种情况:先阻塞后中断
第一个例子在主线程中创建了一个子线程t,子线程中调用了sleep方法睡眠2000ms。主线程随后调用start方法启动线程t,然后睡眠500ms,此处是为了确保子线程进入sleep状态。主线程睡眠结束后,调用interrupt方法,中断t线程,从控制台可以看出,t线程抛出了InterruptedException,在sleep的睡眠时间结束之前就打破了阻塞。
第二种情况:先中断后阻塞
第二个例子就比较简单了,直接使用主线程来模拟,在主线程中先行调用interrupt方法发送中断信号,然后调用sleep方法,从控制台的输出可以看出,当调用sleep的时候,主线程立马抛出了InterruptedException,描述是睡眠中断。
综上所述,当我们遇到了一些可中断的阻塞方法时,可以通过Thread类中的interrupt方法来产生中断,从而打断阻塞。
3.通过中断机制来终止线程的运行:
前面我们有说过中断机制仅仅只是起到协作的作用,至于被中断的线程在中断之后的具体操作是什么,是由被中断的线程自己来决定的。下面通过几个小例子来展示如何通过中断机制来终止线程的运行。
a)通过自定义中断状态来中断一个线程的运行(有些情况下Thread类提供的三个支持中断线程的方法已经不能满足我们的需求时,需要我们自定义中断状态来解决问题):
package com.newway.interruputtest;
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mt.interrupt();
mt.stop = true;
}
}
class MyThread extends Thread {
volatile boolean stop;
@Override
public void run() {
while(!stop){
long time = System.currentTimeMillis();
System.out.println("mythread is working...");
while(System.currentTimeMillis()-time < 1000){}
}
System.out.println("mythread is stoping...");
}
}
运行上述代码可以得到如下打印:
示例中,我们创建了一个MyThread类,其继承于java lang包下的Thread类。在类中我们定义了一个boolean型变量,作为我们自定义的中断状态,该变量使用了volatile关键字进行修饰,是为了保证其可见性,关于该关键字在其它的篇幅中将会讲述。MyThread类中还重写了run方法,方法内通过一个while循环来每隔一秒打印一次输出语句,这里我们将自定义的中断状态量stop作为循环的控制变量。
在主线程中,我们创建出MyThread类的对象,并调用其start方法启动了它,主线程在睡眠了三秒之后调用其interrupt方法(这里只是为了表示这里要产生中断),并将stop的值设为了true,这样线程中的循环打印将在下一个循环判断中退出循环,从而终止了线程的运行。
b)通过Thread类提供的三个方法来中断线程的运行:
package com.newway.interruputtest;
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mt.interrupt();
}
}
class MyThread extends Thread {
@Override
public void run() {
//这里同样可以使用Thread.currentThread().interrupted()方法来判断中断,但是该方法会在判断状态之后清空中断状态位,在使用时应根据实际场景来选择。
while(!Thread.currentThread().isInterrupted()){
long time = System.currentTimeMillis();
System.out.println("mythread is working...");
while(System.currentTimeMillis()-time < 1000){}
}
System.out.println("mythread is stoping...");
}
}
执行上述代码后可以从控制台看到如下打印:
该示例与第一个示例区别不大,仅仅只是循环的控制变量从自定义的中断状态改为通过Thread类提供的isInterrupted方法获取的线程中断状态。
乍一看下来,有了Thread类提供的方法后,第一个例子中自定义中断状态的方法似乎变得不再需要了,其实并不然,在实际的开发中,我们经常需要使用比人的库,如果别人的方法中自己处理了中断并且没有继续向上通知,而你却需要对中断进行响应,在你不能改动比人代码的时候你就需要使用自定义的中断状态来实现自己的功能。
c)阻塞状态下对中断的处理:
前面提到过,线程在调用可中断的阻塞方法时,当调用其中断方法后,会抛出相应InterruptedException。当我捕获到这些异常时,是不推荐对该异常进行空处理的,如:
package com.newway.interruputtest;
public class Test {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (;;) {
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.out.println("thread t is working");
}
});
t.start();
t.interrupt();
}
}
执行上述代码后我们可以在控制台看到如下打印:
事实上当我们调用interrupt方法时就已经抛出了InterruptedException,但因为我们没有对该异常进行任何处理,这样导致上层完全无法得知此处发生了中断,所以一般情况下是不建议如此操作的(当然特殊情况下你自己了解这样做不会对整体造成任何问题,你要这样写也不是不可以的,比如本示例中在catch中进行空操作也没有造成程序问题)。
一般情况下,当我们捕捉到了可中断阻塞方法抛出的InterruptedException,我们可以继续向上层抛出该异常,如果是检测到了中断,我们则可以清空当前的中断状态同样抛出该异常,使我们自己的方法成为一个可中断的方法。在一些不好抛出异常的情况下,可以调用Thread类提供的interrupt方法,重置当前的中断状态,因为抛出异常时该状态会被清空,重置后,在其它需要对中断进行操作的地方可以继续通过判断中断标志位来继续工作。
关于中断就说到这儿吧,下面贴出两个专门写线程中断的帖子,笔者在学习中断的时候,从中得到了很多帮助:
1.详细分析java中断机制
2.Thread中的中断机制(interrupt)
等待线程
在一个线程中我们偶尔会启动另一个线程进行一些操作,并要等待其返回一个结果后,当前线程再继续向下执行。这时我们就需要使用到Thread类中提供的join方法。
Thread类中总共提供了3中join方法:
void join(long millis):调用线程在死亡之前最多等待millis毫秒,如果传入的值为0,将无限等待下去,如果传入millis为负数,则会抛出IllegalArgumentException。该方法是可中断的,当等待过程中遭遇到线程中断时,该方法会抛出InterruptedException,并且清空中断状态。
void join(long millis, int nanos):调用线程在死亡之前最多等待millis毫秒nanos纳秒,如果传入的参数包含负数或者nanos的值大于999999将会抛出IllegalArgumentException。该方法是可中断的,当等待过程中遭遇到线程中断时,该方法会抛出InterruptedException,并且清空中断状态。
void join():调用线程在死亡之前将无限期的等待。该方法是可中断的,当等待过程中遭遇到线程中断时,该方法会抛出InterruptedException,并且清空中断状态。
从源码中可以看出,join方法的本质是通过wait方法来实现的,并且当且仅当当前线程是处于存活状态时,该方法才有作用。我们可以看出该方法是同步方法,意味着使用该方法的前提是调用者能拿到被调用者的锁。下面我们通过一个简单的小例子来展示join的使用方法。
package com.newway.interruputtest;
public class Test {
static Thread t,t1;
public static void main(String[] args) {
t = new Thread(()->{
System.out.println("thread t is working");
try {
t1.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("thread t has been finished");
});
t1= new Thread(()->{
System.out.println("thread t1 is working");
System.out.println("thread t1 has been finished");
});
t.start();
t1.start();
}
}
运行以上代码可以在控制台看到如下打印:
在示例中,我们在主线程中创建了两个子线程t,t1,并顺序启动了它们。在t线程的run方法中我们调用了t1对象的join方法,此时t线程将等待t1线程执行完毕后才会继续执行,因此控制台打印中“thread t has been finished”才在最后打印出来。
所以也有人将join比作可以将两个交替执行的线程合并为一个顺序执行的线程的工具。
线程睡眠
Thread类提供了一对静态方法来使当前线程进入睡眠状态,暂时性的停止执行任务。
1. void sleep(long millis):使当前线程睡眠millis毫秒的时间。如果millis是负数,则会抛出IllegalArgumentException。该方发是可中断的,如果在睡眠期间当前线程被中断,那么会抛出InterruptedException,并且清空当前线程的中断状态。
2. void sleep(long millis,int nanos):使当前线程睡眠millis毫秒和nanos纳秒的时间。如果传入的参数存在负数,或者nanos的值大于999999,那么会抛出IllegalArgumentException。该方发是可中断的,如果在睡眠期间当前线程被中断,那么会抛出InterruptedException,并且清空当前线程的中断状态。
对于sleep方法的使用这里就不举例描述了,在 前面的示例中已经多次使用过该方法,该方法是静态方法,所以直接通过Thread类调用即可,并且操作的是当前线程。
这里注意的是,sleep方法中传入的参数是表示当前线程进入睡眠暂停执行的最短时间,因为其结束睡眠后进入的是准备状态,所以具体什么时候恢复操作还要看调度器是否立马给予当前线程执行的权限。
以上为本篇的全部内容。