第6章开始是第二部分,讲解结构化并发应用程序,大多数并发应用程序都是围绕“任务执行”构造的,任务通常是一些抽象的且离散的工作单元。

一、线程池

大多数服务器应用程序都提供了一种自然的任务边界:以独立的客户请求为边界。现在我们要实现自己的web服务器,你一定见过这样的代码:

class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
Socket connection = socket.accept();
//处理请求
handleRequest(connection);
}
}
}

这种串行的执行任务的方法当然不可行,它使程序失去可伸缩性。我们将它改进:

class ThreadPerTaskWebServer{
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
@Override
public void run() {
//处理请求
handleRequest(connection);
}
}; new Thread(task).start();
}
}
}

  我们为每个任务分配一个线程,但它仍存在很多问题,如,线程生命周期开销非常高、消耗过多的资源,尤其是内存、可创建线程的数量上有一个上限,如果超出,可能会抛出OutOfMemoryError异常。我在读这本书之前,最多也就是理解到这里。但其实java对任务执行提供了支持,也就是Executor。

public interface Executor{
void execute(Runnable command);
}

虽然Executor是一个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础。现在把上例修改为基于线程池的web服务器:

class TaskExecutionWebServer{
private static final int NTHREADS = 100;
private static final Executor 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() {
@Override
public void run() {
//处理请求
handleRequest(connection);
}
}; exec.execute(task);
}
}
}

每当看到下面这种形式的代码时:

new Thread(task).start();

并且你希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread。

  Executor基于生产者-消费者模式。提交任务的线程相当于生产者,执行任务的线程相当于消费者。生产者将任务提交给队列,消费者从队列中获得任务执行。这样将任务的提交过程与执行过程解耦。

  java提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors中的静态工厂方法来创建:

  1newFixedThreadPool newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化。(如果某个线程由于发生了未预期的Exception而结束,那么线程池将补充一个新的线程) 。

  2newCachedThreadPool newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲线程,当需求增加时,可以添加新的线程,线程池的规模不存在任何限制。

  3newSingleThreadExecutor newSingleThreadExecutor是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来代替。newSingleTheadExecutor能确保依照任务在队列中的顺序来串行执行。(例如:FIFO、LIFO、优先级)

  4、newScheduledThreadPool newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或者定时的方式执行任务。

二、Executor的生命周期

  我们已经知道如何创建一个Executor,但如何关闭它呢?Executor的实现通常会创建线程来执行任务。但JVM只有在所有(非守护)线程全部终止后才会退出。因此,如果无法正确关闭Executor,那么JVM将无法结束。

  为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法,如下:

public interface ExecutorServce implements Executor{
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout,TimeUnit unit) throws InterruptedException;
}

  ExecutorService的生命周期有3中状态:运行、关闭、已终止。ExecutorService在初始创建时处于运行状态。shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成。shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。

  我们再将web服务器改进成支持关闭的形式:

class LifecycleWebServer{
private final ExecutorService exec = Executors.newCachedThreadPool(); public void start() throws IOException{
ServerSocket socket = new ServerSocket(80);
while (!exec.isShutdown()) {
try {
final Socket conn = socket.accept();
exec.execute(new Runnable() {
@Override
public void run() {
handleRequest(conn);
}
});
} catch (RejectedExecutionException e) {
if (!exec.isShutdown()) {
log("task submission rejected",e);
}
}
}
} public void stop(){
exec.shutdown();
} void handleRequest(Socket connection){
Request req = readRequest(connection);
if (isShutdownRequest(req)) {
stop();
}else {
dispatchRequest(req);
}
}
}

三、携带结果的任务Callable与Future

  Executor框架使用Runnable作为其基本的任务表示形式。Runnable是一种有很大局限的抽象,它不能返回一个值或者抛出一个受检查的异常。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;
}

  如果任务已经完成,那么get会立即返回或者抛出一个Exception,如果任务没有完成,那么get将阻塞并直到任务完成。如果任务抛出了异常,那么get将该异常封装为ExecutionException并重新抛出。如果任务被取消,那么get将抛出CannellationException。如果get抛出了ExecutionException,那么可以通过getCause来获得被封装的初始异常。ExecutorService中的所有方法都将返回一个Future。

例子:使用Future实现页面渲染器

  为了使页面渲染器实现更高的并发性,首先将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图像。(因为其中一个任务是CPU密集型,另一个任务是I/O密集型,因此这种方法即使在单CPU系统上也能提升性能。)

  伪代码:

