6.1  在线程中执行任务

  应用程序提供商希望程序支持尽可能多的用户,从而降低每个用户的服务成本,而用户则希望获得尽可能快的响应。大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。

6.1.1  串行地执行任务

  在应用程序中可以通过多种策略来调度任务,而其中一些策略能够更好地利用潜在的并发性。最简单的策略就是在单个线程中串行地执行各项任务。

  程序清单 6-1 :串行的 Web 服务器

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

  SingleThreadWebServer  很简单,且在理论上是正确的,但在实际生产环境中的执行性能却很糟糕,因为它每次只能处理一个请求。

  在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。也有一些例外,例如,当任务数量很少且执行时间长时,或者当服务器只为单个用户提供服务,并且该客户每次只发出一种请求。

6.1.2  显示地为任务创建线程

  通过为每一个请求创建一个新的线程来提供服务,从而实现更高的响应性,如程序清单 6-2 中的 ThreadTaskWebWebServer 所示。

  程序清单 6-2:在 Web 服务器中为每个请求启动一个型的线程。

public class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable bleck = new Runnable() {
public void run() {
//handleRequest(connection);
         }
       };
     }
  }
}

  对比 ThreadPerTaskWebServer 和 SingleThreadWebServer 区别在于,对于每个连接,主循环都将创建一个新线程来处理请求,而不是在主循环中进行处理。因此可得出三个结论:

  • 任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接。这使得程序在完成前面的请求之前可以接受新的请求,从而提高响应性。
  • 任务可以并行处理,从而能同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,例如等待 I/O 完成、获取锁或者资源可用性等,程序的吞吐量将得到提高。
  • 任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。

6.1.3  无限制创建线程的不足

  在生产环境中,“为每个任务分配一个线程” 这种方法存在一些缺陷,尤其是当需要创建大量的线程时:

  • 线程生命周期的开销非常高:线程的创建与销毁并不是没有代价的。根据平台的不同,实际的开销也有所不同,但线程的创建过程都会需要时间,延迟处理的请求,并且需要 JVM 和操作系统提供一些辅助操作。如果请求的到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为每个请求创建一个新线程将消耗大量的计算资源。
  • 资源消耗:活跃的线程会消耗系统资源,尤其是内存。如果你已经拥有足够多的线程使 CPU 保持忙碌状态,那么再创建更多的线程反而会降低性能。
  • 稳定性:在可创建线程的数量上存在一个限制。这个限制随着平台的不同而不同,并且受到多个限制约束,包括 SVM 的启动参数、Thread 构造函数中请求的栈大小,以及底层操作系统对线程的限制等。如果破坏这些限制,则抛出 OutOfMemoryError 异常。

6.2  Executor 框架

   我们已经分析了两种通过线程来执行任务的策略,即把所有任务放在单个线程中串行执行,以及将每个任务放在各自的线程中执行。这两种方式都存在一些严格的限制:串行执行的问题在于其糟糕的响应性和吞吐量,而 “为每个任务分配一个线程” 的问题在于资源管理的复杂性。在第五章中,我们介绍了如何通过有界队列来防止高负荷的应用程序耗尽内存。线程池简化了线程的管理工作,并且 java.util.concurrent 提供了一种灵活的线程池实现作为 Executor 框架的一部分。在Java 类库中,任务执行的主要抽象不是 Thread,而是 Executor,如程序清单6-3:Executor 接口

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

6.2.1  示例:基于 Executor  的 Web 服务器

  基于 Executor 来构建 Web 服务器是非常容易的。在程序清单 6-4 中用 Executor 代替了硬编码的线程创建。在这种情况下使用了一种标准的 Executor 实现,即一个固定长度的线程池,可以容纳 100 个线程。

public class TaskExecutingWebServer {
private static final int NTHREADS = 100;
private static final Executor exe = 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);
}
};
exe.execute(task);
}
}
}

  我们可以很容易地将 TaskExecutionWebServer 修改为类似 ThreadPerTaskWebServer 的行为,只需使用一个为每个请求都创建新线程的 Executor。程序清单 6-5:为每个请求启动一个新线程的 Executor

public class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}

  同样,我们可以编写一个 Executor 使 TaskExecutionWebServer 的行为类似于单线程的行为,如程序清单 6-6:在调用线程中以同步方式执行所有任务的 Executor

public class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}
每当看到下面这种形式的代码时:
new Thread(rennable).start();
并且你希望获得一种更灵活的执行策略时,请考虑使用 Executor 来代替 Thread

6.2.3  线程池

  “在线程池中执行任务” 比 “为每个任务分配一个线程” 优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。

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

  • newFixedThreadPool:将创建一个固定长度的线程池。(如果某个线程由于发生了未预期的 Exception 而结束,那么线程池会补充一个新的线程)。
  • newCachedThreadPool:将创建一个可缓存的线程池,如果线程池当前规模超过了处理需求,那么回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池规模不存在任何限制。
  • newSingleThreadExecutor:一个单线程的 Executor,它创建单个工作线程来执行任务,如果线程异常结束,会创建另一个线程来替代。
  • newScheduledThreadPool:创建一个固定长度的线程池,而且延迟或定时的方式来执行任务,类似 Timer。

