1.Executor允许你管理异步任务的执行,而无须显示地管理线程的生命周期。Executor在Java SE5/6中是启动任务的优选方法。可以使用Executor来代替显示地创建Thread对象。
2.ExecutorService知道如何构建恰当的上下文来执行Runnable对象。
3.CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后再它回收旧线程时停止创建新线程,因此它是合理的Executor的首选。只有当这种方式会引发问题时,才需要切换到FixedThreadPool。
4.SingleThreadExecutor就像是线程数量为1的FixedThreadPool。如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己(隐藏)的悬挂任务队列。
SingleThreadExecutor确保任何时刻在任何线程中都只有唯一的任务在运行,就不需要在共享资源上处理同步。
5.Runnable是执行工作的独立任务,但是他不返回任何值。如果希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它。
测试代码如下:
1 |
|
输出结果为:
==taskIndex 0==
isDone(): false
TaskWithResult id: 0
isDone(): true==taskIndex 1==
isDone(): false
TaskWithResult id: 1
isDone(): true==taskIndex 2==
isDone(): true
TaskWithResult id: 2
isDone(): true==taskIndex 3==
isDone(): false
TaskWithResult id: 3
isDone(): true==taskIndex 4==
isDone(): true
TaskWithResult id: 4
isDone(): true==taskIndex 5==
isDone(): true
TaskWithResult id: 5
isDone(): true==taskIndex 6==
isDone(): true
TaskWithResult id: 6
isDone(): true==taskIndex 7==
isDone(): false
TaskWithResult id: 7
isDone(): true==taskIndex 8==
isDone(): true
TaskWithResult id: 8
isDone(): true==taskIndex 9==
isDone(): true
TaskWithResult id: 9
isDone(): true
且每当输出到isDone(): false的地方,输出线程就会卡住,直到取到这个线程的返回值后,才会继续后续的打印。
Future的javadoc说明如下:
A Future represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation. The result can only be retrieved using method get when the computation has completed, blocking if necessary until it is ready. Cancellation is performed by the cancel method. Additional methods are provided to determine if the task completed normally or was cancelled. Once a computation has completed, the computation cannot be cancelled. If you would like to use a Future for the sake of cancellability but not provide a usable result, you can declare types of the form Future<?> and return null as a result of the underlying task.
Future.get()方法的javadoc说明如下:
Waits if necessary for the computation to complete, and then retrieves its result.
6.调度器倾向于让优先级最高的线程先执行。如果要设置线程的优先级,应该在run()方法体中使用Thread.currentThread().setPriority(newPriority)
来设置,而不要在构造器中设置。
不同操作系统对优先级的分级不同,为了可移植性,应该只使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三种级别。
7.yield()方法会暗示线程调度器,我的工作做得差不多了,具有相同优先级的其他线程可以运行了。但这仅仅是一个暗示,没有任何机制保证它将被采纳。所以,不能依赖于yield()。实际上,yield()经常被误用。
8.所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法才能把它设置为后台线程,如以下代码:
1 |
|
可以用isDaemon()方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何子线程都将被自动设置成后台线程。
9.是线程睡眠时可以调用
1 |
|
等方法来代替直接调用Thread.sleep(timeout)
方法,这样时间的长度就更直观,这些方法内部会自动调用Thread.sleep(timeout)方法。
10.join()方法:
- 一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive()返回为假)。
- 可以在调用join()时带上一个超时参数(单位可以是毫秒,或者毫秒和纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。
- 对join()方法的调用可以被中断,做法是在调用join()的线程上再次调用interrupt()方法。
11.由于线程的本质特性,不能捕获从线程中逃逸的异常,即使在主线程中执行子线程任务时把它包在try-catch块中,任然无法捕获到逃逸的异常,异常会直接向外传播的控制台。为了捕获异常,可以在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler。
例如下面的代码:
1 |
|
执行的结果为:
Exception in thread “Thread-0” java.lang.RuntimeException: I throw this Exception on purpose!
at MyThread.run(ThreadExceptionCaughtTest.java:5)
可见try-catch并不能捕获到我在MyThread中抛出的异常。
那么我为MyThread附加一个我自己实现的UncaughtExceptionHandler,代码如下:
1 |
|
执行的结果为:
Haha! I caught : java.lang.RuntimeException: I throw this Exception on purpose!
可以看到MyUncaughtExceptionHandler成功捕获到了MyThread抛出的异常,try-catch仍然没有捕获到异常。
12.关于synchronized方法
所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。
例如这段代码:
1 |
|
其执行结果始终为:
===start===
incAccount result: 201
decAccount result: 200
且控制台打印出“===start===”后,会暂停2秒钟才继续输出后面的2行结果。
而如果把函数decAccount()前面的synchronized修饰符去掉,则打印结果为:
===start===
decAccount result: 199
incAccount result: 200
且前两行结果是瞬间打印出来的,然后暂停了2秒,才输出最后一行结果。
13.在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。
如果你的类中有超过一个方法在处理临界数据,那么你必须同步所有相关的方法。
14.一个任务可以多次获得对象的锁。JVM负责跟踪对象被加锁的次数。只有首先获得了锁的任务才能允许继续获取多个锁。
15.针对每个类,也有一个锁,作为类的Class对象的一部分,所以synchronized static方法可以在类的范围内防止对static数据的并发访问。
16.在java.util.concurrent.locks中定义了显示的互斥机制Lock。Lock对象必须被显示地创建、锁定和释放。
好的使用习惯是,使用try-finally语句块,将lock()放在try中,将unlock()放在finally中。注意,return语句必须放在try子句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。
17.如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。即便使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中。
如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。
使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。你的第一选择应该是使用synchronized关键字,这是最安全的方式,而尝试其他任何方式都是有风险的。(我理解这句话想表达的意思是,尽量不要使用volatile关键字,而是使用synchronized关键字或者Lock类。)
18.调用sleep()和yield()的时候并没有释放锁。
调用wait()的时候锁被释放。
wait()、notify()以及notifyAll()有一个比较特殊的方面,那就是这些方法是基类Object的一部分,而不是属于Thread的一部分,只能在synchronized方法或synchronized块中调用这几个方法。sleep()可以在非synchronized控制方法里调用,是因为不用操作锁。