[关闭]
@lambeta 2016-08-29T01:43:29.000000Z 字数 13518 阅读 344

第4章

translation


第4章

额外的线程能力

第1到3章介绍了java.lang.Thread类、java.lang.Runnable接口、同步、等待以及通知等内容。在本章中,我会以线程组和线程局部变量来结束有关线程基础知识的讨论。同时,我也会介绍定时器框架以及其如何利用幕后的线程简化了面向定时器的任务。

线程组

查看Thread类的时候,你可能会在构造函数中看到java.lang.ThreadGroup的引用,如:Thread(ThreadGroup group, Runnable target),又或者在static int activeCount()和static int enumerate(Thread[] tarray)方法文档中看到相关描述。

对于ThreadGroup,JDK文档这样阐述:一个线程组代表了一组线程。除此之外,一个线程组也能包含其它的线程组。这些线程组形成一棵树,其中除了初始线程,每个线程组都有一个双亲。

使用ThreadGroup对象,你对其中的所有线程对象进行操作。举个例子:假设一个线程组被变量tg引用,tg.suspend();会暂停线程组内的所有线程。线程组简化了多条线程的管理工作。

尽管线程组看上去很有用,但是有碍于下列原因,你万万不能使用这个类:

不过,鉴于ThreadGroup在处理线程执行中抛出的异常方面做出的贡献,你还是应该对它有所了解。清单4-1示范了异常处理。它展示了一个尝试除0的run()方法,导致抛出java.lang.ArithmeticException。

清单4-1 从run()方法中抛出一个异常

  1. public class ExceptionThread
  2. {
  3. public static void main(String[] args)
  4. {
  5. Runnable r = new Runnable()
  6. {
  7. @Override
  8. public void run()
  9. {
  10. int x = 1 / 0; // Line 10
  11. }
  12. };
  13. Thread thd = new Thread(r);
  14. thd.start();
  15. }
  16. }

默认主线程创建了一个尝试除0而故意抛出ArithmeticException对象的runnable。

按照下面的方式编译清单4-1:

  1. javac ExceptionThread.java

运行结果应用程序如下:

  1. java ExceptionThread

你会看到一条类ArithmeticException的实例被抛出的异常栈信息:

  1. Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
  2. at ExceptionThread$1.run(ExceptionThread.java:10)
  3. at java.lang.Thread.run(Thread.java:745)

一旦run()方法抛出异常,线程就会中止并被下列活动取代:

清单4-2演示了Thread的setUncaughtExceptionHandler()和setDefaultUncaughtExceptionHandler()方法。

清单4-2 未捕获异常处理器示例

  1. public class ExceptionThread
  2. {
  3. public static void main(String[] args)
  4. {
  5. Runnable r = new Runnable()
  6. {
  7. @Override
  8. public void run()
  9. {
  10. int x = 1 / 0;
  11. }
  12. };
  13. Thread thd = new Thread(r);
  14. Thread.UncaughtExceptionHandler uceh;
  15. uceh = new Thread.UncaughtExceptionHandler()
  16. {
  17. @Override
  18. public void uncaughtException(Thread t, Throwable e)
  19. {
  20. System.out.println("Caught throwable " + e +
  21. " for thread " + t);
  22. }
  23. };
  24. thd.setUncaughtExceptionHandler(uceh);
  25. uceh = new Thread.UncaughtExceptionHandler()
  26. {
  27. @Override
  28. public void uncaughtException(Thread t, Throwable e)
  29. {
  30. System.out.println("Default uncaught exception handler");
  31. System.out.println("Caught throwable " + e +
  32. " for thread " + t);
  33. }
  34. };
  35. thd.setDefaultUncaughtExceptionHandler(uceh);
  36. thd.start();
  37. }
  38. }

编译清单4-2 (javac ExceptionThread.java)并运行结果程序(java ExceptionThread)。你应该能观测到这样的输出:

  1. Caught throwable java.lang.ArithmeticException: / by zero for thread
  2. Thread[Thread-0,5,main]