public class FutureRenderer{
private final ExecutorService executor = Executors.newCachedThreadPool(); void renderPage(CharSequence source){
final List<ImageInfo> imageInfos = scanForImageInfo(source);
Callable<List<ImageData>> task =
new Callable<List<ImageData>>() {
@Override
public List<ImageData> call() throws Exception {
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> imageData = future.get();
for (ImageData data : imageData) {
renderImage(data);
}
} catch (InterruptedException e) {
//重新设置线程的中断状态
Thread.currentThread().interrupt();
//由于不需要结果,因此取消任务
future.cancel(true);
}catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}

  FutureRenderer中创建了一个Callable来下载所有的图像,并将其提交到一个ExecutorService。这将返回一个描述任务执行情况的Future。当主任务需要图像时,它会等待Future.get的调用结果。如果幸运的话,当请求开始时所有的图像都已经下载完成了,即使没有,至少图像的下载任务也已经提前开始了。

  问题:如果渲染文本的速度远远高于下载图像的速度,那么程序的最终性能与串行执行时的性能差别不大,而代码却变得更复杂了。所以,只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。

  解决:为每一幅图像的下载都创建一个独立任务,并在线程池中执行它们。如果向线程池提交一组任务,并希望在计算完成后获得结果,可以保留与每个计算结果相关联的Future,然后反复的使用get方法,然后通过轮询判断任务是否已经完成,这种方法可行,但很繁琐。幸运的是,还有一种更好的方法:完成服务(CompletionService)。

  CompletionService将Executor和BlockingQueue的功能融合在一起。你可以将Callable任务交给它来执行,然后使用类似于队列操作的take和poll方法来获得已经完成的结果。ExecutorCompletionService实现了CompletionService。

public class Render {
private final ExecutorService executor; public Render(ExecutorService executor) {
super();
this.executor = executor;
} void renderPage(CharSequence source){
List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService =
new ExecutorCompletionService<ImageData>(executor);
//提交每张图片的下载任务
for (final ImageInfo imageInfo: info) {
completionService.submit(new Callable<ImageData>(){
public ImageData call(){
return imageInfo.downloadImage();
}
});
}
//加载文本
renderText(source); try {
for (int t = 0 t = info.size(); t < n ;t++) {
//获得已经完成的任务,每次获得一个
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
//线程中断会执行两个操作:1.清除线程的中断状态 2.抛出InterruptedException
//所以捕获异常后使该线程仍处于中断状态
Thread.currentThread().interrupt();
}catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}

java并发基础(三)--- 任务执行的更多相关文章

  1. Java 并发基础

    Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...

  2. java并发基础(五)--- 线程池的使用

    第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...

  3. java并发基础(二)

    <java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...

  4. Java并发基础概念

    Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...

  5. java并发基础及原理

    java并发基础知识导图   一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...

  6. 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!

    本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...

  7. 【Java并发基础】并发编程领域的三个问题:分工、同步和互斥

    前言 可以将Java并发编程抽象为三个核心问题:分工.同步和互斥. 这三个问题的产生源自对性能的需求.最初时,为提高计算机的效率,当IO在等待时不让CPU空闲,于是就出现了分时操作系统也就出现了并发. ...

  8. Java并发基础:进程和线程之由来

    转载自:http://www.cnblogs.com/dolphin0520/p/3910667.html 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程. ...

  9. java并发基础(六)--- 活跃性、性能与可伸缩性

    <java并发编程实战>的第9章主要介绍GUI编程,在实际开发中实在很少见到,所以这一章的笔记暂时先放一放,从第10章开始到第12章是第三部分,也就是活跃性.性能.与测试,这部分的知识偏理 ...

  10. java并发基础(一)

    最近在看<java并发编程实战>,希望自己有毅力把它读完. 线程本身有很多优势,比如可以发挥多处理器的强大能力.建模更加简单.简化异步事件的处理.使用户界面的相应更加灵敏,但是更多的需要程 ...

随机推荐

  1. Ubuntu django+nginx 搭建python web服务器文件日志

    uwsgi 配置文件 [uwsgi] http-socket = 127.0.0.1:8080 # 项目目录 chdir=/home/ubuntu/mkweb # 指定项目的application m ...

  2. Myeclipse 工具优化 [内存一直增加, jsp,javascript 编辑很卡]

    首先看这篇随笔 地址: Myeclipse/STS 首次在本地部署配置一个Spring MVC 项目 (十二) [http://www.cnblogs.com/editor/p/3915239.htm ...

  3. PHP删除数组中空值

    array_filter   函数的功能是利用回调函数来对数组进行过滤,一直都以为用回调函数才能处理, 却没有发现手册下面还有一句,如果没有回调函数,那么默认就是删除数组中值为false的项目 代码: ...

  4. 用Java构建一个简单的WebSocket聊天项目之新增HTTP接口调度

    采用框架 我们整个Demo基本不需要大家花费太多时间,就可以实现以下的功能. 用户token登录校验 自我聊天 点对点聊天 群聊 获取在线用户数与用户标签列表 发送系统通知 首先,我们需要介绍一下我们 ...

  5. CUDA性能优化----warp深度解析

    本文转自:http://blog.163.com/wujiaxing009@126/blog/static/71988399201701224540201/ 1.引言 CUDA性能优化----sp, ...

  6. 记关于webpack4下css提取打包去重复的那些事

    注意使用vue-cli3(webpack4),默认小于30k不会抽取为公共文件,包括css和js,已测试 经过2天的填坑,现在终于有点成果 环境webpack4.6 + html-webpack-pl ...

  7. list(列表)操作【五】

    L表示从左边(头部)开始插与弹出,R表示从右边(尾部)开始插与弹出. 一.概述:      在Redis中,List类型是按照插入顺序排序的字符串链表.和数据结构中的普通链表一样,我们可以在其头部(l ...

  8. java中URL 的编码和解码函数

    java中URL 的编码和解码函数java.net.URLEncoder.encode(String s)和java.net.URLDecoder.decode(String s);在javascri ...

  9. pip --version问题

    安装pip之后,在cmd下输入 pip --version始终提示: Unknown option:versionDid not provide a command自己安装步骤没错,怎么想也不明白,无 ...

  10. hdu 2065(泰勒展式)

    比赛的时候遇到这种题,只能怪自己高数学得不好,看着别人秒.... 由4种字母组成,A和C只能出现偶数次. 构造指数级生成函数:(1+x/1!+x^2/2!+x^3/3!……)^2*(1+x^2/2!+ ...