用Java线程获取优异性能(I)——介绍线程、线程类及Runnable
Jeff Friesen 著 刘建华 编译
怎样使用名称
在一个调试会话期间,使用用户友好方式从另一个线程区别其中一个线程证明是有帮助的。要区分其中一个线程,Java给一个线程取一个名称。Thread缺省的名称是一个短线连字符和一个零开始的数字符号。你可以接受Java的缺省线程名称或选择使用你自己的。为了能够自定义名称,Thread提供带有name参数和一个setName(String name)方法的构造器。Thread也提供一个getName()方法返回当前名称。表2显示了怎样通过Thread(String name)创建一个自定义名称和通过在run()方法中调用getName()检索当前名称:
表2.NameThatThread.java
// NameThatThread.java
class NameThatThread
{
public static void main (String [] args)
{
MyThread mt;
if (args.length == 0)
mt = new MyThread ();
else
mt = new MyThread (args [0]);
mt.start ();
}
}
class MyThread extends Thread
{
MyThread ()
{
//编译器创建等价于super()的字节代码
}
MyThread (String name)
{
super (name); //将名称传递给Thread超类
}
public void run ()
{
System.out.println ("My name is: " + getName ());
}
}
你能够在命令行向MyThread传递一个可选的name参数。例如,java NameThatThread X 建立X作为线程的名称。如果你指定一个名称失败,你将看到下面的输出:
My name is: Thread-1
如果你喜欢,你能够在MyThread(String name)构造器中将super(name)调用改变成setName(String name)调用——作为setName(name)后一种方法调用达到同样建立线程名称的目的——作为super(name)我作为练习保留给你们。
注意
Java主要将名称指派给运行main() 方法的线程,开始线程。你特别要看看当开始线程掷出一个例外对象时在线程“main”的例外显示的JVM的缺省例外处理打印消息。
休眠或停止休眠
在这一栏后面,我将向你介绍动画——在一个表面上重复画图形,这稍微不同于完成一个运动画面。要完成动画,一个线程必须在它显示两个连续画面时中止。调用Thread的静态sleep(long millis)方法强迫一个线程中止millis毫秒。另一个线程可能中断正在休眠的线程。如果这种事发生,正在休眠的线程将醒来并从sleep(long millis)方法掷出一个InterruptedException对象。结果,调用sleep(long millis)的代码必须在一个try代码块中出现——或代码方法必须在自己的throws子句中包括InterruptedException。
为了示范sleep(long millis),我写了一个CalcPI1应用程序。这个应用程序开始了一个新线程便于用一个数学运算法则计算数学常量pi的值。当新线程计算时,开始线程通过调用sleep(long millis)中止10毫秒。在开始线程醒后,它将打印pi的值,其中新线程存贮在变量pi中。表3给出了CalcPI1的源代码:
表3. CalcPI1.java
// CalcPI1.java
class CalcPI1
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
try
{
Thread.sleep (10); //休眠10毫秒
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi; //缺省初始化为0.0
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
如果你运行这个程序,你将看到输出如下(但也可能不一样):
pi = -0.2146197014017295
完成计算PI
为什么输出不正确呢?毕竟,pi的值应近似等于3.14159。回答是:开始线程醒得太快了。在新线程刚开始计算pi时,开始线程就醒过来读取pi的当前值并打印其值。我们可以通过将10毫秒延迟增加为更长的值来进行补偿。这一更长的值(不幸的是它是依赖于平台的)将给新线程一个机会在开始线程醒过来之前完成计算。(后面,你将学到一种不依赖平台的技术,它将防止开始线程醒来直到新线程完成。)
注意
线程同时提供一个sleep(long millis, int nanos)方法,它将线程休眠millis 毫秒和nanos 纳秒。因为多数基于JVM的平台都不支持纳秒级的分解度,JVM 线程处理代码将纳秒数字四舍五入成毫秒数字的近似值。如果一个平台不支持毫秒级的分解度,JVM 线程处理代码将毫秒数字四舍五入成平台支持的最小级分解度的近似倍数。
它是死的还是活的?
当一个程序调用Thread的start()方法时,在一个新线程调用run()之前有一个时间段(为了初始化)。run()返回后,在JVM清除线程之前有一段时间通过。JVM认为线程立即激活优先于线程调用run(),在线程执行run()期间和run()返回后。在这时间间隔期间,Thread的isAlive()方法返回一个布尔真值。否则,方法返回一个假值。
isAlive()在一个线程需要在第一个线程能够检查其它线程的结果之前等待另一个线程完成其run()方法的情形下证明是有帮助的。实质上,那些需要等待的线程输入一个while循环。当isAlive()为其它线程返回真值时,等待线程调用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪费更多的CPU循环)。一旦isAlive()返回假值,等待线程便检查其它线程的结果。
你将在哪里使用这样的技术呢?对于起动器,一个CalcPI1的修改版本怎么样,在打印pi的值前开始线程在哪里等待新线程的完成?表4的CalcPI2源代码示范了这一技术:
表4. CalcPI2.java
// CalcPI2.java
class CalcPI2
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
while (mt.isAlive ())
try
{
Thread.sleep (10); //休眠10毫秒
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi; //缺省初始化成0.0
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
CalcPI2的开始线程在10毫秒时间间隔休眠,直到mt.isAlive ()返回假值。当那些发生时,开始线程从它的while循环中退出并打印pi的内容。如果你运行这个程序,你将看到如下的输出(但不一定一样):
完成计算PI
pi = 3.1415726535897894
这不,现在看上去更精确了?
注意
一个线程可能对它自己调用isAlive() 方法。然而,这毫无意义,因为isAlive()将一直返回真值。
合力
因为while循环/isAlive()方法/sleep()方法技术证明是有用的,Sun将其打包进三个方法组成的一个组合里:join(),join(long millis)和join(long millis, int nanos)。当当前线程想等待其它线程结束时,经由另一个线程的线程对象引用调用join()。相反,当它想其中任意线程等待其它线程结束或等待直到millis毫秒和nanos纳秒组合通过时,当前线程调用join(long millis)或join(long millis, int nanos)。(作为sleep()方法,JVM 线程处理代码将对join(long millis)和join(long millis,int nanos)方法的参数值四舍五入。)表5的CalcPI3源代码示范了一个对join()的调用:
表5. CalcPI3.java
// CalcPI3.java
class CalcPI3
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
try
{
mt.join ();
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi; //缺省初始化成0.0
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
CalcPI3的开始线程等待与MyThread对象有关被mt引用的线程结束。接着开始线程打印pi的值,其值与CalcPI2的输出一样。
警告
不要试图将当前线程与其自身连接,因为这样当前线程将要永远等待。