并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(子任务)中的每一个都将由执行线程 来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的 CPU 一样,其底层机制是切分 CPU 时间 ,通常不需要考虑。
线程模型为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理。
在使用线程时,CPU 将轮流给每个任务分配其占用时间(根据系统并发模型不同而不同),每个任务都觉得自己一直在占用 CPU,但事实上 CPU 时间是划分成片段分配给了所有的任务(例外情况是程序确实运行在多个 CPU 上)。线程的一大好处是可以使你从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个 CPU 的机器上。所以使用线程机制是一种建立透明的、可扩展的程序的方法,如果程序运行得太慢,为机器增添一个 CPU 就能很容易地加快程序的运行素的。多任务和多线程往往是使用多处理器系统的最合理的方式。
补充:Windows 使用时间切片机制,因此其 CPU 将轮流给每个任务分配其占用时间。而 Solaris 使用了 FIFO 并发模型,除非有高优先级的线程被唤醒,否则当前线程将一直运行,直至它被阻塞或终止。这意味着具有相同优先级的其他线程在当前线程放弃处理器之前,将不会运行。
一、定义任务 线程可以驱动任务,可以由 Runnable
接口来提供描述任务的方式。要想定义任务,只需要实现 Runnable
接口并重写 run()
方法,使得该任务可以执行你的命令。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class LiftOff implements Runnable { protected int countDown = 10 ; private static int taskCount = 0 ; private final int id = taskCount++; public LiftOff () { } public LiftOff (int countDown) { this .countDown = countDown; } public String status () { return "#" + id + " (" + (countDown > 0 ? countDown : "liftoff!" ) + "), " ; } @Override public void run () { while (countDown-- > 0 ) { System.out.print(status()); Thread.yield (); } } public static void main (String[] args) { LiftOff liftOff = new LiftOff (); liftOff.run(); } }
输出如下:
1 #0 (9), #0 (8), #0 (7), #0 (6), #0 (5), #0 (4), #0 (3), #0 (2), #0 (1), #0 (liftoff!),
上面代码中的 run()
方法中有对静态方法 Thread.yield()
的调用,其是对线程调度器 (Java 线程机制的一部分,可以将 CPU 从一个线程转移给另一个线程)的一种建议,其在声明:“我已经执行完声明周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机”。这是选择性的,这里使用是因为可能会看到任务换进换出的证据。
二、Thread 类 将 Runnable 对象转变为工作任务的传统方式是把它提交给一个 Thread
构造器,下面的示例展示了如何使用 Thread
来驱动 LiftOff
对象:
1 2 3 4 5 6 7 class BasicThreads { public static void main (String[] args) { Thread thread = new Thread (new LiftOff ()); thread.start(); System.out.println("waiting for LiftOff" ); } }
输出如下:
1 2 waiting for LiftOff #0 (9), #0 (8), #0 (7), #0 (6), #0 (5), #0 (4), #0 (3), #0 (2), #0 (1), #0 (liftoff!),
调用 Thread
对象的 start()
方法为该线程执行必须的初始化操作,然后调用 Runnable
的 run()
方法,以便在这个新线程中启动该任务。上面输出中需要注意的一个点是:”waiting for LiftOff“在 LiftOff
之前输出了。实际上,start
方法产生的是对 LiftOff.run()
的方法调用,并且这个方法还没有完成,但是因为 LiftOff.run()
是由不同的线程执行的,因此仍旧可以执行 main 线程中的其他操作(并不局限于 main 线程,任何线程都可以启动另一个线程)。
下面的程序中,添加了更多的线程去驱动更多的任务,可以看到所有任务彼此之间是如何互相呼应的:
1 2 3 4 5 6 7 8 class MoreBasicThreads { public static void main (String[] args) { for (int i = 0 ; i < 5 ; i++) { new Thread (new LiftOff ()).start(); } System.out.println("Waiting for LiftOff" ); } }
输出如下:
1 2 Waiting for LiftOff #3 (9), #3 (8), #4 (9), #0 (9), #1 (9), #3 (7), #2 (9), #4 (8), #3 (6), #1 (8), #3 (5), #3 (4), #3 (3), #0 (8), #0 (7), #0 (6), #2 (8), #4 (7), #0 (5), #2 (7), #0 (4), #3 (2), #0 (3), #2 (6), #1 (7), #1 (6), #4 (6), #1 (5), #2 (5), #0 (2), #0 (1), #3 (1), #0 (liftoff!), #3 (liftoff!), #2 (4), #2 (3), #2 (2), #2 (1), #1 (4), #4 (5), #1 (3), #4 (4), #4 (3), #2 (liftoff!), #4 (2), #1 (2), #4 (1), #4 (liftoff!), #1 (1), #1 (liftoff!),
上面的输出说明,不同任务的执行在线程被换进换出时混在一起了,这种交换是由线程调度器自动控制的。如果机器有多个处理器,线程调度器将会在这些处理器之间默默地分发线程。(某些较早的 Java 来说,情况并非如此)。
上面 MoreBasicThreads
类中,是由 main 来创建 LiftOff
线程的,如果多个线程在创建 LiftOff
线程,那么就有可能会有多个 LiftOff
拥有相同的 id。
这个程序一次运行的结果可能与另一次运行的结果不同,因为线程调度机制是非确定性的。多个 JDK 版本之间这个简单的输出是会有巨大差异的。较早的 JDK 不会频繁地对时间切片,因此线程 一可能会首先循环到尽头,然后线程二会经历其所有的循环。较晚的 JDK 会产生更好的时间切片行为。
三、使用 Executor Java SE5 的 java.lang.concurrent
包中的执行器 Executor
将为你管理 Thread
对象,从而简化了并发编程。其在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务。Executor
允许你管理异步任务的执行,而无需显式地管理线程的生命周期。Executor
在 Java SE5 或 6 中是启动任务的优选方法。
1 2 3 4 5 6 7 8 9 10 11 class CachedThreadPool { public static void main (String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0 ; i < 5 ; i++) { executorService.execute(new LiftOff ()); } executorService.shutdown(); executorService.execute(new LiftOff ()); System.out.println("Waiting for LiftOff" ); } }
输出如下:
1 2 Waiting for LiftOff #4 (9), #4 (8), #2 (9), #2 (8), #2 (7), #2 (6), #2 (5), #2 (4), #2 (3), #2 (2), #2 (1), #2 (liftoff!), #1 (9), #1 (8), #1 (7), #3 (9), #3 (8), #3 (7), #3 (6), #3 (5), #3 (4), #0 (9), #0 (8), #0 (7), #0 (6), #0 (5), #0 (4), #4 (7), #4 (6), #4 (5), #4 (4), #4 (3), #1 (6), #1 (5), #3 (3), #3 (2), #1 (4), #1 (3), #4 (2), #4 (1), #4 (liftoff!), #3 (1), #0 (3), #1 (2), #3 (liftoff!), #1 (1), #0 (2), #0 (1), #1 (liftoff!), #0 (liftoff!),
常见的情况是,单个的 Executor 被用来创建和管理系统中所有的任务 。
对 shutdown
方法的调用可以防止新任务被调教给这个 Executor
。此示例中,main 线程将继续运行在 shutdown
被调用之前提交的所有任务。这个程序将在 Executor
中的所有任务完成之后尽快退出。
在 shutdown
方法执行后启用新线程:
1 2 executorService.shutdown(); executorService.execute(new LiftOff ());
将有如下异常:
1 2 3 4 5 6 Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task cn.z2huo.knowledge.concurrent.section_21_2.LiftOff@13221655 rejected from java.util.concurrent.ThreadPoolExecutor@2f2c9b19[Shutting down, pool size = 5 , active threads = 5 , queued tasks = 0 , completed tasks = 0 ] at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2065 ) at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833 ) at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365 ) at cn.z2huo.knowledge.concurrent.section_21_2.CachedThreadPool.main(CachedThreadPool.java:13 ) #3 (9 ), #0 (9 ), #1 (9 ), #1 (8 ), #0 (8 ), #2 (9 ), #3 (8 ), #4 (9 ), #3 (7 ), #2 (8 ), #1 (7 ), #3 (6 ), #4 (8 ), #1 (6 ), #0 (7 ), #2 (7 ), #4 (7 ), #3 (5 ), #2 (6 ), #4 (6 ), #0 (6 ), #2 (5 ), #1 (5 ), #0 (5 ), #2 (4 ), #0 (4 ), #4 (5 ), #3 (4 ), #3 (3 ), #3 (2 ), #3 (1 ), #3 (liftoff!), #0 (3 ), #2 (3 ), #1 (4 ), #2 (2 ), #2 (1 ), #0 (2 ), #0 (1 ), #0 (liftoff!), #4 (4 ), #2 (liftoff!), #1 (3 ), #4 (3 ), #1 (2 ), #4 (2 ), #1 (1 ), #4 (1 ), #1 (liftoff!), #4 (liftoff!),
上面的输出中,可以看到没有“waiting for LiftOff”的输出,因为 main 线程在 shutdown
执行后再启用新线程时发生了异常,发生了异常。
替换不同类型的 Executor
:
1 2 3 4 5 6 7 8 9 10 11 class FixedThreadPoll { public static void main (String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5 ); for (int i = 0 ; i < 5 ; i++) { executorService.execute(new LiftOff ()); } executorService.shutdown(); executorService.execute(new LiftOff ()); System.out.println("Waiting for LiftOff" ); } }
输出如下:
1 2 3 4 5 6 Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task cn.z2huo.knowledge.concurrent.section_21_2.LiftOff@19469ea2 rejected from java.util.concurrent.ThreadPoolExecutor@13221655 [Shutting down, pool size = 5 , active threads = 5 , queued tasks = 0 , completed tasks = 0 ] at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2065 ) at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833 ) at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365 ) at cn.z2huo.knowledge.concurrent.section_21_2.FixedThreadPoll.main(FixedThreadPoll.java:13 ) #1 (9 ), #1 (8 ), #3 (9 ), #1 (7 ), #3 (8 ), #4 (9 ), #1 (6 ), #0 (9 ), #2 (9 ), #1 (5 ), #3 (7 ), #2 (8 ), #1 (4 ), #0 (8 ), #3 (6 ), #4 (8 ), #4 (7 ), #1 (3 ), #4 (6 ), #2 (7 ), #1 (2 ), #4 (5 ), #4 (4 ), #0 (7 ), #0 (6 ), #0 (5 ), #1 (1 ), #1 (liftoff!), #3 (5 ), #2 (6 ), #2 (5 ), #3 (4 ), #3 (3 ), #0 (4 ), #4 (3 ), #3 (2 ), #2 (4 ), #0 (3 ), #4 (2 ), #3 (1 ), #2 (3 ), #4 (1 ), #3 (liftoff!), #0 (2 ), #2 (2 ), #4 (liftoff!), #0 (1 ), #2 (1 ), #0 (liftoff!), #2 (liftoff!),
通过 FixedThreadPool
,可以一次性预先执行代价高昂的线程分配,同时可以限制线程的数量。这可以节省时间,因为不需要为每个任务都固定地付出创建线程的开销。在事件驱动的系统中,需要线程的事件处理器,通过直接从池中获取线程,也可以如愿地尽快得到服务。
在任何线程池中,现有线程在可能的情况下,都会被自动复用 。
CachedThreadPool
在程序执行过程中,通常会创建与所需数量相同的线程,然后在它回收旧线程时,停止创建新线程,因此它是合理的 Executor
的首选。只有当这种方式会引发问题时,才需要切换到 FixedThreadPool
。
而 SingleThreadExecutor
就像是线程数量为 1 的 FixedThreadPool
。这对于你希望在另一个线程中连续运行的任何事物(长期存活的任务)来说,都是很有用的。例如监听进入的套接字连接任务。它对于希望在线程中运行的短任务也同样很方便。例如,更新本地或远程日志的小任务,或者是事件分发线程。
如果向 SingleThreadExecutor
提交多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务都将使用相同的线程。示例如下:
1 2 3 4 5 6 7 8 9 class SingleThreadExecutor { public static void main (String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0 ; i < 5 ; i++) { executorService.execute(new LiftOff ()); } executorService.shutdown(); } }
输出如下:
1 #0 (9), #0 (8), #0 (7), #0 (6), #0 (5), #0 (4), #0 (3), #0 (2), #0 (1), #0 (liftoff!), #1 (9), #1 (8), #1 (7), #1 (6), #1 (5), #1 (4), #1 (3), #1 (2), #1 (1), #1 (liftoff!), #2 (9), #2 (8), #2 (7), #2 (6), #2 (5), #2 (4), #2 (3), #2 (2), #2 (1), #2 (liftoff!), #3 (9), #3 (8), #3 (7), #3 (6), #3 (5), #3 (4), #3 (3), #3 (2), #3 (1), #3 (liftoff!), #4 (9), #4 (8), #4 (7), #4 (6), #4 (5), #4 (4), #4 (3), #4 (2), #4 (1), #4 (liftoff!),
从输出中可以看到,可以看到每个任务都是按照它们被提交的顺序,并且在下一个任务开始之前完成的。
使用 SingleThreadExecutor
可以让你省去只是为了维持某些事物的原型而进行的各种协调努力,但有时更好的解决方案是在资源上同步。
四、从任务中产生返回值 Runnable
是执行工作的独立任务,但是它不返回任何值。如果希望任务在完成时更够返回一个值,可以实现 Callable
接口。在 Java SE5 中引入的 Callable
是一种具有类型参数的泛型,它的类型参数表示的是从方法 call()
中返回的值,并且必须使用 ExecutorService.submit()
方法调用它,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class TaskWithResult implements Callable <String> { private final int id; public TaskWithResult (int id) { this .id = id; } @Override public String call () throws Exception { return "result of TaskWithResult " + id; } }
调用上面任务的程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class CallableDemo { public static void main (String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); List<Future<String>> results = new ArrayList <>(); for (int i = 0 ; i < 10 ; i++) { results.add(executorService.submit(new TaskWithResult (i))); } for (Future<String> result : results) { try { System.out.println(result.get()); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException (e); } finally { executorService.shutdown(); } } } }
输出如下:
1 2 3 4 5 6 7 8 9 10 result of TaskWithResult 0 result of TaskWithResult 1 result of TaskWithResult 2 result of TaskWithResult 3 result of TaskWithResult 4 result of TaskWithResult 5 result of TaskWithResult 6 result of TaskWithResult 7 result of TaskWithResult 8 result of TaskWithResult 9
submit()
方法会产生 Future
对象,它用 Callable
返回结果的特定类型进行了参数化。可以用 isDone()
方法来查询 Future
是否已经完成。当任务完成时,它具有一个结果,可以调用 get()
方法来获取该结果。可以不用 isDone()
进行检测就直接调用 get()
,这时,get()
将阻塞,直至结果准备就绪。你还可以在试图调用 get()
来获取结果之前,先调用具有超时的 get()
或 isDone()
来查看任务是否完成。
上面有一个程序需要注意的点是:finally 块中的 executorService.shutdown();
方法的调用,针对这个方法的调用在 for 循环里面,get()
方法会阻塞,也就是需要从 get 方法中获取到线程返回值之后,关闭,这个关闭方法是只关闭之前提交的任务。
五、休眠 影响任务行为的一种简单方法是调用 sleep()
,这将使任务终止执行给定的时间。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class SleepingTask extends LiftOff { @Override public void run () { try { while (countDown-- > 0 ) { System.out.print(status()); TimeUnit.SECONDS.sleep(5 ); } } catch (InterruptedException e) { System.err.println("Interrupted" ); } } public static void main (String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0 ; i < 5 ; i++) { executorService.execute(new SleepingTask ()); } executorService.shutdown(); } }
输出结果如下:
1 #0 (9), #2 (9), #3 (9), #1 (9), #4 (9), #0 (8), #4 (8), #3 (8), #2 (8), #1 (8), #0 (7), #4 (7), #2 (7), #3 (7), #1 (7), #3 (6), #1 (6), #0 (6), #2 (6), #4 (6), #2 (5), #0 (5), #3 (5), #1 (5), #4 (5), #2 (4), #0 (4), #3 (4), #4 (4), #1 (4), #3 (3), #1 (3), #0 (3), #4 (3), #2 (3), #1 (2), #0 (2), #2 (2), #4 (2), #3 (2), #0 (1), #4 (1), #3 (1), #2 (1), #1 (1), #4 (liftoff!), #2 (liftoff!), #3 (liftoff!), #1 (liftoff!), #0 (liftoff!),
在每个线程调用完成一次 status()
方法之后,都会休眠一段时间,等休眠时间过去之后,控制台会继续输出。
对 sleep
的调用会产生异常,该异常需要在 run
方法内部捕获。因为异常不能跨线程传播回 main 方法,所以必须在本地处理所有在任务内部产生的异常。
这里的输出跟书里面的输出顺序是不同的,书中每一次的输出,都是按照 id 从 0 到 4 来打印到控制台的。而上面代码的输出中不是按顺序的。说是这取决于平台,每个打印语句之后,每个任务都将要睡眠(阻塞),这使得线程调度器可以切换到另一个线程,进而驱动另一个任务。顺序行为依赖于底层的线程机制,这种机制在不同的操作系统之间是有差异的。
六、优先级 线程的优先级将该线程的重要性传递给了调度器。尽管 CPU 处理现有线程的顺序是不确定的,但是调度器将倾向于让优先级最高的线程先执行,但这并不意味着优先级较低的线程将得不到执行(优先级不会导致死锁),只是执行的频率较低。
绝大多数时间里,所有线程都应该以默认的优先级运行。试图操纵线程优先级通常是一种错误 。
程序示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class SimplePriorities implements Runnable { private int countDown = 5 ; private volatile double d; private final int priority; public SimplePriorities (int priority) { this .priority = priority; } @Override public String toString () { return Thread.currentThread() + ": " + countDown; } @Override public void run () { Thread.currentThread().setPriority(priority); while (true ) { for (int i = 1 ; i < 100000 ; i++) { d += (Math.PI + Math.E) / (double ) i; if (i % 1000 == 0 ) { Thread.yield (); } } System.out.println(this ); if (--countDown == 0 ) { return ; } } } public static void main (String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0 ; i < 5 ; i++) { executorService.execute(new SimplePriorities (Thread.MIN_PRIORITY)); } executorService.execute(new SimplePriorities (Thread.MAX_PRIORITY)); executorService.shutdown(); } }
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Thread[pool-1-thread-3,1,main]: 5 Thread[pool-1-thread-6,10,main]: 5 Thread[pool-1-thread-1,1,main]: 5 Thread[pool-1-thread-5,1,main]: 5 Thread[pool-1-thread-2,1,main]: 5 Thread[pool-1-thread-4,1,main]: 5 Thread[pool-1-thread-6,10,main]: 4 Thread[pool-1-thread-6,10,main]: 3 Thread[pool-1-thread-5,1,main]: 4 Thread[pool-1-thread-2,1,main]: 4 Thread[pool-1-thread-3,1,main]: 4 Thread[pool-1-thread-1,1,main]: 4 Thread[pool-1-thread-4,1,main]: 4 Thread[pool-1-thread-3,1,main]: 3 Thread[pool-1-thread-2,1,main]: 3 Thread[pool-1-thread-6,10,main]: 2 Thread[pool-1-thread-1,1,main]: 3 Thread[pool-1-thread-5,1,main]: 3 Thread[pool-1-thread-4,1,main]: 3 Thread[pool-1-thread-2,1,main]: 2 Thread[pool-1-thread-6,10,main]: 1 Thread[pool-1-thread-3,1,main]: 2 Thread[pool-1-thread-5,1,main]: 2 Thread[pool-1-thread-1,1,main]: 2 Thread[pool-1-thread-4,1,main]: 2 Thread[pool-1-thread-2,1,main]: 1 Thread[pool-1-thread-3,1,main]: 1 Thread[pool-1-thread-5,1,main]: 1 Thread[pool-1-thread-1,1,main]: 1 Thread[pool-1-thread-4,1,main]: 1
上面的程序展示出的点:
可以在一个任务的内部,通过调用Thread.currentThread()
来获得对驱动该任务的 Thread
对象的引用。
Thread.toString()
方法会打印出该线程的名称,线程的优先级以及线程所属的线程组。
优先级是在 run
方法的开头部分设定的,在构造器中设置线程优先级没有任何好处,因为 Executor
在此刻还没有开始执行任务。
变量 d 被设置为volatile
,以努力确保不进行任何编译器优化
书中是这样说的:
在 run
方法里,执行了十万次的开销相当大的浮点运算。如果没有加入这个运算,就看不到设置优先级的效果。有了这些运算,就能观潮到优先级为MAX_PRIORITY
的线程被调度器优先选择(至少我的 windows xp 机器是这样的)。尽管向控制台打印也是开销较大的操作,但在那种情况下看不出优先级效果,因为向控制台打印不能被中断,而数学运算是可以中断的。这里运算时间足够长,因此线程调度机制才来得及介入,交换任务并关注优先级,使得最高优先级线程被优先选择 。
但是从上面的输出结果中,可以看到,线程 6 并没有优先被执行完成,根据书中描述,xp 电脑的时代,cpu 性能不够强,因此增大运算次数从十万到十亿,输出如下,跟书中想要的顺序相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Thread[pool-1-thread-6,10,main]: 5 Thread[pool-1-thread-4,1,main]: 5 Thread[pool-1-thread-1,1,main]: 5 Thread[pool-1-thread-5,1,main]: 5 Thread[pool-1-thread-2,1,main]: 5 Thread[pool-1-thread-3,1,main]: 5 Thread[pool-1-thread-6,10,main]: 4 Thread[pool-1-thread-1,1,main]: 4 Thread[pool-1-thread-5,1,main]: 4 Thread[pool-1-thread-4,1,main]: 4 Thread[pool-1-thread-2,1,main]: 4 Thread[pool-1-thread-3,1,main]: 4 Thread[pool-1-thread-6,10,main]: 3 Thread[pool-1-thread-1,1,main]: 3 Thread[pool-1-thread-4,1,main]: 3 Thread[pool-1-thread-5,1,main]: 3 Thread[pool-1-thread-2,1,main]: 3 Thread[pool-1-thread-3,1,main]: 3 Thread[pool-1-thread-6,10,main]: 2 Thread[pool-1-thread-1,1,main]: 2 Thread[pool-1-thread-4,1,main]: 2 Thread[pool-1-thread-5,1,main]: 2 Thread[pool-1-thread-2,1,main]: 2 Thread[pool-1-thread-3,1,main]: 2 Thread[pool-1-thread-6,10,main]: 1 Thread[pool-1-thread-1,1,main]: 1 Thread[pool-1-thread-5,1,main]: 1 Thread[pool-1-thread-4,1,main]: 1 Thread[pool-1-thread-2,1,main]: 1 Thread[pool-1-thread-3,1,main]: 1
还需要注意一个优先级的问题,JDK 有 10 个优先级,但其与多数操作系统都不能映射得很好。windows 有 7 个优先级且不是固定的,所以这种映射关系也不是确定的。而 Sun 的 Solaris 有 2^31 个优先级。唯一个一直的方法是,当调整优先级时,只使用MAX_PRIORITY
,MIN_PRIORITY
,NORM_PRIORITY
三种级别 。
七、让步 如果知道已经完成了在 run
方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示:我的工作已经做的差不多了,可以让别的线程来使用 CPU 了。这个暗示通过调用 yield()
方法来做出。只是一个暗示,没有任何机制保证它会被采纳。当调用 yield
时,也是在建议具有相同优先级的其他线程可以运行 。
大体上,对于任何重要的控制或在调整应用时,都不能依赖于 yield
。实际上,yield
经常被误用。
八、后台线程 1、创建后台线程 后台线程,是指在程序运行时在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行 main 的就是一个非后台线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class SimpleDaemons implements Runnable { @Override public void run () { try { while (true ) { TimeUnit.SECONDS.sleep(1 ); System.out.println(Thread.currentThread() + " " + this ); } } catch (InterruptedException e) { System.err.println("sleep() interrupted" ); } } public static void main (String[] args) throws InterruptedException { for (int i = 0 ; i < 10 ; i++) { Thread daemon = new Thread (new SimpleDaemons ()); daemon.setDaemon(true ); daemon.start(); } System.out.println("all daemons started" ); TimeUnit.SECONDS.sleep(10 ); } }
上面程序需要注意的点:
必须在线程启动之后调用 setDaemon()
方法,才能把它设置为后台线程
一旦 main 方法执行完成,程序就终止了。因为除了后台线程外,已经没有线程在运行了。
通过编写定制的 ThreadFactory
,可以定制由 Executor
创建的线程的属性(后台、优先级、名称):
1 2 3 4 5 6 7 8 class DaemonThreadFactory implements ThreadFactory { @Override public Thread newThread (Runnable r) { Thread thread = new Thread (r); thread.setDaemon(true ); return thread; } }
创建线程程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class DaemonFromFactory implements Runnable { @Override public void run () { try { while (true ) { TimeUnit.SECONDS.sleep(1 ); System.out.println(Thread.currentThread() + " " + this ); } } catch (InterruptedException e) { System.err.println("sleep() interrupted" ); } } public static void main (String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(new DaemonThreadFactory ()); for (int i = 0 ; i < 10 ; i++) { executorService.execute(new DaemonFromFactory ()); } System.out.println("all daemons started" ); TimeUnit.SECONDS.sleep(10 ); } }
2、确认后台线程 可以通过调用 isDaemon()
方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置为后台线程 。示例如下:
1 2 3 4 5 6 7 8 class DaemsonSpawn implements Runnable { @Override public void run () { while (true ) { Thread.yield (); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Daemon implements Runnable { private Thread[] threads = new Thread [10 ]; @Override public void run () { for (int i = 0 ; i < threads.length; i++) { threads[i] = new Thread (new DaemsonSpawn ()); threads[i].start(); System.out.println("DaemsonSpawn " + i + " started, " ); } for (int i = 0 ; i < threads.length; i++) { System.out.println("threads[" + i + "].isDaemon() = " + threads[i].isDaemon() + ", " ); } while (true ) { Thread.yield (); } } public static void main (String[] args) throws InterruptedException { Thread thread = new Thread (new Daemon ()); thread.setDaemon(true ); thread.start(); System.out.println("thread.isDaemon() = " + thread.isDaemon() + ", " ); TimeUnit.SECONDS.sleep(5 ); } }
输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 DaemsonSpawn 0 started, thread.isDaemon() = true, DaemsonSpawn 1 started, DaemsonSpawn 2 started, DaemsonSpawn 3 started, DaemsonSpawn 4 started, DaemsonSpawn 5 started, DaemsonSpawn 6 started, DaemsonSpawn 7 started, DaemsonSpawn 8 started, DaemsonSpawn 9 started, threads[0].isDaemon() = true, threads[1].isDaemon() = true, threads[2].isDaemon() = true, threads[3].isDaemon() = true, threads[4].isDaemon() = true, threads[5].isDaemon() = true, threads[6].isDaemon() = true, threads[7].isDaemon() = true, threads[8].isDaemon() = true, threads[9].isDaemon() = true,
上面的程序示例中,Daemon 线程被设置成了后台线程,然后派生出许多子线程,这些线程并没有被显示地设置为后台模式,不过它们确实是后台线程。
3、后台线程中的 finally 有一个后台线程的程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class ADaemon implements Runnable { @Override public void run () { try { System.out.println("starting ADaemon" ); TimeUnit.SECONDS.sleep(5 ); } catch (InterruptedException e) { throw new RuntimeException (e); } finally { System.out.println("this should always run?" ); } } public static void main (String[] args) { Thread thread = new Thread (new ADaemon ()); thread.setDaemon(true ); thread.start(); } }
输出结果如下:
从输出结果中看到,finally 块中的输出并没有被执行,但是该线程如果不是守护线程,则其中的内容将会执行。
这种行为是正确的,即便对于 finally 总会执行的承诺,并不希望出现这种行为,单情况仍旧如此。
当最后一个非后台线程终止时,后台线程会“突然”终止 。因此,一旦 main 方法退出,JVM 就会立即关闭所有的后台进程,而不会有任何你希望出现的确认形式。
因为你不能以优雅的方式来关闭后台线程,所以它几乎不是一种好的思想,非后台的 Executor
通常是一种更好的方式,因为其控制的所有任务可以同时被关闭。
上面的程序中,自己敲的时候,没有通过 thread.start()
方法启动,而是通过如下两种方式创建,不论哪一种(两种方式一样,只是因为线程工厂而出现不同),都会有 “this should always run?” 的输出,印证了上面一句话,而书中也提到了,说,“正如你将要在本章稍后看到的,在这种情况下,关闭将以有序的方式执行” 。
1 2 3 4 5 6 7 8 9 ExecutorService executorService = Executors.newCachedThreadPool();Thread thread = new Thread (new ADaemon ());thread.setDaemon(true ); executorService.execute(thread); executorService.shutdown(); ExecutorService executorService2 = Executors.newCachedThreadPool(new DaemonThreadFactory ());executorService2.execute(new ADaemon ()); executorService2.shutdown();
九、编码的变体 1、继承自 Thread 上面所使用的方式中,绝大部份,任务类都实现了 Runnable
接口,在非常简单的情况下,你可能会希望使用直接从 Thread
即成这种可替换的方式。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class SimpleThread extends Thread { private int countDown = 5 ; private static int threadCount = 0 ; public SimpleThread () { super (Integer.toString(++threadCount)); start(); } @Override public String toString () { return "#" + getName() + "(" + countDown + "), " ; } @Override 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 (); } } }
输出如下:
1 #1(5), #1(4), #1(3), #1(2), #5(5), #5(4), #2(5), #2(4), #2(3), #5(3), #4(5), #1(1), #4(4), #3(5), #3(4), #3(3), #3(2), #3(1), #4(3), #4(2), #4(1), #5(2), #5(1), #2(2), #2(1),
2、自管理的 Runnable 另一种可能会看到的惯用法是自管理的 Runnable
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class SelfManaged implements Runnable { private int countDown = 5 ; private Thread thread = new Thread (this ); public SelfManaged () { thread.start(); } @Override public String toString () { return "#" + Thread.currentThread().getName() + "(" + countDown + "), " ; } @Override 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
继承并没有什么特别的差异,只是语法稍微晦涩一些。但实现接口可以使你继承另一个不同的类,而继承自 Thread
不行 。
注意 :start
方法是在构造方法中调用的。这个示例相当简单,因此可能是安全的,但是,在构造器中启动线程可能会变得很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问处于不稳定状态的对象 。这是优选 Executor
而不是显示地创建 Thread
对象的另一个原因。
3、通过内部类隐藏线程代码 有时,通过使用内部类将线程代码隐藏在类中将会很有用。
3.1 继承自 Thread 的内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class InnerThread { private int countDown = 5 ; private Inner inner; private class Inner extends Thread { Inner (String name) { super (name); start(); } @Override public void run () { try { while (true ) { System.out.println(this ); if (--countDown == 0 ) { return ; } sleep(100 ); } } catch (InterruptedException e) { System.err.println("interrupted" ); } } @Override public String toString () { return getName() + ": " + countDown; } } public InnerThread (String name) { inner = new Inner (name); } }
3.2 继承自 Thread 的匿名内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class InnerThread2 { private int countDown = 5 ; private Thread thread; public InnerThread2 (String name) { thread = new Thread (name) { @Override public void run () { try { while (true ) { System.out.println(this ); if (--countDown == 0 ) { return ; } sleep(100 ); } } catch (InterruptedException e) { System.err.println("interrupted" ); } } @Override public String toString () { return getName() + ": " + countDown; } }; thread.start(); } }
3.3 实现 Runnable 接口的内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class InnerRunnable { private int countDown = 5 ; private Inner inner; private class Inner implements Runnable { Thread thread; Inner(String name) { thread = new Thread (this , name); thread.start(); } @Override public void run () { try { while (true ) { System.out.println(this ); if (--countDown == 0 ) { return ; } TimeUnit.SECONDS.sleep(1 ); } } catch (InterruptedException e) { System.err.println("interrupted" ); } } @Override public String toString () { return thread.getName() + ": " + countDown; } } public InnerRunnable (String name) { inner = new Inner (name); } }
3.4 实现 Runnable 接口的匿名内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class InnerRunnable2 { private int countDown = 5 ; private Thread thread; public InnerRunnable2 (String name) { thread = new Thread (new Runnable () { @Override public void run () { try { while (true ) { System.out.println(this ); if (--countDown == 0 ) { return ; } TimeUnit.SECONDS.sleep(1 ); } } catch (InterruptedException e) { System.err.println("interrupted" ); } } @Override public String toString () { return thread.getName() + ": " + countDown; } }, name); thread.start(); } }
3.5 在方法内部创建线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class ThreadMethod { private int countDown = 5 ; private Thread thread; private String name; public ThreadMethod (String name) { this .name = name; } public void runTask () { if (thread == null ) { thread = new Thread (name) { @Override public void run () { try { while (true ) { System.out.println(this ); if (--countDown == 0 ) { return ; } TimeUnit.SECONDS.sleep(1 ); } } catch (InterruptedException e) { System.err.println("interrupted" ); } } @Override public String toString () { return thread.getName() + ": " + countDown; } }; thread.start(); } } }
上面的代码中,展示了如何在方法内部创建线程,当你准备好运行线程时,就可以调用这个方法,而在线程开始之后,该方法将返回。如果该线程只执行辅助操作,而不是该类的重要操作,那么这与在该类的构造器内部启动线程相比,可能是一种更加有用而合适的方式。
十、术语 到目前为止,应该已经看到,要执行的任务与驱动它的线程之间有一个差异。这个差异在 Java 类库中尤为明显,因为你对 Thread
类实际上没有任何控制权,并且这种隔离在使用执行器时更加明显,因为执行器将替你处理线程的创建和管理。你创建任务,并通过某种方式将一个线程附着到任务上,以使得这个线程可以驱动任务。
在 Java 中,Thread
类本身不执行任何操作,它只是驱动赋予它的任务 。但是线程研究中,总是不变地使用“线程执行这项或那项动作”这种语言。给人的印象就是,”线程就是任务“。
另外 Runnable
接口的名字选则很糟糕,所以我认为 Task 应该是好得多的名字。如果接口只是其方法的返型封装,那么”它执行能做的事情“这种命名方式就是恰当的,但是如果它要表示更高层的抽象,例如 Task,那么概念名将有用。
从概念上来讲,我们希望创建独立于其他任务运行的任务,因此我们应该能够定义任务,然后说”开始“,并且不用操心其实现细节。但是在物理上,创建线程可能会代价高昂,因此你必须保存并管理它们。这样,从现实的角度看,将任务从线程中分离出来时很有意义的。另外,Java 的线程机制基于来自 C 的低级的 p 线程方式 ,这是一种你必须深入研究,并且需要完全理解其所有事物的所有细节的方式。
十一、加入一个线程 一个线程可以在其他线程上调用 join
方法,其效果是等待一段时间直到第二个线程结束才继续执行。
如果某个线程在另一个线程 t 上调用 t.join()
(即某个线程中有一段代码为t.join()
,或者说某个线程调用另一个线程的 join()
方法),此线程将被挂起,直到目标线程 t 结束才恢复(即 t.isAlive()
返回为假)。
也可以在调用 join
方法时带上一个超时参数,这样,如果目标线程在这段时间到期时还没有结束的话,join
方法总能返回。
对 join
方法的调用可能被中断,做法是在调用线程上调用 interrupt()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Sleeper extends Thread { private int duration; public Sleeper (String name, int sleepTime) { super (name); duration = sleepTime; start(); } @Override public void run () { System.out.println(getName() + " run() start" ); try { sleep(duration); } catch (InterruptedException e) { System.out.println(getName() + " was interrupted, " + "isInterrupted(): " + isInterrupted()); return ; } System.out.println(getName() + " run() end" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Joiner extends Thread { private Sleeper sleeper; public Joiner (String name, Sleeper sleeper) { super (name); this .sleeper = sleeper; start(); } @Override public void run () { System.out.println(getName() + " run() start" ); try { sleeper.join(); } catch (InterruptedException e) { System.out.println("interrupted" ); } System.out.println(getName() + " join completed" ); System.out.println(getName() + " run() end" ); } }
1 2 3 4 5 6 7 8 9 10 class Joining { public static void main (String[] args) { Sleeper sleepy = new Sleeper ("Sleepy" , 5000 ); Joiner dopey = new Joiner ("Dopey" , sleepy); Sleeper grumpy = new Sleeper ("Grumpy" , 5000 ); Joiner doc = new Joiner ("Doc" , grumpy); grumpy.interrupt(); } }
main 方法中两部分的输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 Sleepy run() start Dopey run() start ...... 5s Sleepy run() end Dopey join completed Dopey run() end Grumpy run() start Doc run() start Grumpy was interrupted, isInterrupted(): false Doc join completed Doc run() end
上面的程序中,Sleeper
是一个 Thread
类型,它要休眠一段时间,这段时间是通过构造器传入的时间确定的。sleep
方法有可能在指定的时间期满时返回,但也可能被中断。当另一个线程在该线程上调用 interrupt()
时,将给该线程设定一个标志,表明该线程已经被中断。==然而,异常被捕获时将清理这个标志,所以在 catch 子句中,在异常被捕获时这个标志总是为假。em,大大的问号。==
Joiner
线程将通过在 Sleeper
对象上调用 join()
方法来等待 Sleeper
醒来。
注意 :Java SE5 中包含诸如 CyclicBarrier
这样的工具,可能比最初的线程类库中的 join
更加合适。
十二、创建有响应的用户界面 使用线程的动机之一就是建立有响应的用户界面。这里给出一个基于控制台的用户界面的简单示例。
1 2 3 4 5 6 7 8 9 10 11 12 class UnresponsiveUI { private volatile double d = 1 ; public UnresponsiveUI () throws IOException { while (d > 0 ) { d = d + (Math.PI + Math.E) / d; } System.in.read(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class ResponsiveUI extends Thread { private static volatile double d = 1 ; public ResponsiveUI () { setDaemon(true ); start(); } @Override public void run () { while (true ) { d = d + (Math.PI + Math.E) / d; } } public static void main (String[] args) throws IOException { new ResponsiveUI (); System.in.read(); System.out.println(d); } }
十三、线程组 线程组 持有一个线程集合。
最好把线程组看成是一次不成功的尝试,你只要忽略它就好了 —— Joshua Bloch
继续错误的代价由别人来承担,而承认错误的代价由自己承担。 —— Joseph Stiglitz
十四、捕获异常 由于线程的本质特性,使得你不能捕获从线程中逃逸的异常。一旦异常逃出任务的 run
方法,它就会向外传播到控制台,除非你才去特殊的步骤捕获这种错误的异常。
在 Java SE5 之前,你可以使用线程组来捕获这些异常,但是有了 Java SE5,就可以用 Executor
来解决这个问题。
下面的任务会抛出一个异常,该异常会传播到其 run
方法外部:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class ExceptionThread implements Runnable { @Override public void run () { throw new RuntimeException ("this is a runtime exception" ); } public static void main (String[] args) { try { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new ExceptionThread ()); executorService.shutdown(); } catch (Exception e) { System.err.println("throw a exception: " + e.getMessage()); } try { Thread thread = new Thread (new ExceptionThread ()); thread.start(); } catch (Exception e) { System.err.println("throw a exception: " + e.getMessage()); } } }
输出如下:
1 2 3 4 5 6 7 8 Exception in thread "pool-1-thread-1" java.lang.RuntimeException: this is a runtime exception at cn.z2huo.knowledge.concurrency.section_21_2.exception.ExceptionThread.run(ExceptionThread.java:9) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:833) Exception in thread "Thread-0" java.lang.RuntimeException: this is a runtime exception at cn.z2huo.knowledge.concurrency.section_21_2.exception.ExceptionThread.run(ExceptionThread.java:9) at java.base/java.lang.Thread.run(Thread.java:833)
从程序中可以看出,将 main 中任务启动的主体放入 try-catch 中是没用的。
为了解决这个问题,需要修改 Executor
产生线程的方式,通过使用 Thread.UncaughtExceptionHandler
,其是 Java SE5 中的新接口,它允许你在每个 Thread
对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()
会在线程因微博活的异常而临近死亡时被调用。
下面的代码中,创建了一个线程工厂,它将每个新创建的 Thread
对象上附着一个 Thread.UncaughtExceptionHandler
。代码如下:
1 2 3 4 5 6 7 8 9 10 class ExceptionThread2 implements Runnable { @Override public void run () { System.out.println("=== thread run ===" ); Thread thread = Thread.currentThread(); System.out.println("run() by " + thread); System.out.println("eh = " + thread.getUncaughtExceptionHandler()); throw new RuntimeException ("thread " + thread.getName() + "throw a runtime exception" ); }
1 2 3 4 5 6 7 class MyUncaughtExceptionHandler implements Thread .UncaughtExceptionHandler { @Override public void uncaughtException (Thread t, Throwable e) { System.out.println("caught " + e); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class HandlerThreadFactory implements ThreadFactory { @Override public Thread newThread (Runnable r) { System.out.println("=== thread factory ===" ); System.out.println(this + " creating new Thread" ); Thread thread = new Thread (r); System.out.println("created " + thread); thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler ()); System.out.println("eh = " + thread.getUncaughtExceptionHandler()); return thread; } }
1 2 3 4 5 6 7 class CaptureUncaughException { public static void main (String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(new HandlerThreadFactory ()); executorService.execute(new ExceptionThread2 ()); executorService.shutdown(); } }
输出如下:
1 2 3 4 5 6 7 8 9 >>> thread factory cn.z2huo.knowledge.concurrency.section_21_2.exception.HandlerThreadFactory@5b2133b1 creating new Thread created Thread[Thread-0,5,main] eh = cn.z2huo.knowledge.concurrency.section_21_2.exception.MyUncaughtExceptionHandler@681a9515 >>> thread run run() by Thread[Thread-0,5,main] eh = cn.z2huo.knowledge.concurrency.section_21_2.exception.MyUncaughtExceptionHandler@681a9515 >>> custom exception handler caught java.lang.RuntimeException: thread Thread-0throw a runtime exception
从上面的程序中可以看到,未捕获的异常是通过 uncaughtException
来捕获的。
上面的示例使得你可以按照具体情况逐个地设置处理器,更简单的方式是,在 Thread
类中设置一个静态域,并将这个处理器设置为默认的为捕获异常处理器,代码如下:
1 2 3 4 5 6 7 8 9 class SettingDefaultHandler { public static void main (String[] args) { Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler ()); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new ExceptionThread2 ()); executorService.shutdown(); } }
这个处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。系统会检查线程专有版本,如果没有发现,则检查线程组是否有其专有的 uncaughtException
方法,如果也没有,再调用 defaultUncaughtExceptionHandler
。
相关链接 线程组遗留代码,从 MindView Exceptional Learning Experiences (mindviewllc.com) 上下载英文版的《Thinking in Java》
OB links #Java #并发 #多线程 #Thinking-in-Java