能有此文十分感谢《Java编程思想》一书及其作者Bruce Eckel!
七、让步
如果知道已经完成了在run()方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示:工作已经做得差不多了,可以让别的线程使用CPU了。这个暗示将通过调用yield()方法来作出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。当调用yield()时,你也是在建议具有相同优先级的其他线程可以运行。
八、后台线程
所谓后台线程,是指在程序运行时在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中所有的后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。import java.util.concurrent.*;
public class SimpleDaemons implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public static void main(String[] args) throws Exception {
for(int i = 0; i < 10; i++) {
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true); // Must call before start()
daemon.start();
}
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(175);
}
}
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
一旦main()完成其工作,就没什么能阻止程序终止了,因为除了后台线程之外,已经没有线程在运行了。main()线程被设定为短暂睡眠,所以可以观察到所有后台线程启动后的结果。
SimpleDaemons.java创建了显式的线程,以便可以设置它们的后台标志。通过编写定制的ThreadFactory可以定制由Executor创建的线程的属性(后台、优先级、名称):
import java.util.concurrent.ThreadFactory;
public class DaemonThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}
与普通的ThreadFactory的唯一差异就是它将后台状态全部设置为True。下面可以用这个新的DaemonThreadFactory 作为参数传递给Executor.newCachedThreadPool():
import java.util.concurrent.*;
public class DaemonFromFactory implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool(
new DaemonThreadFactory());
for(int i = 0; i < 10; i++)
exec.execute(new DaemonFromFactory());
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(500); // Run for a while
}
}
可以通过调用isDaemon()方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置为后台线程。
后台线程在不执行finally子句的情况下就会终止其run()方法:
import java.util.concurrent.*;
class ADaemon implements Runnable {
public void run() {
try {
System.out.println("Starting ADaemon");
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException e) {
System.out.println("Exiting via InterruptedException");
} finally {
System.out.println("This should always run?");
}
}
}
public class DaemonsDontRunFinally {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new ADaemon());
t.setDaemon(true);
t.start();
}
}
当运行这个程序时候将看到finally子句并不会执行。但是如果注释掉对t.setDaemon(true);调用,就会看到finally子句将会执行。
九、实现线程的几种代码编写方式
到目前为止,以上示例中,任务类都实现了Runnable。在一些简单的情况下使用 直接从Thread继承这种可替换的方式,如下:public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
// Store the thread name:
super(Integer.toString(++threadCount));
start();
}
public String toString() {
return "#" + getName() + "(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new SimpleThread();
}
}
可以通过调用适当的Thread构造器为Thread对象赋予具体的名称,这个名称可以通过使用getName()从toString()中获得。
另外一种可能会看到的惯用法是
自管理的Runnable:
public class SelfManaged implements Runnable {
private int countDown = 5;
private Thread t = new Thread(this);
public SelfManaged() { t.start(); }
public String toString() {
return Thread.currentThread().getName() +
"(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new SelfManaged();
}
}
这与从Thread继承并没有什么特别差异,只是语法稍晦涩一点。但是,实现接口使得可以继承另外一个不同的类。
注意,start()是在构造器中调用的。这个示例相当简单,因此可能是安全的,但是应该意识到,在构造器中启动线程可能会变得很有问题,因为另外一个任务可能会在构造器结束之前执行,这就意味着该任务能够访问处于不稳定状态的对象。这是优选Executor而不是显式创建Thread对象的另一个原因。
有时通过内部类来将线程代码隐藏在类中将会很有用,如下:
import java.util.concurrent.*;
// Using a named inner class:
class InnerThread1 {
private int countDown = 5;
private Inner inner;
private class Inner extends Thread {
Inner(String name) {
super(name);
start();
}
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
}
public InnerThread1(String name) {
inner = new Inner(name);
}
}
// Using an anonymous inner class:
class InnerThread2 {
private int countDown = 5;
private Thread t;
public InnerThread2(String name) {
t = new Thread(name) {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
// Using a named Runnable implementation:
class InnerRunnable1 {
private int countDown = 5;
private Inner inner;
private class Inner implements Runnable {
Thread t;
Inner(String name) {
t = new Thread(this, name);
t.start();
}
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return t.getName() + ": " + countDown;
}
}
public InnerRunnable1(String name) {
inner = new Inner(name);
}
}
// Using an anonymous Runnable implementation:
class InnerRunnable2 {
private int countDown = 5;
private Thread t;
public InnerRunnable2(String name) {
t = new Thread(new Runnable() {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return Thread.currentThread().getName() +
": " + countDown;
}
}, name);
t.start();
}
}
// A separate method to run some code as a task:
class ThreadMethod {
private int countDown = 5;
private Thread t;
private String name;
public ThreadMethod(String name) { this.name = name; }
public void runTask() {
if(t == null) {
t = new Thread(name) {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
}
public class ThreadVariations {
public static void main(String[] args) {
new InnerThread1("InnerThread1");
new InnerThread2("InnerThread2");
new InnerRunnable1("InnerRunnable1");
new InnerRunnable2("InnerRunnable2");
new ThreadMethod("ThreadMethod").runTask();
}
}
十、术语
在Java中,可以选择如何实现并发编程,并且这个选择会令人困惑。这个问题通常来自于用来描述并发程序技术的术语,特别是涉及线程的那些。
到目前为止,应该看到要执行的任务与驱动它的线程之间有一个差异,这个差异在java类库中尤为明显。因为对Thread类实际上没有任何的控制权(并且这种隔离在使用执行器时更加明显,因为执行器将替你处理线程的创建和管理)。你创建任务,并通过某种方式将一个线程附着到任务上,以使得这个线程可以驱动任务。Thread类自身不执行任何操作,它只是驱动赋予它的任务。
从概念上讲,我们希望创建独立于其他任务运行的任务,因此我们应该能够定义任务,然后说“开始”,并且不用操心其细节。但是在物理上,创建线程可能代价高昂,因此必须保存并管理它们。这样从实现角度看,将任务从线程分离出来是很有意义的。
十一、加入一个线程
一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才能继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程结束才恢复(即t.isAlive()返回为假)。
也可在调用join()时带上一个超时参数,这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。
对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时需要用到try-catch子句。如下面示例:
class Sleeper extends Thread {
private int duration;
public Sleeper(String name, int sleepTime) {
super(name);
duration = sleepTime;
start();
}
public void run() {
try {
sleep(duration);
} catch(InterruptedException e) {
System.out.println(getName() + " was interrupted. " +
"isInterrupted(): " + isInterrupted());
return;
}
System.out.println(getName() + " has awakened");
}
}
class Joiner extends Thread {
private Sleeper sleeper;
public Joiner(String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
public void run() {
try {
sleeper.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}
public class Joining {
public static void main(String[] args) {
Sleeper
sleepy = new Sleeper("Sleepy", 1500),
grumpy = new Sleeper("Grumpy", 1500);
Joiner
dopey = new Joiner("Dopey", sleepy),
doc = new Joiner("Doc", grumpy);
grumpy.interrupt();
}
} /* Output:
Grumpy was interrupted. isInterrupted(): false
Doc join completed
Sleepy has awakened
Dopey join completed
*/
Sleeper是一个Thread类型,它要休眠一段时间,这段时间是通过构造器传进来的参数所指定的。在run()中,sleep()方法有可能在指定的时间期满时返回,但也可能被中断。在catch子句中,将根据isInterrupted()的返回值来报告这个中断。当另一个线程在该线程上调用interrupt()时,将给该线程设定一个标志,标明该线程已被中断。然而异常捕获时将清理这个标志,所以在catch子句中,在异常被捕获的时候这个标志总是假的。除异常之外,这个标志还可以用于其他情况,比如线程可能会检查其中断状态。
Joiner线程通过在Sleeper对象上调用join()方法来等待Sleeper醒来。在main()里面,每个Sleeper都有一个Joiner,这可以在输出中发现,如果Sleeper中断或者是正常结束,Joiner将和Sleeper一同结束。
注意,Java SE5的java.util.concurrent类库包含诸如CyclicBarrier这样的工具,它们可能比最初的线程类库中的join()更加适合。
十二、创建有相应的用户界面
使用线程的动机之一就是建立有响应的用户界面。下面给出了一个基于控制台用户界面的简单示例。下面例子有两个版本:一个关注于运算,所以不能读取控制台输入,另一个把运算放在任务里单独运行,此时就可以在进行运算的同时监听控制台输入。class UnresponsiveUI {
private volatile double d = 1;
public UnresponsiveUI() throws Exception {
while(d > 0)
d = d + (Math.PI + Math.E) / d;
System.in.read(); // Never gets here
}
}
public class ResponsiveUI extends Thread {
private static volatile double d = 1;
public ResponsiveUI() {
setDaemon(true);
start();
}
public void run() {
while(true) {
d = d + (Math.PI + Math.E) / d;
}
}
public static void main(String[] args) throws Exception {
//! new UnresponsiveUI(); // Must kill this process
new ResponsiveUI();
System.in.read();
System.out.println(d); // Shows progress
}
}
UnresponsiveUI 在一个无限循环里执行运算,显然程序不可能到达读取控制台输入的那一行。要想让程序有响应,就得把计算程序放在run()方法中,这样它就能让出处理器给别的程序。
十三、线程组
线程组持有一个线程的集合。十四、捕获异常
由于线程的本质特性,使得不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,它就会向外传播到控制台,除非采取特殊的步骤捕获这种错误的异常。在java se5之前可以使用线程组来捕获这些异常,但是之后就可以用Executor来解决这个问题。
下面的任务总是会抛出一个异常,该异常会传播到其run()方法的外部。
import java.util.concurrent.*;
public class ExceptionThread implements Runnable {
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}
将main()的主体放到try-catch语句中是没有作用的:
import java.util.concurrent.*;
public class NaiveExceptionHandling {
public static void main(String[] args) {
try {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch(RuntimeException ue) {
// This statement will NOT execute!
System.out.println("Exception has been handled!");
}
}
}
这将产生与前面示例相同的结果:未捕获的异常。
为解决这个问题,可以修改Executor产生线程的方式。Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许在每个Thread对象上附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。为了使用它,创建一个新型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler。将这个工厂传递给Executors创建新的ExecService的方法:
import java.util.concurrent.*;
class ExceptionThread2 implements Runnable {
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println(
"eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughtExceptionHandler implements
Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
t.setUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());
System.out.println(
"eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(
new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
} /* Output: (90% match)
HandlerThreadFactory@de6ced creating new Thread
created Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@1fb8ee3
run() by Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@1fb8ee3
caught java.lang.RuntimeException
*/
上面的示例使得可以按照具体的情况逐个地设置处理器。如果知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器:
import java.util.concurrent.*;
public class SettingDefaultHandler {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
} /* Output:
caught java.lang.RuntimeException
*/
这个处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。