关于《Java并发编程实战》 – 第二部分的阅读笔记
第二部分: 结构化并发应用程序
第二部分主要介绍了: 如何利用线程来提高并发应用程序的吞吐量或响应性。
第六章介绍了如何识别可并行执行的任务,以及如何在任务执行框架中执行它们。第七章介绍了如何使用任务和线程在执行完正常工作前提起结束。在健壮的并发应用程序与看似能正常工作的应用程序之间存在的重要差异之一就是: 如何实现取消以及关闭等操作。 第八章介绍了任务执行框架中的一些高级特性。第九章介绍了如何提高单线程子系统的响应性。
第六章:任务执行
本章介绍了如何识别可并行执行的任务,以及如何在任务执行框架中执行它们。
01 . 最简单的方式就是在单个线程中串行的执行各项任务。
class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true){
Socket connection = socket.accept();
handleRequest(connection);
}
}
}
缺点: 在服务器应用程序中,串行处理机制通常无法提供高吞吐率和快速响应性。
02 . 显式的为任务创建线程
示例: 为每个请求创建一个线程
class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true){
Socket connection = socket.accept();
while(true) {
final Socket connection = socket.accept();
Runnable task = new Runnable(){
public void run(){
handleRequest(connection);
}
};
new Thread(task).start();
}
}
}
03 . 创建过多线程产生的问题:
- 线程生命周期的开销非常高: 线程的创建和销毁并不是没有代价的。
- 资源消耗: 活跃的线程会消耗系统资源,尤其是内存。
- 稳定性: 在可创建线程的数量上存在一个限制。这个限制随着平台的不同而不同。
二 、 Executor框架
01 . 由于上面的一系列原因,这章引入了Executor框架来解决问题。
Executor接口代码
public interface Executor {
void execute(Runnable command);
}
优点:
- 为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。
- 提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。
- Executor的实现还提供了对生命周期的支持、统计信息收集、应用程序管理机制和性能监视等机制。
02 . 一种标准的Executor 的实现
class TaskExecutionWebServer {
private static final int MTHREADS = 100;
private static final Executor exec = Executors.newFixedThreadPool(MTHREADS);
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true){
Socket connection = socket.accept();
while(true) {
final Socket connection = socket.accept();
Runnable task = new Runnable(){
public void run(){
handleRequest(connection);
}
};
exec.execute(task);
}
}
}
优点: 通过使用Executor,将请求处理任务的提交和执行解耦开来。并且只需采用另一种不同的Executor实现,就可以改变服务器的行为。
下面程序是为每个请求启动一个新线程的Executor
public class ThreadPerTaskExecutor implements Executor {
public void executor(Runnable r) {
new Thread(r).start();
}
}
下面程序以同步的方式执行所有任务的Executor
public class WithinThreadExecutor implements Executor {
public void execute(Runnable r) {
r.run();
};
}
03 . 执行策略
在执行策略中定义了任务执行的以下方面:
- 在什么线程中执行任务?
- 任务按照什么顺序执行(FIFO、LIFO、优先级)?
- 有多少个任务能并发执行?
- 在队列中有多少个任务在等待执行?
- 如果系统过载而需要拒绝一个任务,那么应该选择那个任务?如何通知应用程序有任务被拒绝?
- 在执行一个任务前后,应该进行哪些动作?
04 . 线程池
用线程池解决任务策略的问题,从“为每个任务分配一个线程”策略变成了基于线程池的策略。
线程池的优点:
- 重用现在有的线程,而不是新建线程。减少创建和销毁线程时的巨大消耗。
- 当请求到达时,工作线程已经存在,不会由于等待创建线程而延迟任务执行,提高响应性。
类库提供了一个灵活的线程池:
- newFixedThreadPool 将创建一个固定长度的线程池,如果有线程发生了为预期的Exception而结束,那么线程池会补充一个新的线程。
- newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求,那么将回收空闲线程,而当需求增加时,则可以添加新的线程。线程池的规模没有任何限制。
- newSingleThreadExecutor。创建一个单线程的Executor,它创教育一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替换。这样做可以确保依照队列中的顺序来串行执行。
- newScheduledThreadPool创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务。
05 . Executor的生命周期
生命周期一共有三种状态: 运行、关闭、已终止。
为了解决执行服务器的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法。
示例: ExecutorService中的生命周期管理方法
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//...其他一些用于任务提交的便利方法。
}
shutdown方法将执行平缓的关闭过程:不再接受新任务,同时等待已经提交的任务执行完成(包括那些未开始执行的任务)。
shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并不再启动队列中尚未开始的任务。
06 . 线程池提供多线程来执行延迟任务和周期任务。
三 、 找出可利用的并行性
在大多数服务器应用程序中都存在一个明显的任务边界:单个客户请求。
01 . 示例: 串行的页面渲染器: 它先绘制文本元素,同时为图像预留出矩形的占位空间,在处理完了第一遍文本以后,程序再开始下载图像,并将它们绘制到相应的占位空间中。
public class SingleThreadRenderer {
void renderPage(Charsequence source) {
renderText(source);
List<ImageInfo> imageData = new ArrayList<ImageData>();
for(ImageInfo imageInfo : scanForImageInfo(source)){
imageData.add(imageInfo.downloadImage());
}
for (ImageData data : imageData)
renderImage(data);
}
}
这种做法的缺点: 图片下载过程都是在等待I/O操作执行完成,这期间cpu几乎不做任何工作。因此,这种串行执行的方式没有充分利用cpu。我们可以通过将问题分解为多个独立的任务并发执行,能够获得更高的cpu利用率和响应灵敏度。
解决办法:使用CompletionService实现页面渲染器
通过使用CompletionService从俩个方面来提高页面渲染器的性能: 缩短总运行时间以及提高响应性。为每幅图像都创建一个独立的下载任务,并在线程池中执行他们,从而将串行下载过程转换为并行的过程:这将减少下载图像总时间。此外,每张图片下载完成后都会立刻显示出来,能使用户获得一个更加动态和更高响应性的用户界面。
示例: 使用CompletionService,使页面元素在下载完成后立即显示出来
public class Renderer{
private final ExecutorService executor;
Renderer(ExecutorService executor) { this.executor = executor ; }
void renderPage(CharSequence source) {
List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService =
new ExecutorCompletionService<ImageDate>(executor);
for(final ImageInfo imageInfo : info)
completionService.submit(new Callable<ImageData> {
public ImageData call(){
return imageInfo.downloadImage();
}
} );
renderText(source);
try {
for(int t = 0, n = info.size() ; t < n; t ++ ) {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e){
thread.currentThread().interrupt();
} catch (ExecutionExeption e) {
throw launderThrowable(e.getCase());
}
}
}
02 . 携带结构的任务Callable与Future
许多任务实际都是存在延迟计算—-执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务:
Callable是一种更好的抽象:它任务主入口点(即call)将返回一个值,并可能抛出一个异常。
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在Future规范中包含的隐含信息意义是,任务的生命周期只能前进,不能后退,当某个任务完成后,它就永远停留在“完成”状态上。
示例: Callable和Future接口
public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIFRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException,ExecutionException,CancellationException;
V get(long timeout, TimeUnit unit)
throws InterruptedException,ExecutionException,CancellationException,TimeoutException;
}
第七章:取消与关闭
本章介绍了如何使用任务和线程在执行完正常工作前提起结束。在健壮的并发应用程序与看似能正常工作的应用程序之间存在的重要差异之一就是: 如何实现取消以及关闭等操作。
01 . Java并没有提供任何机制来安全的终止线程,但是提供了“中断”,这是一种协作机制。要想取消任务的执行,我们可以设置一个boolean变量的标志,并设置为volatile类型(保证线程间同步)。
示例: 使用volatile类型的域来保存取消状态
@ThreadSafe
public class PrimeGenerator implements Runnable {
@GuardedBy("this")
private final List<BigInteger> primes = new ArrayList<BigInteger>();
private volatile boolean cancelled;
public void run(){
BIgInteger p = BigInteger.ONE;
while(!cancelled ) {
p = p.nextProvavlePrime();
synchronized (this) {
primes.add(p);
}
}
}
public void cancel() {cancelled = true; }
public synchronized List<BigInteger> get(){
return new ArrayList<BigInteger>(primes);
}
}
02 . 通常中断是实现取消的最合理的方式
示例: 通过中断来取消
class PrimeProducer extends Threads {
private final BlockingQueue<BigInteger> queue;
primeProducer(BlockingQueue<BigInteger> queue){
this.queue = queue;
}
public void run() {
try{
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted())//线程中断来实现取消
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
/* 允许线程退出 */
}
}
public void cancel(){ interrupt(); }//调用线程中断方法
}
第八章:线程池的使用
本章介绍了任务执行框架中的一些高级特性。
一 、 在任务和策略之间的隐性耦合
01 . 我们已经知道,Executor框架可以将任务的提交与执行策略解耦开来。但有些任务需要明确指出执行策略,包括:
依赖性任务 : 当在线程池中执行独立的任务时,可以随意的改变线程池的大小和配置,这些修改只会对执行性能产生影响。然而,如果提交给线程的任务需要依赖其他的任务,那么就隐含的对执行策略产生影响。
使用线程封闭机制的任务 : 使用线程封闭机制的任务要求其执行所在的Executor是单线程的,如果将Executor从单线程环境改为线程池环境,那么将会实现线程安全性。
对响应时间敏感的任务 : 如果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将降低由该Executor管理的服务的响应性。
- 使用ThreadLocal的任务 : ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本”。只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义。
02 . 线程饥饿死锁: 在线程池中,如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,那么会发生同样的问题。这种现象被称为线程饥饿死锁。
03 . 运行时间较长的任务: 如果任务阻塞时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。执行时间较长的任务不仅会造成线程池阻塞,甚至还会增加执行时间较短的任务的服务时间。
二、 设置线程池的大小
01 . 线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置文件提供,或者根据Runtime.availableProcessors来动态计算。
02 . 线程池只需要避免“过大”或者“过小”这俩种极端的情况就可以了。
三、 线程工厂
01 . 每当线程池需要创建一个线程的时候,都是通过线程工厂方法来完成的。
ThreadFactory接口
public interface ThreadFactory {
Thread newThread(Runnable r);
}
然而在许多情况下,我们都需要使用自定义工厂:
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable runnable){
return new myAppThread(runnable,poolName);
}
}
第九章: 图像用户界面应用程序
第九章介绍了如何提高单线程子系统的响应性。
一、为什么GUI是单线程
01 . 所有GUI框架基本上都实现为单线程的子系统,其中所有与表现相关的代码都作为任务在事件线程中运行。
02 . 早期的GUI应用程序都是单线程的,并且GUI事件在“主事件循环”进行处理。当前的GUI框架则使用了一种略有不同的模型: 在该模型中创建了一个专门事件分发线程来处理GUI事件。
03 . 在多线程的GUI框架中更容易发生死锁问题,其部分原因在于,在输入事件的处理过程与GUI组件的面向对象模型之间会存在错误的交互。另一个在多线程GUI框架中导致死锁的原因就是“模型 - 视图 - 控制(MVC)”的广泛使用,这进一步增加了出现不一致锁定顺序的风险。
二、短时间的GUI任务
01 . 短时间的任务可以把整个操作都放在事件线程中执行,而对于长时间的任务,则应该将某些操作放到另一个线程中执行。对于长时间的任务,可以使用缓存线程池。