6.2.4  Executor 的生命周期

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

  当关闭应用程序时,可能采用最平缓的关闭形式(完成所有已经启动的任务,并且不再接受任何新的任务),也可能采用最粗暴的关闭形式(直接关闭电脑),以及其他各种可能的形式。

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

  程序清单 6-7:ExecutorService 中的生命周期管理方法

public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long var1, TimeUnit var3) throws InterruptedException;
// ... 其他用于任务提交的便利方法
}

  shutdown 方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成 -- 包括那些还未开始执行的任务。shutdownNow 方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。

  那么我们尝试吧生命周期管理扩展到 Web服务器的功能。 程序清单 6-8:支持关闭操作的 Web 服务器

public class LifecycleWebServer {
private final ExecutorService exe = ...;
public void start() throws IOException {
ServerSocket socket = new ServerSocket(80);
while (!exe.isShutdown()) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
@Override
public void run() {
//handleRequest(connection);
}
};
exe.execute(task);
}
}
public void stop() {
exe.shutdown();
}
void handleRequest(Socket connection) {
Request req = readRequest(connection);
if (isShutdownRequest(connection)) {
stop();
} else {
dispatchrequest(热情);
}
}
}

6.3  找出可利用的并行性

  本节我们将开发一些不同版本的组件,该示例实现浏览器程序中的页面渲染(Page-Rendering)功能,它的作用是将 HTML 页面绘制到图像缓存中。为了简便,假设 HTML 页面只包含标签文本,以及预定义大小的图片和 URL。

6.3.1  示例:串行的页面渲染器

  最简单的方式是对 HTML 文档进行串行处理,但这种方法可能会令用户感到烦恼,它们必须等待很长时间。另一种串行执行方法更好一些,它先绘制文本元素,同时为图像预留出矩形的占位空间,在处理完了第一遍文本后,程序再开始下载图像,并将它们绘制到相应的占位空间中。

  程序清单 6-10:串行地渲染页面元素

public class SingleThreadRender {
void rederPage(CharSequence source) {
renderText(source);
List<ImageData> imageDataList = new ArrayList<ImageData>();
for (ImageInfo imageInfo : scanFoeImageInfo(source)) {
imageDataList.add(imageInfo.downloadImage());
}
for (ImageData image : imageDataList) {
rederImage(image);
}
}
}

6.3.2  携带结果的任务 Callable 与 Future

  许多任务实际上都是存在延迟的计算----执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务,Callable 是一种更好的抽象:它认为主入口点(即 call)将返回一个值,并可能抛出异常。在Executor 中包含了一些辅助方法能将其他类型的任务封装为一个 Callable ,例如 Runable 和 java.security.privilegedAction。

  程序清单 6-11:Callable 与 Future 接口

public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean var1);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException;
}

  

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

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

  程序清单 6-13:使用 Future 等待图像下载

public class FutureRender {
private final ExecutorService executor = ...;
void rederPage(CharSequence source) throws Exception{
final List<ImageInfo> imageInfoList = scanFoeImageInfo(source);
Callable<List<ImageData>> task = new Callable<List<ImageData>>() {
public List<ImageData> call() {
List<ImageData> imageDataList = new ArrayList<ImageData>();
for (ImageInfo imageInfo : imageInfoList) {
imageDataList.add(imageInfo.downloadImage());
}
return imageDataList
}
};
Future<List<ImageData>> future = executor.submit(task);
renderText(source); List<ImageData> imagedata = future.get();
for (ImageData image : imagedata) {
rederImage(image);
}
}
}

6.3.6  示例:使用 CompletionService 实现页面渲染器

  可以通过 CompletionService 从两个方面来提高页面渲染器的性能:缩短总运行时间以及提高响应性。为每一幅图像的下载都创建一个独立任务,并在线程池中实行它们。

  程序清单 6-15:使用 CompletionService ,使页面元素在下载完成后立即显示出来

public class Render {
private final ExecutorService executor = ...;
Render(ExecutorService exe) {
this.executor = exe;
}
void rederPage(CharSequence source) throws Exception{
final List<ImageInfo> imageInfoList = scanFoeImageInfo(source);
CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageDara>(executor);
for (final ImageInfo info: imageInfoList) {
completionService.submit(new Callable<ImageData>() {
public List<ImageData> call() {
return info.downloadImage();
}
});
}
renderText(source); for (int i = 0, n = imageInfoList.size(); i < n; i++) {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
rederImage(imageData);
}
}
}

6.3.7  为任务设置时限

  程序清单 6-16:在指定时间内获取广告信息

