《java并发编程实战》读书笔记(三)

2019, Jul 16    

第6章任务执行

大多数并发应用程序是围绕执行任务(task)进行管理的。所谓任务就是抽象、离散的工作单元(unit of work)。把一个应用程序的工作(work)分离到任务中,可以简化程序的管理:这种分离还在不同事务间划分了自然的分界线,可以方便程序在出现错误时进行恢复:同时这种分离还可以为并行工作提供-一个自然的结构,有利于提高程序的并发性。

任务执行方式:

  • 顺序执行一次只能处理一个请求。资源利用率非常低。
  • 每任务每线程(无限制创建线程) 线程生命周期的开销 线程的创建与关闭不是“免费”的。实际的开销依据不同平台而不同,但是创建线程的确需要时间,带来处理请求的延迟,并且需要在JVM和操作系统之间进行相应的处理活动。如果请求是频繁的且轻量的,就像大多数服务器程序- -样,那么为每个请求创建-一个新线程的做法就会消耗大量的计算资源。 资源消耗量 活动线程会消耗系统资源,尤其是内存。如果可运行的线程数多于可用的处理器数,线程将会空闲。大量空闲线程占用更多内存,给垃圾回收器带来压力,而且大量线程在竞争CPU资源时,还会产生其他的性能开销。如果你已经有了足够多的线程保持所有CPU忙碌,那么再创建更多的线程是有百害而无一利的。 稳定性 应该限制可创建线程的数目。限制的数目依不同平台而定,同时也受到JVM的启动参数、Thread的构造函数中请求的栈大小等因素的影响,以及底层操作系统线程的限制”。如果你打破了这些限制,最可能的结果是收到一个OutofMemoryError。企图从这种错误中恢复是非常危险的;更简单的办法是构造你的程序时避免超出这些限制。

  • 线程池

Executor框架

线程池:线程池管理一个 工作者线程的同构池(homogeneous pool)。线程池是与工作队列(work queue)紧密绑定的。所谓工作队列,其作用是持有所有等待执行的任务。工作者线程的生活从此轻松起来:它从工作队列中获取下一个任务,执行它,然后回来继续等待另-一个线程。

类库提供了一个灵活的线程池实现和一些有用的预设配置。你可以通过调用Executors中的某个静态工厂方法来创建一个线程池:

  1. newFixedThreadPool创建一个定长的线程池,每当提交-一个任 务就创建一个线程,直到达到池的最大长度,这时线程池会保持长度不再变化(如果一个线程由于非预期的Except ion而结束,线程池会补充-一个新的线程)。
  2. newCachedThreadPool创建-个可缓存的线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活地回收空闲的线程,当需求增加时,它可以灵活地添加新的线程,而并不会对池的长度作任何限制。
  3. newSingleThreadExecutor创建一个 单线程化的executor,它只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它。executor会保证任务依照任务队列所规定的顺序(FIFO, LIFO, 优先级)执行。
  4. newScheduledThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer 。

第7章 取消和关闭

精灵线程

线程被分为两种:普通线程和精灵线程。JVM启动的时创建所有的线程,除了主线程以外,其他的都是精灵线程(比如垃圾回收器和其他类似线程)。当一个新的线程创建时,新线程继承了创建它的线程的后台状态,所以默认情况下,任何主线程创建的线程都是普通线程。

普通线程和精灵线程之间的差别仅仅在于退出时会发生什么。当一个线程退出时,JVM会检查-一个运行中线程的详细清单,如果仅剩下精灵线程,它会发起正常的退出。当JVM停止时,所有仍然存在的精灵线程都会被抛弃一不会执行 finally块,也不会释放栈一-JVM 直接退出。

精灵线程应该小心使用–在任何时候, 几乎没有哪些活动的处理可以在不进行清理的情况下,被安全地抛弃。特别是执行I/O操作的任务运行在精灵线程中是很危险的。精灵线程最好用于“家务管理(housekeeping) ”的任务,比如-一个背景线程可以从内存的缓存中周期性地移除过期的访问。

总结

任务、线程、服务以及应用程序在生命周期结束时的问题,可能会导致向它们引入复杂的设计和实现。Java 没有提供具有明显优势的机制来取消活动或者终结线程。它提供了协作的中断机制,能够用来帮助取消,但是这将取决你如何构建取消的协议,并是否能一致地使用该协议。使用FutureTask和Executor框架可以简化构建可取消的任务和服务。