因为默认的处理器并未被调用,所以你看不到它的输出。想要看到那样的输出,你必须注释掉thd.setUncaught ExceptionHandler(uceh);这行。如果你也注释掉了thd.setUncaught ExceptionHandler(uceh);这行,那么就会看到原本清单4-1中的输出。

线程局部变量

有时候,你想要把线程私有数据(如一个用户ID)和线程关联起来。尽管你可以用一个局部变量完成这项任务,那也得它确实存在才行。你可能会使用一个实例属性保存数据,但是这样你又不得不处理同步问题了。万幸的是,Java提供了类java.lang.ThreadLocal作为一个简单(还非常好用)的可选项。

每个ThreadLocal实例描述了一个线程局部变量,它针对每个访问该变量的线程都提供了单独的存储槽。你可以把它想象成每条线程在相同变量中存储不同值、具有多个槽的变量。每条线程只能看到自己的值而不会意识到其它的线程在这个变量中也有属于自己的值。

ThreadLocal被声明成泛型的ThreadLocal< T >,这里的T标识了存储在该变量中值的类型。这个类声明了如下的构造函数和方法:

清单4-3展示了如何使用ThreadLocal把不同的用户ID连接到两条线程上。

清单 4-3 为不同的线程关联不同的用户ID

  1. public class ThreadLocalDemo
  2. {
  3. private static volatile ThreadLocal<String> userID =
  4. new ThreadLocal<String>();
  5. public static void main(String[] args)
  6. {
  7. Runnable r = new Runnable()
  8. {
  9. @Override
  10. public void run()
  11. {
  12. String name = Thread.currentThread().getName();
  13. if (name.equals("A"))
  14. userID.set("foxtrot");
  15. else
  16. userID.set("charlie");
  17. System.out.println(name + " " + userID.get());
  18. }
  19. };
  20. Thread thdA = new Thread(r);
  21. thdA.setName("A");
  22. Thread thdB = new Thread(r);
  23. thdB.setName("B");
  24. thdA.start();
  25. thdB.start();
  26. }
  27. }

初始化ThreadLocal并将引用赋值给一个volatile的类属性userID上(由于这个属性可能会运行在多处理器、多核的机器上,被多条线程访问,所以它是volatile的 —— 也可以指定final来替代)之后,默认的主线程创建了两条线程在userID中存储不同的java.lang.String对象并打印出这些对象。

照下面这样编译清单4-3:

  1. javac ThreadLocalDemo.java

运行结果应用程序如下:

  1. java ThreadLocalDemo

你应该观察到如下的输出(顺序可能有所不同):

  1. A foxtrot
  2. B charlie

存储在线程本地变量中的值都是不相关的。当一个线程的线程被创建出来,它就会获得一个新的包含initialValue()值的存储槽。有时候你或许想要把值从父线程(一个创建其它线程的线程)传给子线程(被创建的线程),借助InheritableThreadLocal就可以完成这项任务。

InheritableThreadLocal是ThreadLocal的子类。除了定义了一个InheritableThreadLocal()构造方法,这个类还声明了下面的protected方法:

清单4-4展示了如何使用InheritableThreadLocal将父线程中的整型对象传给子线程中。

清单 4-4 将一个对象从父线程传到子线程中

  1. public class InheritableThreadLocalDemo
  2. {
  3. private static final InheritableThreadLocal<Integer> intVal =
  4. new InheritableThreadLocal<Integer>();
  5. public static void main(String[] args)
  6. {
  7. Runnable rP = () ->
  8. {
  9. intVal.set(new Integer(10));
  10. Runnable rC = () ->
  11. {
  12. Thread thd = Thread.currentThread();
  13. String name = thd.getName();
  14. System.out.printf("%s %d%n", name,intVal.get());
  15. };
  16. Thread thdChild = new Thread(rC);
  17. thdChild.setName("Child");
  18. thdChild.start();
  19. };
  20. new Thread(rP).start();
  21. }
  22. }

