(接上文《线程基础:线程(3)——JAVA中的基本线程操作(中)》)
2-2、interrupt信号
interrupt,单词本身的含义是中断、终止、阻断。当某个线程收到这个信号(命令)的时候,会将自生的状态属性置为“interrupted”,但是线程本身并不会立刻终止。程序员需要根据这个状态属性,自行决定如何进行线程的下一步活动。
2-2-1、interrupt和InterruptedException
上图是文章中已出现无数次的线程状态变化图,我们已经知道线程从创建后可以处于多种不同状态:就绪(可运行)、运行中、阻塞(等待中)、死亡。并不是线程处于任何状态,都可以接收interrupt信号。如果在收到interrupt信号时,线程处于阻塞状态(wait()、wait(time)或者sleep引起的),那么线程将会抛出InterruptedException异常:
Thrown when a thread is waiting, sleeping, or otherwise occupied, and the thread is interrupted, either before or during the activity. Occasionally a method may wish to test whether the current thread has been interrupted, and if so, to immediately throw this exception.
上图已经清楚说明了当Thread收到interrupt信号时,可能的两种结果:要么其线程对象中的isinterrupt属性被置为true;要么抛出InterruptedException异常。注意,如果抛出了InterruptedException异常,那么其isinterrupt属性不会被置为true。
2-2-2、代码示例
下面我们通过一段测试代码,来说明interrupt信号的工作情况:
package test.thread.interrupt;
public class InterruptProcessor {
public static void main(String[] args) throws Exception {
// thread one线程
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
// 并不是线程收到interrupt信号,就会立刻种种;
// 线程需要检查自生状态是否正常,然后决定下一步怎么走。
while(!currentThread.isInterrupted()) {
/*
* 这里打印一句话,说明循环一直在运行
* 但是正式系统中不建议这样写代码,因为没有中断(wait、sleep)的无限循环是非常耗费CPU资源的
* */
System.out.println("Thread One 一直在运行!");
}
System.out.println("thread one 正常结束!" + currentThread.isInterrupted());
}
});
// thread two线程
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
while(!currentThread.isInterrupted()) {
synchronized (currentThread) {
try {
// 通过wait进入阻塞
currentThread.wait();
} catch (InterruptedException e) {
e.printStackTrace(System.out);
System.out.println("thread two 由于中断信号,异常结束!" + currentThread.isInterrupted());
return;
}
}
}
System.out.println("thread two 正常结束!");
}
});
threadOne.start();
threadTwo.start();
// 您可以通过eclipse工具在这里打上端点,以保证threadOne和threadTwo完成了启动
// 当然您还可以使用其他方式来确保这个事情
System.out.println("两个线程正常运行,现在开始发出中断信号");
threadOne.interrupt();
threadTwo.interrupt();
}
}
上面的示例代码中,我们创建了两个线程threadOne和threadTwo。其中threadOne线程在没有任何阻塞的情况下一直循环运行(虽然这种方式在正式环境中不建议使用,但是这里我们是为了模拟这种线程运行状态),每循环一次都检测该线程的isInterrupt属性的值,如果发现值为true则终止循环;另一个threadTwo线程,在启动后马上进入阻塞状态,等待唤醒(实际上没有其他线程会唤醒它,以便模拟线程阻塞的状态)。
这时我们向两个线程发出interrupt信号。以下是两个线程的执行结果:
Thread One 一直在运行!
Thread One 一直在运行!
thread one 正常结束!true
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:503)
at test.thread.interrupt.InterruptProcessor$2.run(InterruptProcessor.java:34)
at java.lang.Thread.run(Unknown Source)
thread two 由于中断信号,异常结束!false
通过IDE工具显示的结果,我们看到threadOne线程的isInterrupt属性被成功置为true,循环正常结束线程运行正常完成;而threadTwo线程由于处于wait()引起的阻塞状态,所以在收到interrupt信号后,抛出了异常,其isInterrupt属性依然是false;
2-2-3、thread.isInterrupted()和Thread.interrupted()的区别
在Java的线程基本操作方法中,有两种方式获取当前线程的isInterrupt属性。一种是对象方法thread.isInterrupted(),另一种是Thread类的静态方法Thread.interrupted()。这两个方法看似相同,实际上是有区别的,我们来看看Java的Thread类的这部分源代码:
public
class Thread implements Runnable {
......
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
......
}
可以看到,对象方法的thread.isInterrupted()和静态方法的Thread.interrupted()都是调用的JNI底层的isInterrupted()方法。但是区别在于这个ClearInterrupted参数,前者传入的false,后者传入的是true。相信各位读者都已经猜出其中的含义了,ClearInterrupted参数向操作系统层指明是否在获取状态后将当前线程的isInterrupt属性重置为(或者叫恢复,或者叫清除)false。
这就意味着当某个线程的isInterrupt属性成功被置为true后,如果您使用对象方法thread.isInterrupted()获取值,无论您获取多少次得到的返回值都是true;但是如果您使用静态方法Thread.interrupted()获取值,那么只有第一次获取的结果是true,随后线程的isInterrupt属性将被恢复成false,后续无论使用Thread.interrupted()调用还是使用thread.isInterrupted()调用,获取的结果都是false。
2-3、join操作
2-3-1、基本操作
join操作会使两个线程的执行过程具有先后顺序,具体来说:如果A线程调用B线程的join操作,A线程就会一直等待(或者等待某个指定的时间长度),直到B线程执行完成后(死亡状态)A线程才会继续执行。如下图所示:
下面我们演示一段示例代码,在代码中我们创建了两个线程:一个是main方法执行的线程(记为mainThread),另一个是名叫joinThread的线程。接下来我们在mainThread中调用joinThread的join方法,让mainThread线程一直阻塞到joinThread线程执行结束后,再继续执行。
package test.thread.join;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
/**
* 这个线程用来测试join的执行
* @author yinwenjie
*/
public class JoinThread implements Runnable {
static {
BasicConfigurator.configure();
}
/**
* 日志
*/
private static Log LOGGER = LogFactory.getLog(JoinThread.class);
public static void main(String[] args) throws Exception {
/*
* 启动一个子线程joinThread,然后等待子线程joinThread运行完成后
* 这个线程再继续运行
* */
Thread currentThread = Thread.currentThread();
long id = currentThread.getId();
Thread joinThread = new Thread(new JoinThread());
joinThread.start();
try {
// 等待,知道joinThread执行完成后,main线程才继续执行
joinThread.join();
JoinThread.LOGGER.info("线程" + id + "继续执行!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
Thread currentThread = Thread.currentThread();
long id = currentThread.getId();
JoinThread.LOGGER.info("线程" + id + "启动成功,准备进入等待状态(5秒)");
// 使用sleep方法,模型这个线程执行业务代码的过程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
}
//执行到这里,说明线程被唤醒了
JoinThread.LOGGER.info("线程" + id + "执行完成!");
}
}
以下是执行结果:
0 [Thread-0] INFO test.thread.join.JoinThread - 线程12启动成功,准备进入等待状态(5秒)
5002 [Thread-0] INFO test.thread.join.JoinThread - 线程12执行完成!
5002 [main] INFO test.thread.join.JoinThread - 线程1继续执行!
注意:调用join方法的线程,如果接收到interrupt信号,也会抛出InterruptedException异常。
2-3-2、join()、join(millis)和join(millis, nanos)
在Java的基本线程操作中,join()、join(millis)和join(millis, nanos),都可以实现针对目标线程的等待,而这三个方法的区别主要是在等待时间上:
join:相当于调用join(0),即一直等待到目标线程运行结束,当前调用线程才能继续执行;
join(millis):调用线程等待millis毫秒后,无论目标线程执行是否完成,当前调用线程都会继续执行;
join(millis, nanos):调用线程等待 millis毫秒 + nanos 纳秒 时间后,无论无论目标线程执行是否完成,当前调用线程都会继续执行;实际上这个join方法的描述并不准确:第二个参数nanos只是一个参考值(修正值),且只有大于等于500000时,第二个参数才会起作用(纳秒是一秒的十亿分之一)。请看这个方法的源代码:
public final synchronized void join(long millis, int nanos) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
以上是JDK1.7中,关于join(millis, nanos)方法的源代码。从源代码中我们可以看出,只有当nanos大于等于500000纳秒时,这个参数才发生意义。并且它的作用也并不是让调用线程再精确的等待如参数指定的纳秒数。
因为在现代计算机体系中,让某一种高级语言精确到1纳秒级别的控制还是一件困难的事。您可以想象一下,2.4GHz的CPU主频在一秒也只是产生24亿次高低电压,而一次硬件级别的位运算就需要多次高低电压的组合。这就不难理解为什么源代码中是在指定的纳秒数大于等于500000时,增加一个毫秒的等待时间了。所以,有时网络上的资料真的不能全信,只有读者亲自验证过的知识才是真实可信的知识。
2-4、sleep操作
sleep操作是大家在工作中最喜欢使用的方法。什么原因呢?在我来看,不是因为大多数使用者理解了sleep的使用场景,而是因为sleep操作是JAVA基本线程操作中操作意义“看似”最明显的一个方法。
Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.
sleep将“当前线程”进入阻塞状态,并且不会释放这个线程所占用的任何对象锁的独占状态。这里请注意:
- 当前线程,是指当前调用sleep方法的线程。而不是指被调用的目标线程。请看如下代码片段:
......
Thread currentThread = Thread.currentThread();
Thread joinThread = new Thread(new SleepThread());
joinThread.start();
joinThread.sleep(50000);
......
请问,并阻塞的线程是哪一个?currentThread还是joinThread?思考1秒钟,,,那些心里回答“joinThread”的同学,说明还是没有搞清楚sleep方法的含义:请一定注意“当前线程”,进入阻塞状态的一定是“currentThread”。这也是为什么sleep方法是静态方法。
- sleep方法也不像wait方法的执行效果那样:sleep方法不会释放当前线程所占用的对象锁独占模式。请再看如下代码片段:
public class Join2Thread implements Runnable {
static {
BasicConfigurator.configure();
}
/**
* 日志
*/
private static Log LOGGER = LogFactory.getLog(Join2Thread.class);
public static void main(String[] args) throws Exception {
Thread joinThread1 = new Thread(new Join2Thread());
joinThread1.start();
Thread joinThread2 = new Thread(new Join2Thread());
joinThread2.start();
}
@Override
public void run() {
// 使用sleep方法,模型这个线程执行业务代码的过程
try {
synchronized (Join2Thread.class) {
Thread.sleep(Long.MAX_VALUE);
}
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
}
}
}
请问有几个线程可以获取Join2Thread.class对象锁的钥匙(独占Join2Thread.class对象锁的抢占权)?
当joinThread1进行对象锁检查的时候,发现Join2Thread.class对象没有其它任何线程独占,于是获得Join2Thread.class对象锁的独占权,并执行到“Long.MAX_VALUE”;由于sleep并不会释放这个joinThread1线程的任何对象锁独占权(肯定就不会释放Join2Thread.class对象锁的独占权);
那么当joinThread2线程进行Join2Thread.class对象锁独占权的检查是,发现它依然被joinThread1线程占有。所以joinThread2线程等待在边界区,无法获得Join2Thread.class对象锁的钥匙。
所以能够拥有Join2Thread.class对象锁钥匙的线程就只有一个:joinThread1。
3、后文介绍
写到这里,我们已经用三篇文章的篇幅,介绍了JAVA中线程的基本操作。包括了:notify、notifyAll、wait、join、sleep、interrupt等。通过这些操作,读者已经能够操作线程在多种不同状态下进行工作切换了。当然这些方法并不是JAVA中线程的全部操作,例如还有destroy、stop、resume等操作,但这些操作都已经被淘汰(因为它们不安全)。
下篇文章开始,我将在这些线程基本操作的基础上,为读者讲解java的线程池技术、线程间通讯技术和相关的开发工具。敬请关注。