用Java线程获取优异性能(I)——介绍线程、线程类及Runnable
Jeff Friesen 著 刘建华 编译
查询活跃线程
在有些情形下,你可能想了解在你的程序中哪些线程是激活的。Thread支持一对方法帮助你完成这个任务: activeCount()和 enumerate(Thread [] thdarray)。但那些方法只工作在当前线程的线程组中。换句话说,那些方法只识别属于当前线程的同一线程组的活跃线程。 (我将在以后的系列文章中讨论线程组——一种组织机制。)
静态activeCount()方法返回在当前线程的线程组中正在活跃运行的线程数量。一个程序利用这个方法的整数返回值设定一个Thread引用数组的大小。检索那些引用,程序必须调用静态enumerate(Thread [] thdarray)方法。这个方法的整数返回值确定Thread引用存贮在数组中的enumerate(Thread []thdarray)的总数。要看这些方法如何一起工作,请查看表6:
表6. Census.java
// Census.java
class Census
{
public static void main (String [] args)
{
Thread [] threads = new Thread [Thread.activeCount ()];
int n = Thread.enumerate (threads);
for (int i = 0; i < n; i++)
System.out.println (threads [i].toString ());
}
}
在运行时,这个程序会产生如下的输出:
Thread[main,5,main]
输出显示一个线程,开始线程正在运行。左边的main表示线程的名称。5显示线程的优先权,右边的main表示线程的线程组。你也许很失望不能在输出中看到任何系统线程,比如垃圾收集器线程。那种限制由Thread的enumerate(Thread [] thdarray) 方法产生,它仅询问当前线程线程组的活跃线程。然而, ThreadGroup类包含多种enumerate()方法允许你捕获对所有活跃线程的引用而不管线程组。在稍后的系列中,探讨ThreadGroup时我将向你显示如何列举所有的引用。
警告
当重申一个数组时不要依靠activeCount()的返回值。如果你这样做了,你的程序将冒掷出一个NullPointerException对象的风险。为什么呢?在调用activeCount()和enumerate(Thread [] thdarray)之间,一个或更多线程可能结束。结果, enumerate(Thread [] thdarray)能够复制少数线程引用进它的数组。因此,仅考虑将activeCount()的返回值作为数组可能大小的最大值。同样,考虑将enumerate(Thread [] thdarray)的返回值作为在一个程序对那种方法调用时活跃线程的数目。
反臭虫
如果你的程序出现故障并且你怀疑问题出在线程,通过调用Thread的dumpStack()和toString()方法你能够了解到线程的更多细节。静态dumpStack()方法提供一个new Exception ("Stack trace").printStackTrace ()的封装,打印一个追踪当前线程的堆栈。toString()依据下面格式返回一个描述线程的名称、优先权和线程组的字符串: Thread[thread-name,priority,thread-group]. (在稍后的系列中你将学到更多关于优先权的知识。)
技巧
在一些地方,这篇文章提到了当前线程的概念。如果你需要访问描述当前线程的Thread对象,则调用Thread的静态currentThread()方法。例:Thread current = Thread.currentThread ()。
等级系统
不是所有线程都被平等创建。它们被分成两类:用户和监督。一个用户线程执行着对于程序用户十分重要的工作,工作必须在程序结束前完成。相反,一个监督线程执行着后勤事务(比如垃圾收集)和其它可能不会对应用程序的主要工作作出贡献但对于应用程序继续它的主要工作却非常必要的后台任务。和用户线程不一样,监督线程不需要在应用程序结束前完成。当一个应用程序的开始线程(它是一个用户线程)结束时,JVM检查是否还有其它用户线程正在运行。如果有,JVM就会阻止应用程序结束。否则,JVM就会结束应用程序而不管监督线程是否正在运行。
当一个线程调用一个线程对象的start()方法时,新的已经开始的线程就是一个用户线程。那是缺省的。要建立一个线程作为监督线程,程序必须在调用start()前调用Thread的一个带布尔真值参数的setDaemon(boolean isDaemon)方法。稍后,你可以通过调用Thread的isDaemon()方法检查一个线程是否是监督线程。如果是监督线程那个方法返回一个布尔真值。
为了让你试试用户和监督线程,我写了一个UserDaemonThreadDemo:
表7. UserDaemonThreadDemo.java
// UserDaemonThreadDemo.java
class UserDaemonThreadDemo
{
public static void main (String [] args)
{
if (args.length == 0)
new MyThread ().start ();
else
{
MyThread mt = new MyThread ();
mt.setDaemon (true);
mt.start ();
}
try
{
Thread.sleep (100);
}
catch (InterruptedException e)
{
}
}
}
class MyThread extends Thread
{
public void run ()
{
System.out.println ("Daemon is " + isDaemon ());
while (true);
}
}
编译了代码后,通过Java2 SDK的java命令运行UserDaemonThreadDemo。如果你没有使用命令行参数运行程序,例如java UserDaemonThreadDemo, new MyThread ().start ()执行。这段代码片断开始一个在进入一个无限循环前打印Daemon is false的用户线程。(你必须按Ctrl-C或一个等价于结束一个无限循环的组合按键。)因为新线程是一个用户线程,应用程序在开始线程结束后仍保持运行。然而,如果你指定了至少一个命令行参数,例如java UserDaemonThreadDemo x,mt.setDaemon (true)执行并且新线程将是一个监督线程。结果,一旦开始线程从100毫秒休眠中醒来并结束,新的监督线程也将结束。
警告
如果线程开始执行后调用setDaemon(boolean isDaemon)方法,setDaemon(boolean isDaemon)方法将掷出一个IllegalThreadStateException对象。
Runnable
学习前面部份的例子后,你可能认为引入多线程进入一个类总是要求你去扩展Thread并将你的子类重载Thread's run()方法。然而那并不总是一种选择。Java对继承的强制执行禁止一个类扩展两个或更多个超类。结果,如果一个类扩展了一个无线程类,那个类就不能扩展Thread. 假使限制,怎样才可能将多线程引入一个已经扩展了其它类的类?幸运的是, Java的设计者已经意识到不可能创建Thread子类的情形总会发生的。这导致产生java.lang.Runnable接口和带Runnable参数的Thread构造器,如Thread(Runnable target)。
Runnable接口声明了一个单独方法署名:void run()。这个署名和Thread的run()方法署名一样并作为线程的执行入口服务。因为Runnable是一个接口,任何类都能通过将一个implements子句包含进类头和提供一个适当的run()方法实现接口。在执行时间,程序代码能从那个类创建一个对象或runnable并将runnable的引用传递给一个适当的Thread构造器。构造器和Thread对象一起存贮这个引用并确保一个新线程在调用Thread对象的start()方法后调用runnable的run()方法。示范如表8:
表8.RunnableDemo.java
// RunnableDemo.java
class RunnableDemo
{
public static void main (String [] args)
{
Rectangle r = new Rectangle (5, 6);
r.draw ();
//用随机选择的宽度和高度画不同的长方形
new Rectangle ();
}
}
abstract class Shape
{
abstract void draw ();
}
class Rectangle extends Shape implements Runnable
{
private int w, h;
Rectangle ()
{
//创建一个绑定这个runnable的新Thread对象并开始一个将调用这个runnable的
//run()方法的线程
new Thread (this).start ();
}
Rectangle (int w, int h)
{
if (w < 2)
throw new IllegalArgumentException ("w value " + w + " < 2");
if (h < 2)
throw new IllegalArgumentException ("h value " + h + " < 2");
this.w = w;
this.h = h;
}
void draw ()
{
for (int c = 0; c < w; c++)
System.out.print ('*');
System.out.print ('\n');
for (int r = 0; r < h - 2; r++)
{
System.out.print ('*');
for (int c = 0; c < w - 2; c++)
System.out.print (' ');
System.out.print ('*');
System.out.print ('\n');
}
for (int c = 0; c < w; c++)
System.out.print ('*');
System.out.print ('\n');
}
public void run ()
{
for (int i = 0; i < 20; i++)
{
w = rnd (30);
if (w < 2)
w += 2;
h = rnd (10);
if (h < 2)
h += 2;
draw ();
}
}
int rnd (int limit)
{
//在0<=x<界限范围内返回一个随机数字x
return (int) (Math.random () * limit);
}
}
RunnableDemo由类RunnableDemo,Shape和Rectangle组成。类RunnableDemo通过创建一个Rectangle对象驱动应用程序—通过调用对象的draw()方法—和通过创建第二个什么都不做的Rectangle类。相反,Shape和Rectangle组成了一个基于shape层次的类。Shape是抽象的因为它提供一个抽象的draw()方法。各种shape类,比如Rectangle,扩展Shape和描述它们如何画它们自己的重载draw()。以后,我可能决定引入一些另外的shape类,创建一个Shape数组,通过调用Shape的draw()方法要求每一个Shape元素画它自己。
RunnableDemo 作为一个不带多线程的简单程序产生。后面我决定引入多线程到Rectangle,这样我能够用各种宽度和高度画种种矩形。因为Rectangle扩展Shape (为了以后的多态性原因),我没有其它选择只有让Rectangle实现Runnable。同样,在Rectangle()构造器内,我不得不将一个Rectangle runnable绑定到一个新的Thread对象并调用Thread的start()方法开始一个新的线程调用Rectangle的run()方法画矩形。
因为包括在这篇文章中的RunnableDemo的新输出太长了,我建议你自己编译并运行程序。
技巧
当你面对一个类不是能扩展Thread就是能实现Runnable的情形时,你将选择哪种方法?如果这个类已经扩展了其它类,你必须实现Runnable。然而,如果这个类没有扩展其它类,考虑一下类的名称。名称将暗示这个类的对象不是积极的就是消极的。例如,名称Ticker暗示它的对象是积极的。因此,Ticker类将扩展Thread,并且Ticker对象将被作为专门的Thread对象。相反,Rectangle暗示消极对象—Rectangle对象对于它们自己什么也不做。因此,Rectangle类将实现Runnable,并且Rectangle 对象将使用Thread对象(为了测试或其它意图)代替成为专门的Thread对象。
回顾
用户期望程序达到优异的性能。一种办法是用线程完成那些任务。一个线程是一条程序代码的独立执行通道。线程有益于基于GUI的程序,因为它们允许那些程序当执行其它任务时仍对用户保持响应。另外,带线程的程序比它们没带线程的副本程序完成的快。这对于运行在多处理器机器上的情形尤其明显,在这里每一个线程有它自己的处理器。Thread和Thread子类对象描述了线程并与那些实体相关。对于那些不能扩展Thread的类,你必须创建一个runnable以利用多线程的优势。