Page RenderPageWithAd() throws Exception {
long endNanos = System.nanoTime() + TIME_BUDGET;
Future<Ad> f = exe.submit(new FetchAdTask());
//在等待广告的同时显示页面
Page page = renderPageBody();
Ad ad;
//指等待指定的时间长度
long timeLeft = endNanos - System.nanoTime();
ad = f.get(timeLeft, NANOSECONDS);
}

6.3.8  示例:批量 为任务设置时限

List<Future<Integer>> futures = exec.invokeAll(tasks, time, unit);

  ExecutorService 中 invokeAll 方法参数为一组任务,并返回一组 Future。

  

【Java并发.6】结构化并发应用程序的更多相关文章

  1. 使用async进行结构化并发程序开发

    异步风格的函数: 继续来学习async相关的东东,对于它其实可以用到函数上,也就是用它可以定义一个异步风格的函数,然后在该函数中再来调用普通的函数,下面来瞅一下: 其实“GlobalScope.asy ...

  2. JAVA并发编程学习笔记------结构化并发应用程序

    1. Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者,如果要在程序中实现一个生产者-消费者的设计,最简单的方式通常就是使用Executor 2. Exe ...

  3. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  4. 4、Java并发性和多线程-并发编程模型

    以下内容转自http://ifeve.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B/: 并发系统可以采用多种并发编程模型来实现. ...

  5. 大神为你分析 Go、Java、C 等主流编程语言(Go可以替代Java,而且最小化程序员的工作量,学习比较容易)

    本文主要分析 C.C++98.C++11.Java 与 Go,主要论述语言的关键能力.在论述的过程中会结合华为各语言编程专家和华为电信软件内部的骨干开发人员的交流,摒弃语言偏好或者语言教派之争,尽量以 ...

  6. Java 8 LongAdders:管理并发计数器的正确方式

    转自:http://www.importnew.com/11345.html 我只是喜欢新鲜的事物,而Java 8 有很多新东西.这次我想讨论其中我最喜欢的之一:并发加法器.这是一个新的类集合,他们用 ...

  7. Java多线程 阻塞队列和并发集合

    转载:大关的博客 Java多线程 阻塞队列和并发集合 本章主要探讨在多线程程序中与集合相关的内容.在多线程程序中,如果使用普通集合往往会造成数据错误,甚至造成程序崩溃.Java为多线程专门提供了特有的 ...

  8. Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)

    JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...

  9. Microsoft Orleans构建高并发、分布式的大型应用程序框架

    Microsoft Orleans 在.net用简单方法构建高并发.分布式的大型应用程序框架. 原文:http://dotnet.github.io/orleans/ 在线文档:http://dotn ...

随机推荐

  1. mumu模拟器安装xposed--如何在android模拟器上进行root

    问题描述 安装xposed表示failed to access root权限,新版的mumu模拟器没有了root选项,需要自己root. 1.先关掉应用兼容性,然后重启 电脑一般都是x86的,mumu ...

  2. apk公钥私钥用法

    每个密钥都包含两个文件:一个是扩展名为 .x509.pem 的证书,另一个是扩展名为 .pk8 的私钥.私钥需要加以保密,并用于对 apk 包进行签名.密钥本身也可能受密码保护.相比之下,证书只包含公 ...

  3. JS学习之路之JavaScript match() 方法

    match() 方法,在字符串内找到相应的值并返回这些值,()内匹配字符串或者正则表达式. 该方法类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置. d ...

  4. java应用系统运行速度慢的解决方法

    场景:我们在部署了TOMCAT应用,刚刚开始启动的一个段时间内.访问系统的速度比较快.但是过了一段时间,应用系统就慢慢的变慢起来了.服务的访问加载时间慢慢变长. 问题解决思路: 1,查看部署应用系统的 ...

  5. c/c++ 智能指针 shared_ptr 和 new结合使用

    智能指针 shared_ptr 和 new结合使用 用make_shared函数初始化shared_ptr是最推荐的,但有的时候还是需要用new关键字来初始化shared_ptr. 一,先来个表格,唠 ...

  6. python编写文件统计脚本

    python编写文件统计脚本 思路:用os模块中的一些函数(os.listdir().os.path.isdir().os.path.join().os.path.abspath()等) 实现功能:显 ...

  7. March 04th, 2018 Week 10th Sunday

    Tomorrow never comes. 我生待明日,万事成蹉跎. Most of my past failures can be chalked up to the bad habit of pr ...

  8. February 13th, 2018 Week 7th Tuesday

    You are your greatest asset. 你就是你自己最大的资本. For most of us, there are few things that we can count on ...

  9. Go学习笔记06-内建容器

    Go学习笔记06-内建容器 Go语言 数组 *切片(Slice) #F44336 Slice的操作 Map map示例 字符处理 数组 定义数组: //这样定义数组编译器自动初始化每个元素为0  va ...

  10. 【Linux常见问题】Centos7的网络配置问题

    在配置Centos7网络的时候,可能出出现虚拟机.本地以及外网三者之间ping不通的问题,可以从以下的几个方面排查: 1.确定需要管理员权限才能修改配置网络,如下图: 需要点下更改设置,然后出现下面的 ...