初始化InheritableThreadLocal之后,将其赋值给一个final名为intVal的类属性(我也可以用volatile替代),默认主线程创建一条父线程,它在intVal中存储了一个值为10的java.lang.Integer对象。该父线程会创建一条子线程,这条子线程访问intVal并取得父线程中的Integer对象。

按照下面的样子编译清单4-4:

  1. javac InheritableThreadLocalDemo.java

运行结果应用程序如下:

  1. java InheritableThreadLocalDemo

你应该能够看到如下输出:

  1. Child 10

注意 更多关于ThreadLocal的洞见以及实现方式,请查看Patson Luk《Java ThreadLocal存储趣学指南》的博客(http://java.dzone.com/articles/painless-introduction-javas-threadlocal-storage)。


定时器框架

通常情况下,安排一次性的执行任务或者规律性的重复任务是有必要的。举个例子:你很可能定制了只运行一次的闹钟任务(或许是为了早上叫你起床)或者一个夜间定时的备份任务。不论是哪种类型的任务,你可能都需要它在未来某个指定的时间点或者经过一段初始延时后执行。

你可以使用线程和相关类型构造一个任务调度的框架。不过,Java 1.3已经以java.util.Timer和java.util.TimerTask的形式引入了一个更加便利和简单的替代方案。

Timer让你能够在一个后台线程中调度TimerTasks用于后续执行(以顺序的方式),它也被称为任务执行线程。定时器任务可能会因为一次性的执行或者规律性的重复执行而被调度。

清单4-5展示了一次性执行的定时器任务

清单4-5 一次性执行任务的示例

  1. import java.util.Timer;
  2. import java.util.TimerTask;
  3. public class TimerDemo
  4. {
  5. public static void main(String[] args)
  6. {
  7. TimerTask task = new TimerTask()
  8. {
  9. @Override
  10. public void run()
  11. {
  12. System.out.println("alarm going off");
  13. System.exit(0);
  14. }
  15. };
  16. Timer timer = new Timer();
  17. timer.schedule(task, 2000); // Execute one-shot timer task after 2-second delay.
  18. } }

清单4-5描述了这样一个应用程序,其默认主线程首先初始化了一个TimerTask的匿名子类,由于应用程序直到非守护的任务执行线程终止之后才能终止,所以这个子类被重写的run()方法会在输出一条警告信息后执行System.exit(0);(译者注:终止程序)。之后默认主线程初始化了Timer并用task作为第一个参数来调用其schedule()方法。方法的第二个参数使得这个任务在经过2000毫秒初始延时后执行一次。

照下面的样子编译清单4-5:

  1. javac TimerDemo.java

运行结果应用程序如下:

  1. java TimerDemo

你应该能够观测到近似下面的输出:

  1. alarm going off

清单4-6展示了一个以固定时间间隔重复执行的定时器任务。

清单4-6 以大约一秒钟的间隔不断显示当前系统时间

  1. import java.util.Timer;
  2. import java.util.TimerTask;
  3. public class TimerDemo
  4. {
  5. public static void main(String[] args)
  6. {
  7. TimerTask task = new TimerTask()
  8. {
  9. @Override
  10. public void run()
  11. {
  12. System.out.println(System.currentTimeMillis());
  13. }
  14. };
  15. Timer timer = new Timer();
  16. timer.schedule(task, 0, 1000);
  17. }
  18. }

清单4-6描述了这样一个应用程序,其默认主线程首先初始化了一个TimerTask的匿名子类,该子类被重写的run()方法输出当前的系统时间(以毫秒的方式)。之后默认主线程初始化了Timer并将task作为第一个参数来调用schedule()方法。方法的第二和第三个参数使得这个任务立即开始、每隔1000毫秒执行一次。

编译清单4-6(javac TimerDemo.java)并运行结果应用程序(java TimerDemo)。你应该能观测到裁剪之后的输出:

  1. 1445655847902
  2. 1445655848902
  3. 1445655849902
  4. 1445655850902
  5. 1445655851902
  6. 1445655852902

深入Timer

前面的应用程序以一种非守护任务执行线程的方式运行了它们自己的任务。当然,其中一个任务作为只执行一次,而另一个任务则重复执行。为了理解为何做出这些选择的,你需要更进一步学习Timer。


注意 Timer适用于大规模并发调度定时任务(数以千计的任务不在话下)。在内部,该类使用了一个二进制的堆表示其定时任务队列,使得调度定时任务的开销为O(log n),这里的n就是并发调度定时任务的数量。进一步了解O()表示法,请查看维基页上“大O表示法”的主题(http://en.wikipedia.org/wiki/Big_O_notation)。


Timer声明了下列构造函数:

Timer还声明了下列的方法:

当最后一条指向Timer对象的引用消失并且所有重要的定时任务完成执行之后,该定时器的任务执行线程会优雅地终止(同时被垃圾回收)。不过,这个过程会在不定的时长里发生。(默认情况下,这个任务执行线程不会以守护线程的方式运行,所以它能够阻止应用程序终止。)当应用程序想要快速地终止一个定时器执行线程,它应该调用Timer的cancel()方法。如果这个定时器的任务执行线程异常终止,举个例子,由于其stop()方法被调用过(你绝不应该调用任何线程的stop()方法,因为它们本身就是不安全的),以后任意针对该定时器尝试调度定时任务都会导致抛出IllegalStateException,就如同Timer的cancel()方法已经被调用过一样。

深入TimerTask

定时任务都是抽象类TimerTask子类的实例,这些子类实现了Runnable接口。当子类化TimerTask的时候,你需要重写其void run()方法。


注意 定时任务应该很快完成。如果一个定时任务完成花费了很长的时间,它就会“霸占”定时器的任务执行线程,推迟后续定时任务的执行,这些后续任务可能会“裹成一团”,并且当这个侵入的定时任务最终完成,它们会接连快速地执行。


你也可以从定时任务被重写的run()方法中调用下列方法:

练习

下面的练习被设计来验证你对第4章内容的理解程度:
1. 定义线程组。
2. 为何你要使用线程组?
3. 为何你应该避免使用线程组?
4. 为何你应该认识线程组?
5. 定义线程局部变量。
6. 判断对错:如果线程调用get()方法时,调用线程的存储槽里没有值,该方法就会去调用initialValue()。
7. 你如何从父线程传递一个值到子线程中?
8. 明确组成Timer框架的所有类。
9. 判断对错:Timer()会创建一个新的定时器,其任务执行线程会以守护线程的方式运行。
10. 定义固定延迟的执行任务。
11. 你会调用哪些方法来调度一个固定延迟的执行任务?
12. 定义固定速率的执行任务。
13. Timer的cancel()方法和TimerTask的cancel()方法有何区别?
14. 创建一个BackAndForth应用程序,使用Timer和TimerTask去反复地移动一个星号向前20步,然后向后20步。星号通过System.out.print()输出。

小结

类ThreadGroup描述了一个线程组,它存储了一组线程。线程组通过把方法应用到其中的所有线程简化了多条线程的管理工作。由于多数有用的方法因竞态条件遭到废弃,你不应该再使用线程组。

类ThreadLocal描述了一个线程局部变量,它可以让你把每条线程的数据(如用户ID)和线程关联。线程局部变量针对每个访问该变量的线程都提供了单独的存储槽。你可以把它想象成每条线程可以在其中存储不同值、具有多个槽的同一变量。每条线程只能看到自己的值而不会意识到其它的线程在这个变量中也有属于自己的值。存储在线程局部变量中的值都是不相关的。父线程可以借助类InheritableThreadLocal把值传递到子线程中。

通常情况下,安排一次性的执行任务或者规律性的重复任务是有必要的。Java 1.3引入了定时器框架用于在定时器上下文中控制线程的执行,其由类Timer和TimerTask构成。

第5章会引入并发工具并展示executors。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注