线程对象
每个线程都关联一个Thread类实例。创建一个并发应用有两种基本策略:
- 直接控制线程创建和管理,每当你的应用需要一个异步运行的任务时简单地实例化一个Tread类,并启动它。
- 创建线程,然后托管你的线程管理,把应用程序的任务执行及管理交给executor。
这一节里我们讨论Thread对象。Executors将在高级并发对象(high-level concurrency objects)中讨论。
定义和启动一个线程
一个应用要创建一个线程实例必须提供在线程中要run的代码。有两种方式:
- 提供一个Runnable对象。Runnable接口定义了一个方法run,run方法中包含了在线程中运行的代码。Runnable对象被传递给Thread的构造器,举例说明如下:
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
- 继承Thread。Thread类本身实现了Runnable接口,尽管它的run实现是空的(没有一行代码)。继承了Thread的子类通过重写run方法,提供自己的run实现。举例说明如下:
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new HelloThread()).start();
}
}
注意,两个例子都通过调用Thread.start来启动一个新线程。
这两种风格你该用哪一种呢?第一种采用(employs)了Runnable对象,这种风格更普适(general),因为Runnable对象在实现Runnable接口的同时可以继承其他父类,而第二种不可以再继承Thread以外的父类。第二种风格在简单的应用中更易于使用,但是你的线程任务类只能是Thread的子类。本课程只关注第一种方法(approach),将Runnable任务的实现和执行Runnable任务的Thread对象分开。不只是因为这种方法更复杂(flexible),更重要的是,这种方法适用于后面要讲到的高级线程管理编程接口(APIs)。
Thead类定义了很多在线程管理中很有用的方法,其中的有些静态方法会返回线程的信息(譬如Thread.currentThread()),还有些静态方法的调用将影响当前线程的状态(譬如 thread.yield())。其他的方法(成员方法)由管理线程和线程对象的线程调用(有点拗口,意思是调用方法影响的是该方法所属线程对象所代表的线程,经常用于线程管理,例如thread.interrupt(),一般情况下线程不会interrupt自己)。我们会在接下来的章节中测试(examine)这些方法。
Sleep暂停线程执行
Thread.sleep 使当前线程暂停执行指定时长。此方法可以很有效地把处理器时间转让给同一进程中的其他线程或同一系统中的其他进程。Sleep方法也用来控制执行进度,就像下面的例子中展示的那样,还可以用来等待另一个有执行时间需求的线程,就像后面的章节中的SimpleThreads例子展示的那样。
有两个重载的sleep版本可供使用:一个睡眠时长的单位是毫秒(millisecond),另一个睡眠时长的单位是纳秒(nanosecond)。然而,由于底层操作系统的限制,这两个方法都不保证睡眠时长是精确的。在后面的章节中我们还会看到,睡眠会因被中断(interrupts)而终结。无论如何,你都不能假设调用sleep会精确地暂停当前线程指定时长。
SleepMessages例子使用sleep每隔四秒打印一条消息:
public class SleepMessages {
public static void main(String args[])
throws InterruptedException {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
for (int i = 0;
i < importantInfo.length;
i++) {
//Pause for 4 seconds
Thread.sleep(4000);
//Print a message
System.out.println(importantInfo[i]);
}
}
}
注意到main方法声明抛出了InterruptedException异常,这实际上是sleep方法在当前线程睡眠过程中被另一个线程中断时抛出的异常。因为程序并没有定义可能会中断当前线程的另一个线程,所以就不用多此一举捕获异常了。
中断(Interrupts)
一个interrupt是一个通知,告诉一个线程“你应该停止正在做的事情,做点别的!”(类似下课铃声,告诉你你需要放松放松了。你应该去上厕所,当然你也可以选择继续学习)。程序员可以任意决定一个线程究竟如何响应一次中断,但是一般情况下你都应该选择终止你的线程。这就是本节中要着重强调的做法(usage)。
一个线程通过调用另一线程对应线程对象的interrupt方法来向该线程发送一次中断通知。为了使中断机制(the interrupt mechanism)能够正常工作,这个被中断的线程必须支持自己的中断。
支持中断
一个线程该如何支持自己的中断?这取决于它正在做什么。如果一个线程频繁地调用抛出InterruptedException异常的方法,只要在捕获异常时选择退出run方法就好了。例如在SleepMessage例子中,假设消息循环在Runnable对象的run方法中,为了支持中断可以这样做:
for (int i = 0; i < importantInfo.length; i++) {
// Pause for 4 seconds
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// 我们被中断了,不要再打印消息了。
return;
}
// Print a message
System.out.println(importantInfo[i]);
}
很多方法会抛出InterruptedException,例如sleeep,当收到中断后应取消当前的操作并立即返回。
如果线程运行中长时间不调用抛出InterruptedException异常的方法呢?那就必须定期地(periodically)调用Thread.interrupted方法,它可以在当前线程收到一次中断后返回true。例如:
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
//我们被中断了,不要再嘎吱嘎吱了。
return;
}
}
在这个简单的例子中,代码只是简单地测试中断,如果收到中断立即退出。在更复杂的程序中,抛出一个InterruptedException会更有意义:
if (Thread.interrupted()) {
throw new InterruptedException();
}
这样可以在异常捕获子句(catch clause)里集中编写中断处理代码。
中断状态标记
中断机制通过维护一个叫作中断状态(interrupt status)的内部标记实现。调用Thread.interrupt(此处应为成员方法而非静态方法)将设置这个标记。当线程调用静态方法Thread.interrupted来检查中断时,中断状态被清除(clear)。一个线程调用非静态方法isInterrupted来查询另一个线程的中断状态,它不会改变中断状态标记。
按照惯例,每一个退出时抛出InterruptedException异常的方法都要清除中断状态。然而,很可能中断状态会立刻被另一个线程通过调用interrupt重新设置。
邀请(Joins)
Join方法允许一个线程等待另一个线程执行直到结束。如果一个线程对象的线程正在执行,
t.join();
将导致当前线程暂停执行直到t的线程终止执行。通过join的另外几个重载方法程序员可以指定一个等待时长。然而,和sleep一样,join在计时上依赖于操作系统,所以你不能假定join会确切地等待你指定的时长。
和sleep一样,要求你对join捕获InterruptedException异常,并以退出作为中断响应。
简单线程示例(The SimpleThreads Example)
下面的线程示例给出了这一章节的一些概念。SimpleTreads包含了两个线程。第一个是每个Java程序都有的主线程。主线程使用Runnable对象创建了一个新线程MessageLoop,并等待它执行完毕。如果MessageLoop运行时间过长,主线程将会中断它。
MessageLoop线程不间断地打印消息。如果它在没有打印完所有消息之前收到中断,MessageLoop会打印一条消息并退出。
public class SimpleThreads {
// Display a message, preceded by
// the name of the current thread
static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}
private static class MessageLoop
implements Runnable {
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
try {
for (int i = 0;
i < importantInfo.length;
i++) {
// Pause for 4 seconds
Thread.sleep(4000);
// Print a message
threadMessage(importantInfo[i]);
}
} catch (InterruptedException e) {
threadMessage("I wasn't done!");
}
}
}
public static void main(String args[])
throws InterruptedException {
// Delay, in milliseconds before
// we interrupt MessageLoop
// thread (default one hour).
long patience = 1000 * 60 * 60;
// If command line argument
// present, gives patience
// in seconds.
if (args.length > 0) {
try {
patience = Long.parseLong(args[0]) * 1000;
} catch (NumberFormatException e) {
System.err.println("Argument must be an integer.");
System.exit(1);
}
}
threadMessage("Starting MessageLoop thread");
long startTime = System.currentTimeMillis();
Thread t = new Thread(new MessageLoop());
t.start();
threadMessage("Waiting for MessageLoop thread to finish");
// loop until MessageLoop
// thread exits
while (t.isAlive()) {
threadMessage("Still waiting...");
// Wait maximum of 1 second
// for MessageLoop thread
// to finish.
t.join(1000);
if (((System.currentTimeMillis() - startTime) > patience)
&& t.isAlive()) {
threadMessage("Tired of waiting!");
t.interrupt();
// Shouldn't be long now
// -- wait indefinitely
t.join();
}
}
threadMessage("Finally!");
}
}