用Java线程获取优异性能(I)——介绍线程、线程类及Runnable

来源:互联网 发布:linux的tmp文件夹 编辑:程序博客网 时间:2024/04/28 04:43

 用Java线程获取优异性能(I)——介绍线程、线程类及Runnable

用Java线程获取优异性能(I) 摘要 用户期望程序能展现优异的性能。为了满足这个期望,你的程序常常使用到线程。在这篇文章中我们开始练习使用线程。你将学习到线程、线程类及Runnable。 用户不喜欢反应迟钝的软件。当用户单击一个鼠标时,他们希望程序立即回应他们的请求,即使程序正处于费时的运行之中,比如为一篇很长的文档重编页码或等待一个网络操作的完成。对用户响应很慢的程序其性能拙劣。为提高程序性能,开发者一般使用线程。 这篇文章是探索线程的第一部份。虽然你可能认为线程是一种难于掌握的事物,但我打算向你显示线程是易于理解的。在这篇文章中,我将向你介绍线程和线程类,以及讨论Runnable。此外,在后面的文章中,我将探索同步(通过锁),同步的问题(比如死锁),等待/通知机制,时序安排(有优先权和没有优先权),线程中断,计时器,挥发性,线程组和线程本地变量。 阅读关于线程设计的整个系列: ·第1部份:介绍线程和线程类,以及Runnable ·第2部份:使用同步使线程串行化访问关键代码部份 注意 这篇文章及其应用程序的三个相关线程练习与applets不同。然而,我在应用程序中介绍的多数应用到applets。主要不同的是:为了安全的原因,不是所有的线程操作都可以放到一个applet中(我将在以后的文章中讨论applets)。 什么是线程? 线程的概念并不难于掌握:它是程序代码的一个独立的执行通道。当多个线程执行时,经由相同代码的一个线程的通道通常与其它的不同。例如,假设一个线程执行一段相当于一个if-else语句的if部分的字节代码时,而另一个线程正执行相当于else部分的字节代码。JVM怎样保持对于每一个线程执行的跟踪呢?JVM给每一个线程它自己的方法调用堆栈。另外跟踪当前指令字节代码,方法堆栈跟踪本地变量,JVM传递给一个方法的参数,以及方法的返回值。 当多个线程在同一个程序中执行字节代码序列时,这种行为叫作多线程。多线程在多方面有利于程序: ·当执行其它任务时多线程GUI(图形用户界面)程序仍能保持对用户的响应,比如重编页码或打印一个文档。 ·带线程的程序一般比它们没有带线程的副本程序完成得快。这尤其表现在线程运行在一个多处理器机器上,在这里每一个线程都有它自己的处理器。 Java通过java.lang.Thread类完成多线程。每一个线程对象描述一个单独的执行线程。那些运行发生在线程的run()方法中。因为缺省的run()方法什么都不做,你必须创建Thread子类并重载run()以完成有用的工作。练习列表1中领略一个在Thread中的线程及多线程: 列表1. ThreadDemo.java // ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('/n'); } } } 列表1显示了一个由类ThreadDemo和MyThread组成的应用程序的源代码。类ThreadDemo通过创建一个MyThread对象驱动应用程序,开始一个与其对象相关的线程并执行一段打印一个正方形表的代码。相反, MyThread重载Thread的run()方法打印(通过标准输入流)一个由星形符号组成的直角三角形。 当你键入java ThreadDemo运行应用程序时, JVM创建一个运行main()方法的开始线程。通过执行mt.start (),开始线程告诉JVM创建一个执行包含MyThread对象的run()方法的字节代码指令的第二个线程。当start()方法返回时,开始线程循环执行打印一个正方形表,此时另一个新线程执行run()方法打印直角三角形。 输出会象什么样呢?运行ThreadDemo就可以看到。你将注意到每一个线程的输出与其它线程的输出相互交替。这样的结果是因为两个线程将它们的输出都发送到了同样的标准输出流。 注意 多数(不是所有)JVM设备使用下层平台的线程性能。因为那些性能是平台特有的,你的多线程程序的输出顺序可能与一些人的其他输出的顺序不一样。这种不同是由于时序的安排,我将在这一系列的稍后探讨这一话题。 线程类 要精通写多线程代码,你必须首先理解创建Thread类的多种方法。这部份将探讨这些方法。明确地说,你将学到开始线程的方法,命名线程,使线程休眠,决定一个线程是否激活,将一个线程与另一个线程相联,和在当前线程的线程组及子组中列举所有激活的线程。我也会讨论线程调试辅助程序及用户线程与监督线程的对比。 我将在以后的文章中介绍线程方法的余下部份,Sun不赞成的方法除外。 警告 Sun有一些不赞成的线程方法种类,比如suspend()和resume(),因为它们能锁住你的程序或破坏对象。所以,你不必在你的代码中调用它们。考虑到针对这些方法工作区的SDK文件,在这篇文章中我没有包含这些方法。 构造线程 Thread有八个构造器。最简单的是: ·Thread(),用缺省名称创建一个Thread对象 ·Thread(String name),用指定的name参数的名称创建一个Thread对象 下一个最简单的构造器是Thread(Runnable target)和Thread(Runnable target, String name)。 除Runnable参数之外,这些构造器与前述的构造器一样。不同的是:Runnable参数识别提供run()方法的线程之外的对象。(你将在这篇文章稍后学到Runnable。)最后几个构造器是Thread(String name),Thread(Runnable target),和Thread(Runnable target, String name)。然而,最后的构造器包含了一个为了组织意图的ThreadGroup参数。 最后四个构造器之一,Thread(ThreadGroup group, Runnable target, String name, long stackSize),令人感兴趣的是它能够让你指定想要的线程方法调用堆栈的大小。能够指定大小将证明在使用递归方法(一种为何一个方法不断重复调用自身的技术)优美地解决一些问题的程序中是十分有帮助的。通过明确地设置堆栈大小,你有时能够预防StackOverflowErrors。然而,太大将导致OutOfMemoryErrors。同样,Sun将方法调用堆栈的大小看作平台依赖。依赖平台,方法调用堆栈的大小可能改变。因此,在写调用Thread(ThreadGroup group, Runnable target, String name, long stackSize)代码前仔细考虑你的程序分枝。 开始你的运载工具 线程类似于运载工具:它们将程序从开始移动到结束。Thread 和Thread子类对象不是线程。它们描述一个线程的属性,比如名称和包含线程执行的代码(经由一个run()方法)。当一个新线程执行run()时,另一个线程正调用Thread或其子类对象的start()方法。例如,要开始第二个线程,应用程序的开始线程—它执行main()—调用start()。作为响应,JVM和平台一起工作的线程操作代码确保线程正确地初始化并调用Thread或其子类对象的run()方法。 一旦start()完成,多重线程便运行。因为我们趋向于在一种线性的方式中思维,我们常发现当两个或更多线程正运行时理解并发(同时)行为是困难的。因此,你应该看看显示与时间对比一个线程正在哪里执行(它的位置)的图表。下图就是这样一个图表。 与时间对比一个开始线程和一个新建线程执行位置的行为 图表显示了几个重要的时间段: ·开始线程的初始化 ·线程开始执行main()瞬间 ·线程开始执行start()的瞬间 ·start()创建一个新线程并返回main()的瞬间 ·新线程的初始化 ·新线程开始执行run()的瞬间 ·每个线程结束的不同瞬间 注意新线程的初始化,它对run()的执行,和它的结束都与开始线程的执行同时发生。 警告 一个线程调用start()后,在run()方法退出前并发调用那方法将导致start()掷出一个java.lang.IllegalThreadStateException对象。 怎样使用名称 在一个调试会话期间,使用用户友好方式从另一个线程区别其中一个线程证明是有帮助的。要区分其中一个线程,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的输出一样。 警告 不要试图将当前线程与其自身连接,因为这样当前线程将要永远等待。 怎样使用名称 在一个调试会话期间,使用用户友好方式从另一个线程区别其中一个线程证明是有帮助的。要区分其中一个线程,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的输出一样。 警告 不要试图将当前线程与其自身连接,因为这样当前线程将要永远等待。 查询活跃线程 在有些情形下,你可能想了解在你的程序中哪些线程是激活的。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以利用多线程的优势。

原创粉丝点击