《Java并发编程实践》笔记3——线程池基础
1.JDK中的Executor框架是基于生产者-消费者模式的线程池,提交任务的线程是生产者,执行任务的线程是消费者。
Executor线程池可以用于异步任务执行,而且支持很多不同类型任务执行策略,同时为任务提交和任务执行之间的解耦提供了标准方法。
Executor线程池支持如下三种线程执行策略:
(1).顺序执行:
类似于单线程顺序执行任务,优点是实现简单;缺点是扩展性受限,执行效率低下,例子代码如下:
public class WithinThreadExecutor implements Executor{ public void execute(Runnable r){ r.run(); } }
(2)每请求每线程:
为每个请求创建一个新的线程,优点是可以并行处理;缺点是线程生命周期开销大,活动线程受内存资源、JVM以及操作系统的限制,当负载过大时响应性和吞吐量会下降严重,同时还会影响稳定性,例子代码如下:
public class ThreadPerTaskExecutor implements Executor{ public void execute(Runnable r){ new Thread(r).start(); } }
(3)线程池:
使用线程池可以重用已有线程,减少线程生命周期开销,同时可以调整活动线程数量,既可以确保足够的并发性,又避免过多线程相互竞争资源,例子代码如下:
public class TaskExecutionWebServer { private static final int NTHREADS = 100; private static final ExecutorService exec = Executors .newFixedThreadPool(NTHREADS); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; exec.execute(task); } } }
2.Executor常用的创建线程池静态工厂方法:
(1).newFixedThreadPool:
创建一个定长的线程池,每当提交一个任务就创建一个线程,直到达到池的最大长度,这时线程池会保持长度不再变化,若一个线程由于非预期的异常而结束,线程池会补充一个新的线程。
(2).newCachedThreadPool:
创建一个可缓存的线程池,若当前线程池的长度超过了处理的需要时,它可以灵活地回收空闲的线程,当需求增加时,它可以灵活地添加新的线程,而并不会对池的长度做任何限制。
(3).newSingleThreadExecutor:
创建一个单线程化的executor,只创建唯一的工作者线程来执行任务,若这个线程异常结束,会有另一个取代它。Executor会保证任务依照任务队列所规定的顺序执行。
(4).newScheduledThreadPool:
创建一个支持定时的以及周期性执行的任务的定长线程池。
3.Executor的生命周期:
ExecutorService接口扩展了Executor,提供了以下用于生命周期管理的方法:
public interface ExecutorService extends Executor{ void shutdown(); list<Runnable> shutdownNow(); boolean isShutdown(); boolean isTerminated(); boolean awaitTermination(long timeout, TimeUnit unit) throws InterruputedException; ...... }
ExecutorService接口暗示了Executor的生命周期有以下3中状态:
(1).运行状态:
ExecutorService最初创建后的初始状态是运行状态。
(2).关闭状态:
ExecutorService的sutdown方法会启动一个平缓的关闭过程,停止接收新任务,同时等待已提交的任务执行完成(包括尚未开始执行的任务)。
ExecutorService的sutdownNow方法会启动一个强制的关闭过程,尝试取消所有运行中的任务和排在队列中尚未开始执行的任务。
(3)终止状态:
一旦所有任务全部完成后,ExecutorService就会进入终止状态,通过调研ExecutorService的awaitTermination方法等待达到终止状态,也可以调用isTerminated来轮询是否达到终止状态。
4.Timer与ScheduledExecutorService:
在JDK1.5之前,经常使用Timer(开源的Quartz框架也可以)作为定时器管理任务的延迟或周期性执行,在JDK1.5引入了ScheduledExecutorService,使用线程池作为定时器管理任务的延迟或周期性执行,二者的区别如下:
(1).Timer对调度的支持是基于绝对时间的,不支持相对时间,因此任务对系统时钟的改变是敏感的;ScheduledExecutorService只支持相对时间。
(2).Timer只创建唯一的线程来执行所有的timer任务,若一个timer任务的执行很耗时,会导致其他的timer任务时效准确性问题,例如一个timer任务每10ms执行一次,而另一个timer任务每40ms执行一次,若按固定频率进行调度则重复出现的任务会在耗时的任务完成后快速联系地被调用4次,若按延迟进行调度则完全丢失4次调用。
ScheduledExecutorService可以提供多个线程来执行延迟或按固定频率执行的周期性任务,解决了Timer任务时效准确性问题。
(3).若Timer任务抛出未检查异常时,Timer将会被异常地终止,Timer也不会再重新恢复线程执行,它错误地认为整个Timer都被取消了,从而产生无法预料的线程泄露:所有已被安排但尚未执行的Timer任务永远不会再执行了,新的任务也不能被调度了。
下面的例子代码演示Timer的线程泄露:
public class OutOfTimer { public static void main(String[] args) throws Exception { Timer timer = new Timer(); timer.schedule(new ThrowTask(), 1); TimeUnit.SECONDS.sleep(1); timer.schedule(new ThrowTask(), 1); TimeUnit.SECONDS.sleep(5); } static class ThrowTask extends TimerTask{ public void run(){ System.out.println("I'm invoked."); throw new RuntimeException(); } } }
上面代码运行后只会打印出一行I'm invoked.然后就抛出Timer already cancelled异常。
ScheduledExecutorService可以妥善地处理异常,避免线程泄露。
下面的例子代码演示ScheduledExecutorService在异常之后仍然可以继续运行:
public class OutOfScheduledExecutor { public static void main(String[] args) throws Exception { ScheduledExecutorService service = Executors.newScheduledThreadPool(1); service.schedule(new ThrowTask(), 1, TimeUnit.SECONDS); TimeUnit.SECONDS.sleep(1); service.schedule(new ThrowTask(), 1, TimeUnit.SECONDS); TimeUnit.SECONDS.sleep(5); service.shutdown(); } static class ThrowTask implements Runnable{ public void run(){ System.out.println("I'm invoked."); throw new RuntimeException(); } } }
上述的ScheduledExecutorService例子没有抛出,可以正常打印出两行I'm invoked.
5.Callable和Future:
Runnable是Executor框架常用的任务基本表达形式,但是其run方法不能返回一个值或者抛出受检查的异常。
Callable类似于Runnable,其call方法可以等待返回值,并为可能抛出的异常预先做好准备。
Future描述了任务的生命周期,并提供了相关的方法来获得任务的结果、取消任务以及检验任务是否已经完成或者被取消。ExecutorService中所有的submit方法都返回一个Future。
使用Runnable/Callable和Future可以提高任务的并行性,例子代码如下:
public class FutureRender{ private final ExecutorService executor = ......; public void renderPage(CharSequence source){ final List<ImageInfo> imageInfos = scanForImageInfo(source); Callable<List<ImageData>> task = new Callable<List<ImageData>>(){ public list<ImageData> call(){ List<ImageData> result = new ArrayList<ImageData>(); for(ImageInfo imageInfo : imageInfos){ result.add(imageInfo.downloadImage()); } return result; } }; Future<List<ImageData>> future = executor.submit(task); renderText(source); try{ List<ImageData> imageDatas = future.get(); for(ImageData data : imageDatas){ renderImage(data); } }catch(InterruptedException e){ Thread.currentThread().interrupt(); future.cancel(true); }catch(ExecutionException e){ throw launderThrowable(e.getCause()); } } }
注意:只有大量相互独立且同类的任务进行并发处理时,会将程序的任务量分配到不同的任务中,才能正在获得并发性能的提高;而对异类任务的并发处理则会因为任务协调的开销,不一定能获得性能的提高。
6.CompletionService介绍:
CompletionService整合了Executor与BlockingQueue的功能,可以将一个批处理任务提交给给它执行,然后返回一个包含每个任务执行结果的QueueingFuture队列,通过调用队列的take和poll方法,可以获得包含每个任务执行结果的Future。
CompletionService的例子代码如下:
public class CompletionServiceRender { private final ExecutorService executor; public CompletionServiceRender(ExecutorService executor) { This.executor = executor; } public void renderPage(CharSequence source) { final List<ImageInfo> imageInfos = scanForImageInfo(source); CompletionService<ImageData> service = new ExecutorCompletionService<ImageData>( executor); for (final ImageInfo imageInfo : imageInfos) { service.submit(new Callable<ImageData>() { public ImageData call() { return imageInfo.downloadImage(); } }); } renderText(source); try { for (int i = 0; i < imageInfos.size(); i++) { Future<ImageDate> f = service.take(); ImageData data = f.get(); renderImage(data); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } } }
7.线程的取消和关闭:
对于非后台线程,如果取消和关闭不当会导致阻塞JVM无法正常关闭,Java提供了一个协作的中断机制使一个线程能够要求另一个线程停止当前工作。
Java中常用的取消和关闭策略如下:
(1).非阻塞方法:
使用volatile域保存取消状态,在每次操作时检测该状态。
(2).阻塞方法:
线程可能永远不会检测取消标志,因此使用volatile域保存取消状态的方案不可行,需要使用线程中断。
线程中断是一个协作机制,一个线程给另一个线程发送信号,通知它在下一个方便时刻(通常称为取消点)停止正在做的工作,去做其他事情。
每个线程都有一个boolean类型的中断状态,在中断的时候该中断状态被设置为true,线程中断相关的方法如下:
public class Thread{ //中断目标线程 public void interrupt(){......} //返回目标线程的中断状态 public boolean isInterrupted(){......} //清除当前线程的中断状态,并返回它之前的值 public static boolean interrupted(){......} ...... }
特定阻塞库类的方法都支持中断,中断通常是实现线程取消最明智的选择。
(3).Executor和Future:
Executor线程池可以使用shutdown和shutdownNow方法来关闭线程池。
Future可以使用cancel方法取消任务。
(4).JVM关闭钩子:
在JVM正常关闭时,可以执行使用Runtime.addShutdownHook注册的尚未开始执行的线程(关闭钩子),例子代码如下:
public void start(){ Runtime.getRuntime().addShutdownHook(new Thread(){ public void run(){ try{ LogService.this.stop(); }catch(InterruptedException ignore){ } } }); }
JVM关闭钩子全部是并发执行,因此必须是线程安全,访问共享数据必须要同步,同时小心避免死锁。
JVM关闭钩子常用于服务或应用程序的清理,或者清除OS不能自动清除的资源。
下一篇: 开会笑因为有人在睡觉