读Java编程思想随笔の并发
编程问题中相当大的一部分都可以通过使用顺序编程来解决。然而, 对于某些问题,如果能够并行地执行程序中的多个部分,则会变得非常方便甚至非常必要,因为这些部分要么看起来在并发地执行,要么在多处理器环境下可以同时执行。
正如你应该看到的,当并行执行的任务彼此开始产生互相干涉时,实际的并发问题就会接踵而至。这可能会以一种微妙而偶然的方式发生,我们可以很公正地说,并发具有“可论证的确定性,但是实际上具有不可确定性”。这就是说,你们可以得出结论,通过仔细设计和代码审查,编写能够正确工作的并发程序时可能的。但是,在实际情况中,更容易发生的情况是所编写的并发程序在给定适当的条件下,仍可能运行失败。这些条件可能从来都不会实际发生,或者发生的不是很频繁。以至于在测试过程中不会碰到它们。实际上,你可能无法编写出针对你并发程序发生故障条件的测试代码。所产生的故障时偶尔发生的,并且经常以客户抱怨的形式出现。这是研究并发问题的最强理由:如果视而不见,你就会遭其反噬。
并发的多面性
并发编程令人困惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系。因此你必须理解所有这些问题和特例,以便有效地使用并发。
更快的执行
速度问题初听起来很简单:如果你想要一个程序运行的更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具。
如果你有一台多处理器的机器,那么就可以在这些处理器之间分布多个任务,从而可以极大的提供吞吐量。这是使用强有力的多处理器的web服务器的常见情况,在为每一个请求分配一个线程的程序中,它可以将大量的用户请求分布到多个CPU上。
但是,并发通常是提高在单处理器上的程序的性能。
这听起来有些违背直觉。如果你仔细考虑一下就会发现,在单处理器上运行的并发程序开销确实比该程序的所有部分按顺序执行的开销大,因为其中增加了所谓的切换上下文的代价(从一个任务切换到另一个任务)。表面上看,将程序的所有部分当作单个任务运行看上去好像是开销小点,并且可以节省切换上下文的代价。
使这个问题变得有些不同的是阻塞。如果程序中的某个任务因为该程序控制范围之外的某些条件(通常I/O)而导致不能继续执行,那么我们就会说这个任务或线程阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。但是,如果使用并发来编写程序,那么当一个任务阻塞时,程序中其他任务还可以继续执行,因为这个程序可以保持继续向前执行。事实上,从性能角度看,如果没有任务阻塞,那么在单处理器机器上使用并发就没有任何意义。
实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容程序。多任务的操作系统可以通过周期性的将CPU从一个进程切换到另一个进程,来实现同时运行多个进程,尽管这使得每个进程看起来在执行过程中都是歇歇停停。进程总是很吸引人,因为操作系统会将进程互相隔离开,因此它们不会彼此干涉,这使得用进程编程相对容易一些。与此相反的是,像Java所用的这种并发系统会共享诸如内存和I/O这样的资源,因此编写多线程程序最基本的困难在于在协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。
每个任务作为进程都会在自己的地址空间内执行,因此任务之间根本不可能互相干涉。更重要的是,对进程来说,它们之间没有任何彼此通信的必要,因为它们都是完全独立的。操作系统会处理确保文件会正确复制的所有细节,因此,不会有任何风险,你可以获得更快的程序,并且完全免费。
Java采用了更加传统的方式,在顺序型语言上提供了对线程的支持。与在多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的一个好处是操作系统的透明性,这对Java而言,是一个重要的设计目标。
Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程。从而为每个线程都提供时间片,使得每个线程都会分配合理数量的时间去驱动它们的任务。在协作式系统中,每个任务都会自动地放弃控制,这要求程序员有意识地在每个任务中插入某种类型的让步语句。协作式系统的优势是双重的:上下文切换的开销通常要比抢占式系统低廉的多,并且对可以同时执行的线程数量在理论上没有任何限制。当你处理大量的仿真元素时,这可以是一种理想的解决方案。但是注意,某些协作式系统并未设计为可以在多个处理器之间分布任务,这可能会非常受限。
基本的线程机制
并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的CPU一样。其底层机制是切分CPU的时间,但通常你不需要考虑它。
线程模型为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理。在使用线程时,CPU将轮流给每个任务分配其占用时间。每个任务都觉得自己在一直占用CPU,但事实上,CPU时间是划分成片段分配给了所有任务。线程的一个好处是你可以从这个层次抽身出来,即代码不必知道它是运行在一个还是多个CPU的机器上。所以,使用线程机制是一种建立透明的,可扩展的程序的方法,如果程序运行的太慢,为机器增添一个CPU就可以提高程序的运行速度。多任务和多线程往往是使用多处理器系统的最合理的方式。
定义任务
线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。
public 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.println(status()); Thread.yield(); } } }
标识符id可以用来区分任务的多个实例,它是final的,因为它一旦被初始化之后就不希望被改变。
任务的run()通常总会有某种形式的循环,使得任务一直运行下去直到不再需要,所以要设定跳出循环的条件(有一种方式是直接从run()方法返回)。通常,run()被写成无限循环的形式,这意味着除非由某个条件让run()终止,否则它将永远运行下去。
在run()中对静态方法Thread.yield()的调用是对线程调度器(Java线程机制的一部分,可以将CPU从一个进程转移到另一个进程)的一种建议,它在声明:“我已经执行完生命周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的打好时机”。这完全是选择性的。
在下面实例中,这个任务的run()不是由单独的线程驱动的,它是在main中直接调用的(实际上,这里仍旧使用了线程,即总是分配给main()的那个线程)。
public class MainThread { public static void main(String[] args) { LiftOff launch = new LiftOff(); launch.run(); } } // #0(9), // #0(8), // #0(7), // #0(6), // #0(5), // #0(4), // #0(3), // #0(2), // #0(1), // #0(Liftoff!),
当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处--它不会产生任何内在的线程能力。要实现线程行为,你必须显式将一个线程附着在线程上。
Thread类
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。下面的示例展示了如何用Thread来驱动LiftOff对象。
public class BasicThreads { public static void main(String[] args) { Thread t = new Thread(new LiftOff()); t.start(); System.out.println("Waiting for LiftOff"); } } // Waiting for LiftOff // #0(9), // #0(8), // #0(7), // #0(6), // #0(5), // #0(4), // #0(3), // #0(2), // #0(1), // #0(Liftoff!),
Thread构造器只需要一个Runnable对象。调用 Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。尽管start()看起来像是产生了一个对长期运行方法的调用,但是从输出中可以看到,start()方法迅速返回了,因为Waiting for LiftOff在倒计时完成之前就出现了。实际上,你产生的是对LiftOff.run()方法的调用,并且这个方法还没有完成,但是因为LiftOff.run()是由不同的线程执行的,因此你仍旧可以执行main()线程中的其他操作(这种能力并不局限于main()线程,任何线程都可以启动另一个线程)。因此,程序会运行两个方法,main()和LiftOff.run()是程序中与其他线程同时执行的代码。
你可以很容易添加更多的线程来驱动更多的任务。下面,你可以看到所有任务彼此之间是如何互相呼应的:
public 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"); } }
输出说明不同任务的执行在被换进换出时混在了一起。这种交换是线程调度器自动控制的。如果在你的机器上有多个处理器,线程调度器将会在这些处理器之间默默分发线程。
当main()创建Thread对象时,它并没有捕获任何对这些对象的引用。在使用普通对象时,这对于垃圾回收来说是一种公平的游戏,但是在使用Thread对象时,情况就有所不同了。每个Thread都注册了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收期无法消除它。你可以从输出中看到,这些任务确实运行到了结束,因此,一个线程会创建一个单独的执行线程,在调用完start()之后,它仍旧会继续存在。
使用Executor
java.util.concurrent包中的Executor将为你管理Thread,从而简化了并发编程。Executor在客户端与任务执行之间提供了一个间接层。与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无需显式地管理线程的生命周期。
我可以使用Executor来代替MoreBasicThreads.java中显式创建的Thread。LiftOff对象知道如何运行具体的任务,与命令设计模式一样,它暴露了要执行的单一方法。ExecutorService(具有服务生命周期的Executor)如何创建恰当地上下文来执行Runnable对象。在接下来的示例中,CachedThreadPool将为每个任务都创建一个线程。注意,ExecutorService是使用静态的Executor方法创建的,这个方法可以确定Executor的类型。
public class CachedThreadPool { public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; i++) exec.execute(new LiftOff()); exec.shutdown(); } }
非常常见的情况是,单个Executor被用来创建和管理系统中所有的任务。
对shutdown()方法的调用可以防止新的任务被提交给Executor,当前线程(驱动main()的线程)将继续运行在shutdown()被调用之前执行的所有任务。这个程序将在Executor中的所有任务完成之后尽快退出。
你可以很容易地将前面示例中的CachedThreadPool替换为不同类型的Executor。FixedThreadPool使用了有限的线程集来执行所提交的任务:
public class FixedThreadPool { public static void main(String[] args) { // Constructor argument is number of threads: ExecutorService exec = Executors.newFixedThreadPool(5); for(int i = 0; i < 5; i++) exec.execute(new LiftOff()); exec.shutdown(); } }
有了FixedThreadPool,你就可以一次性预先执行代价高昂的线程分配工作,因而也就可以限制线程的数量了。这可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。在事件驱动的系统中,需要线程的事件处理器,通过直接从池中获取线程,也可以如你所愿地尽快得到服务。你不会滥用可获得的资源,因为FixedThreadPool使用的Thread对象是有限的。
SingleThreadExecutor就像线程数量为1的FixedThreadPool。这对于你希望在另一个线程中连续运行的任何事物来说,都是很有用的。
如果向SingleThreadExecutor提交多个任务,那么这些任务将排队,每一个任务都会在下一个任务运行之前结束,所有的任务都将使用相同的线程。在下面的示例中,我们可以看到每个任务都是按照它们被提交的顺序,并且是在下一个任务开始运行之前完成。因此,SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己的悬挂任务队列。
public class SingleThreadExecutor { public static void main(String[] args) { ExecutorService exec = Executors.newSingleThreadExecutor(); for(int i = 0; i < 5; i++) exec.execute(new LiftOff()); exec.shutdown(); } }
作为另一个示例,假设你有大量的线程。那它们运行的任务将使用文件系统。你可以用SingleThreadExecutor来运行这些线程。以确保任意时刻在任何线程中都只有唯一的任务在运行。在这种方式中,你不需要在共享资源下共享同步(同时不会过度使用文件系统)。有时更好的解决方案是在资源上同步。但是SingleThreadExecutor可以让你省去只是为了维持某些事物的原形而进行的各种协调努力。通过序列化任务,你可以消除对序列化对象的需求。
从任务中产生返回值
Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成之后返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从call()(而不是run())返回的一个值。并且必须使用ExecutorService.submit()调用它。
class TaskWithResult implements Callable<String> { private int id; public TaskWithResult(int id) { this.id = id; } public String call() { return "result of TaskWithResult " + id; } } public class CallableDemo { public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); ArrayList<Future<String>> results = new ArrayList<Future<String>>(); for(int i = 0; i < 10; i++) results.add(exec.submit(new TaskWithResult(i))); for(Future<String> fs : results) try { // get() blocks until completion: System.out.println(fs.get()); } catch(InterruptedException e) { System.out.println(e); return; } catch(ExecutionException e) { System.out.println(e); } finally { exec.shutdown(); } } } /* Output: 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()来查看任务是否已完成。
休眠
影响任务行为的一种简单方式是调用sleep()方法,这将使任务终止执行给定的时间。在LiftOff类中,要是把对yield()方法调用改为sleep(),将得到如下结果:
public class SleepingTask extends LiftOff { public void run() { try { while(countDown-- > 0) { System.out.print(status()); // Old-style: // Thread.sleep(100); // Java SE5/6-style: TimeUnit.MILLISECONDS.sleep(100); } } catch(InterruptedException e) { System.err.println("Interrupted"); } } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; i++) exec.execute(new SleepingTask()); exec.shutdown(); } }
对sleep()方法的调用可以抛出InterruptedException异常,并且你可以看到,它在run()中被捕获。因为异常不能跨线程传播回main(),所以你必须在本地处理所有在任务内部产生的异常。
你可能会注意到,这些任务是按照“完美的分布”顺序运行的,即从0到4,然后再回过头从0开始,当然这取决于你的平台。这是有意义的,因为在每个打印语句之后,每个任务都将会睡眠(阻塞),这使得线程调度器可以切换到另一个线程,进而驱动另一个任务。但是,顺序行为依赖于底层的线程机制,这种机制在不同操作系统之间是有差异的,因此,你不能依赖于它,如果你必须控制任务执行的顺序,那么最好的押宝是使用同步控制,或者在某些情况下,压根不使用线程,但是要自己编写协作例程,这些例程将会按照指定的顺序在互相之间传递控制权。
优先级
线程的优先级将该线程的重要性传递给调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先级最高的线程来执行。然而,这并不意味着线程优先级低的线程将得不到执行。优先级较低的线程仅仅只是线程执行的没那么频繁。
在绝大多数时间里,所有线程都应该按照默认优先级执行,试图操作线程优先级通常是一种错误。
你可以同getPriority()来读取现有线程的优先级,并且在任意时刻都可以通过setPriority()来修改它。
public class SimplePriorities implements Runnable { private int countDown = 5; private volatile double d; // No optimization private int priority; public SimplePriorities(int priority) { this.priority = priority; } public String toString() { return Thread.currentThread() + ": " + countDown; } public void run() { Thread.currentThread().setPriority(priority); while(true) { // An expensive, interruptable operation: 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 exec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; i++) exec.execute( new SimplePriorities(Thread.MIN_PRIORITY)); exec.execute( new SimplePriorities(Thread.MAX_PRIORITY)); exec.shutdown(); } } /* Output: (70% match) Thread[pool-1-thread-6,10,main]: 5 Thread[pool-1-thread-6,10,main]: 4 Thread[pool-1-thread-6,10,main]: 3 Thread[pool-1-thread-6,10,main]: 2 Thread[pool-1-thread-6,10,main]: 1 Thread[pool-1-thread-3,1,main]: 5 Thread[pool-1-thread-2,1,main]: 5 Thread[pool-1-thread-1,1,main]: 5 Thread[pool-1-thread-5,1,main]: 5 Thread[pool-1-thread-4,1,main]: 5
可以看到,最后一个线程优先级最高,其余所有线程优先级都设置到最低。注意,优先级是在run()开头部分设定的,在构造器中设置他们不会有任何好处,因为Executor在此刻还没有开始执行任务。
让步
如果知道已经完成了在run()方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制的一种暗示,你的工作已经做的差不多了,可以让别的线程使用CPU。这个暗示将通过调用yield()方法来做出(不过只是一种暗示,没有任何机制敢保证它将会被采纳)。当调用yield()时,你也是在建议具有相同优先级的其他线程可以运行。
后台线程
所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的一部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main()方法就是一个非后台线程。
public class SimpleDaemons implements Runnable{ @Override public void run() { while (true){ try { TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread()+" "+this); } catch (InterruptedException e) { System.out.println("sleep() interrupted"); } } } public static void main(String[] args) throws Exception{ for (int i=0;i<10;i++){ Thread thread = new Thread(new SimpleDaemons()); thread.setDaemon(true); thread.start(); } System.out.println("All daemons started"); TimeUnit.MILLISECONDS.sleep(175); } } // All daemons started // Thread[Thread-1,5,main] com.qiqi.concurrency.SimpleDaemons@2fe4cbc4 // Thread[Thread-2,5,main] com.qiqi.concurrency.SimpleDaemons@24a20892 // Thread[Thread-6,5,main] com.qiqi.concurrency.SimpleDaemons@1f24bbbf // Thread[Thread-5,5,main] com.qiqi.concurrency.SimpleDaemons@8b2fd8f // Thread[Thread-3,5,main] com.qiqi.concurrency.SimpleDaemons@55fdc96c // Thread[Thread-4,5,main] com.qiqi.concurrency.SimpleDaemons@55fdc96c // Thread[Thread-7,5,main] com.qiqi.concurrency.SimpleDaemons@64578ceb // Thread[Thread-0,5,main] com.qiqi.concurrency.SimpleDaemons@50337d0f // Thread[Thread-9,5,main] com.qiqi.concurrency.SimpleDaemons@9e0bc08 // Thread[Thread-8,5,main] com.qiqi.concurrency.SimpleDaemons@158b649
必须在线程启动之前调用setDaemon(true),才能把它设置为后台线程。
一旦main()完成其工作,就没什么能阻止程序终止了,因为除了后台线程以外,已经没有线程在运行了。main()线程被设定为短暂睡眠,所以可以观察到所有后台线程启动后的结果。
通过编写定制的ThreadFactory可以定制由Executor创建的线程属性(后台、优先级、名称)。
public class DaemonThreadFactory implements ThreadFactory{ @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); return t; } }
这与普通的ThreadFactory唯一的差别就是它将后台状态全部设置为true。你现在可以用一个新的DaemonThreadFactory作为参数传递给Executor.newCachedThreadPool()。
public class DaemonFromFactory implements Runnable{ @Override public void run() { while (true){ try { TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread()+" "+this); } catch (InterruptedException e) { System.out.println("Interrupted"); } } } public static void main(String[] args) throws Exception{ ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory()); for (int i=0;i<10;i++){ exec.execute(new DaemonFromFactory()); } System.out.println("All daemons started"); TimeUnit.MILLISECONDS.sleep(500); } }
每个静态的ExecutorService创建方法都被重载为接受一个ThreadFactory对象,而这个对象将被用来创建新的线程。
public class DaemonThreadPoolExecutor extends ThreadPoolExecutor{ public DaemonThreadPoolExecutor() { super(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),new DaemonThreadFactory()); } }
可以通过调用isDaemon()方法来确认线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程都将被自动设置为后台线程。
class DaemonSpawn implements Runnable{ @Override public void run() { while(true){ Thread.yield(); } } } class Daemon implements Runnable{ private Thread [] t = new Thread[10]; @Override public void run() { for (int i=0;i<t.length;i++){ t[i] = new Thread(new DaemonSpawn()); t[i].start(); System.out.println("DaemonSpawn="+i+"started"); } for (int i=0;i<t.length;i++){ System.out.println("t["+i+"].isDaemon()="+t[i].isDaemon()+","); } while (true){ Thread.yield(); } } } public class Daemons { public static void main(String[] args) throws Exception{ Thread d = new Thread(new Daemon()); d.setDaemon(true); d.start(); System.out.println("d.isDaemon()="+d.isDaemon()+","); TimeUnit.MILLISECONDS.sleep(1); } } // d.isDaemon()=true, // DaemonSpawn=0started // DaemonSpawn=1started // DaemonSpawn=2started // DaemonSpawn=3started // DaemonSpawn=4started // DaemonSpawn=5started // DaemonSpawn=6started // DaemonSpawn=7started // DaemonSpawn=8started // DaemonSpawn=9started // t[0].isDaemon()=true, // t[1].isDaemon()=true, // t[2].isDaemon()=true, // t[3].isDaemon()=true, // t[4].isDaemon()=true, // t[5].isDaemon()=true, // t[6].isDaemon()=true, // t[7].isDaemon()=true, // t[8].isDaemon()=true, // t[9].isDaemon()=true,
Daemon线程被设置成了后台线程,然后派生出了许多子线程,这些线程并没有被显式地设置为后台模式,不过它们的确是后台线程。接着,Daemon线程进入了无限循环,并在循环里调用yield()方法把控制权交给其他进程。
你应该意识到后台进程在不执行finally子句的情况下就会终止run()方法。
public class ADaemon implements Runnable { @Override public void run() { System.out.println("Starting ADaemon"); try { TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException e) { System.out.println("Exiting via InterruptedException"); }finally { System.out.println("This should be always run?"); } } public static void main(String[] args) { Thread t = new Thread(new ADaemon()); t.setDaemon(true); //t.setDaemon(true); t.start(); } } //Starting ADaemon //Starting ADaemon //This should be always run?
//如果main()线程跑的比ADaemon还快,finally子句肯定不会执行,因为main()线程完了,所有后台线程也完了。
当你运行这个程序时,你将看到finally子句就不会执行,大事如果你注释掉setDaemon()的调用,就会看到finally子句会执行。
这种情况是正确的,即便你基于前面对finally给出的承诺,后台线程会突然终止。因此一旦main()方法退出,JVM将会立即关闭后台所有线程,所以它们几乎不是一种好的思想。
编码的变体
到目前为止,在你所看到的示例中,任务类都实现了Runnable。在非常简单的情况下,你可能会希望使用直接从Thread继承这种可替换的方式,如下:
public class SimpleThread extends Thread { private int countDown = 5; private static int threadCount = 0; public SimpleThread() { // Store the thread name: super(Integer.toString(++threadCount)); start(); } public String toString() { return "#" + getName() + "(" + countDown + "), "; } 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(); } } /* Output: #1(5), #1(4), #1(3), #1(2), #1(1), #2(5), #2(4), #2(3), #2(2), #2(1), #3(5), #3(4), #3(3), #3(2), #3(1), #4(5), #4(4), #4(3), #4(2), #4(1), #5(5), #5(4), #5(3), #5(2), #5(1), *///:~
另一种可能会看到的惯用法是自管理的Runnable:
public class SelfManaged implements Runnable { private int countDown = 5; private Thread t = new Thread(this); public SelfManaged() { t.start(); } public String toString() { return Thread.currentThread().getName() + "(" + countDown + "), "; } 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(); } } /* Output: Thread-0(5), Thread-0(4), Thread-0(3), Thread-0(2), Thread-0(1), Thread-1(5), Thread-1(4), Thread-1(3), Thread-1(2), Thread-1(1), Thread-2(5), Thread-2(4), Thread-2(3), Thread-2(2), Thread-2(1), Thread-3(5), Thread-3(4), Thread-3(3), Thread-3(2), Thread-3(1), Thread-4(5), Thread-4(4), Thread-4(3), Thread-4(2), Thread-4(1), *///:~
注意,start()是在构造器中调用的。这个示例相当简单,因此可能是安全的,但是你应该意识到,在构造器中启动线程可能会变得很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问出于不稳定状态的对象。这是优选Executor而不是显式地创建Thread的另一个原因。
加入一个线程
一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束后才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。
也可以在调用join()时带上一个超时参数,这样如果目标线程在这段时间到期时还没没有结束地话,join()方法总能够返回。
对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时需要用到try-catch子句。
class Sleeper extends Thread { private int duration; public Sleeper(String name, int sleepTime) { super(name); duration = sleepTime; start(); } public void run() { try { sleep(duration); } catch(InterruptedException e) { print(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted()); return; } print(getName() + " has awakened"); } } class Joiner extends Thread { private Sleeper sleeper; public Joiner(String name, Sleeper sleeper) { super(name); this.sleeper = sleeper; start(); } public void run() { try { sleeper.join(); } catch(InterruptedException e) { print("Interrupted"); } print(getName() + " join completed"); } } public class Joining { public static void main(String[] args) { Sleeper sleepy = new Sleeper("Sleepy", 1500), grumpy = new Sleeper("Grumpy", 1500); Joiner dopey = new Joiner("Dopey", sleepy), doc = new Joiner("Doc", grumpy); grumpy.interrupt(); } } /* Output: Grumpy was interrupted. isInterrupted(): false Doc join completed Sleepy has awakened Dopey join completed *///:~