TPS00-J. 用线程池实现应用在流量暴涨时优雅降级

很多程序都要解决这样一个问题——处理一系列外来的请求。Thread- Per-Message这种设计模式是最简单的并发策略了,它为每一个请求创建一个线程。这种模式在耗时较长,受io约束,基于session或者,任务相互独立等场景下表现优于顺序处理。

但是,这种设计也有几个缺陷,包括线程创建和调用,任务处理,资源分配和释放,和频繁的上下文切换等带来的的开销。此外,攻击者可以通过一下子发起铺天盖地的请求造展开DoS攻击。系统不能优雅的降级,而是变得反应迟钝,从而导致拒绝服务。从安全的角度来看,由于一些偶现错误,一个组件可以用尽所有资源,饿死所有其他组件。

线程池允许系统在它承受力充裕的范围内处理尽可能多的请求。而不是一遇到过量的请求就挂掉。线程池通过限制初始化的工作线程数和同时运行的线程数来克服这些问题。每个支持线程池的对象都接受一个Runnable或Callable<T>的任务,并将其存储在临时队列中,直到有资源可用。因为在一个线程池的线程可以重复使用,并能快速的地加入线程池货从其中移出,管理线程的生命周期的开销被最小化。

不规范代码示例

这个示例演示了Thread-Per-Message设计模式,RequestHandler类提供了一个public static的工厂方法。调用者可以通过此方法获得Handler的实例,然后在各自的线程中调用handleRequest()处理请求。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket; class Helper {
public void handle(Socket socket) {
// ... }
}
} final class RequestHandler {
private final Helper helper = new Helper();
private final ServerSocket server; private RequestHandler(int port) throws IOException {
server = new ServerSocket(port);
} public static RequestHandler newInstance() throws IOException {
return new RequestHandler(0); // 自动获得端口
} public void handleRequest() {
new Thread(new Runnable() {
public void run() {
try {
helper.handle(server.accept());
} catch (IOException e) {
// Forward to handler
}
}
}).start();
}
}

Thread-Per-Message设计模式无法让服务优雅的降级。只要稀缺资源没有耗尽,系统还是可以正常的提供服务。举例来说,虽然可以创建很多的线程,系统中打开的文件描述符是有限的,文件描述符就是稀缺资源。如果我们的稀缺资源是内存,系统会突然的停止服务。

TPS01-J. 不要在有界线程池中运行互相依赖的任务

有界线程池是指其同一时刻执行任务的线程总数有上限。需要等待其他任务完成的任务不应该放到游街线程池中执行

有一种死锁叫做线程饿死型死锁,当线程池中的所有线程都在等待一个尚未开始的任务(只是进入了线程池的内部队列),这种死锁就会发生了

这种问题很有欺骗性,因为当需要的线程较少的时候程序可以正常的运行。有时候调大线程数就可以缓解这个问题。但是,确定合适的数量通常很难

不规范代码示例(子任务有依赖关系)

下面的例子有线程饿死的风险,本例包含一个ValidationService类负责执行各种检查,比如到后台的数据库检查用户输入的字段是否存在。

fieldAggregator()接受字符串类型的可变长的参数,并且为每一个参数创建一个任务,以便执行。每个任务使用ValidateInput执行验证。

反过来,ValidateInput 类将尝试为每个请求创建一个子任务,在任务重使用 SanitizeInput 类净化输入。在同一个线程池执行所有任务。在所有任务都执行完毕前FieldAggregator() 方法一直被阻塞,当所有结果都都可用了,FieldAggregator把返回结果汇总成 StringBuilder 对象返回给给调用者。

public final class ValidationService {
private final ExecutorService pool; public ValidationService(int poolSize) {
pool = Executors.newFixedThreadPool(poolSize);
} public void shutdown() {
pool.shutdown();
} public StringBuilder fieldAggregator(String... inputs)
throws InterruptedException, ExecutionException {
StringBuilder sb = new StringBuilder();
Future<String>[] results = new Future[inputs.length]; // 保存结果 for (int i = 0; i < inputs.length; i++) { // 把任务提交到线程池 results[i] = pool
.submit(new ValidateInput<String>(inputs[i], pool));
}
for (int i = 0; i < inputs.length; i++) { // 把结果汇总
sb.append(results[i].get());
}
return sb;
}
} public final class ValidateInput<V> implements Callable<V> {
private final V input;
private final ExecutorService pool; ValidateInput(V input, ExecutorService pool) {
this.input = input;
this.pool = pool;
} @Override
public V call() throws Exception {
// 如果验证失败,在此处抛出异常
Future<V> future = pool.submit(new SanitizeInput<V>(input)); // Subtask
return (V) future.get();
}
} public final class SanitizeInput<V> implements Callable<V> {
private final V input; SanitizeInput(V input) {
this.input = input;
} @Override
public V call() throws Exception {
// Sanitize input and return
return (V) input;
}
}

假设池大小设置为6,调用ValidationService.fieldAggregator()方法来验证6个参数,它提交6项任务的线程池。每个任务提交相应的子任务来清理输入。只要SanitizeInput子任务执行了,验证的线程就可以返回它们结果了,这些线程可以返回他们的结果。然而,这是不可能的,因为在线程池中所有六个线程,它们都被阻塞了。此外,由于还有活跃任务, 调用shutdown()方法无法关闭线程池。

标准代码示例(互相不依赖的任务)

在标准的代码中修改了ValidateInput<V>,SanitizeInput任务和它在同一个线程中执行。这样ValidateInput和SanitizeInput就独立开来,不再需要等待另一个执行结束。对SanitizeInput也做了修改,不再实现Callable接口

public final class ValidationService {
// ...
public StringBuilder fieldAggregator(String... inputs)
throws InterruptedException, ExecutionException {
// ...
for (int i = 0; i < inputs.length; i++) {
// 不把线程池传进去
results[i] = pool.submit(new ValidateInput<String>(inputs[i]));
}
// ...
}
} //不再使用同一个线程池
public final class ValidateInput<V> implements Callable<V> {
private final V input; ValidateInput(V input) {
this.input = input;
} @Override
public V call() throws Exception {
// 如果验证失败,抛出异常
return (V) new SanitizeInput().sanitize(input);
}
} public final class SanitizeInput<V> { // 不实现Callable接口
public SanitizeInput() {
} public V sanitize(V input) {
// 净化输入并返回
return input;
}
}

不规范代码(子任务)

本示例包含了一些列的子任务,运行在一个共享的线程池中。BrowserManager类调用perUser(), perUser()创建子任务来调用perProfile(), perProfile()又创建子任务来调用perTab(),最终perTab()又创建子任务调用doSomething()。BrowserManager等待所有的子任务结束。

public final class BrowserManager {
private final ExecutorService pool = Executors.newFixedThreadPool(10);
private final int numberOfTimes;
private static AtomicInteger count = new AtomicInteger(); // count = 0 public BrowserManager(int n) {
numberOfTimes = n;
} public void perUser() {
methodInvoker(numberOfTimes, "perProfile");
pool.shutdown();
} public void perProfile() {
methodInvoker(numberOfTimes, "perTab");
} public void perTab() {
methodInvoker(numberOfTimes, "doSomething");
} public void doSomething() {
System.out.println(count.getAndIncrement());
} public void methodInvoker(int n, final String method) {
final BrowserManager manager = this;
Callable<Object> callable = new Callable<Object>() {
public Object call() throws Exception {
Method meth = manager.getClass().getMethod(method);
return meth.invoke(manager);
}
};
Collection<Callable<Object>> collection = Collections.nCopies(n, callable);
try {
Collection<Future<Object>> futures = pool.invokeAll(collection);
} catch (InterruptedException e) {
// Forward to handler
Thread.currentThread().interrupt(); // Reset interrupted status
}
// ...
} public static void main(String[] args) {
BrowserManager manager = new BrowserManager(5);
manager.perUser();
}
}

不幸的是,这个方案很容易出现线程饿死型死锁。例如,5个perUser()任务,每个生5个perProfile()任务,每个perProfile()任务生成5个perTab()任务,线程池会被耗尽,没有剩余的线程给而perTab来调用doSomething()方法。

java.util.concurrent.ExecutorService.invokeAll(Collection<? extends Callable<Object>>) 会等待所有任务结束。

标准代码示例(CallerRunsPolicy)

在这个标准的解决方案中,对任务进行了选择和调度,,避免了线程饥饿死锁。它设置了ThreadPoolExecutor的CallerRunsPolicy,并且使用了SynchronousQueue。这个策略要求,如果线程池的线程耗尽,所有后继的任务都在其发起的线程中执行

public final class BrowserManager {
private final static ThreadPoolExecutor pool = new ThreadPoolExecutor(0, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
private final int numberOfTimes;
private static AtomicInteger count = new AtomicInteger(); // count = 0
static {
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
}
// ...
}

TPS02-J. 确保提交给一个线程池的任务是可中断的

TPS03-J. 确保线程池中的任务不会默默的失败

TPS04-J. 使用线程池时要确保ThreadLocal被重置

线程池——JAVA并发编程指南的更多相关文章

  1. Java工程师学习指南第4部分:Java并发编程指南

    本文整理了微信公众号[Java技术江湖]发表和转载过的Java并发编程相关优质文章,想看到更多Java技术文章,就赶紧关注本公众号吧吧. [纯干货]Java 并发进阶常见面试题总结 [Java基本功] ...

  2. Java并发编程指南

    多线程是实现并发机制的一种有效手段.在 Java 中实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable/Callable 接口. java.util.concurre ...

  3. Java并发编程系列-(6) Java线程池

    6. 线程池 6.1 基本概念 在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理.如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题:如果并发的请求数 ...

  4. Java并发编程系列-(2) 线程的并发工具类

    2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...

  5. Java并发编程系列-(7) Java线程安全

    7. 线程安全 7.1 线程安全的定义 如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的. 类的线程安全表现为: 操作的原子性 内存的可见性 不 ...

  6. Java并发编程系列-(5) Java并发容器

    5 并发容器 5.1 Hashtable.HashMap.TreeMap.HashSet.LinkedHashMap 在介绍并发容器之前,先分析下普通的容器,以及相应的实现,方便后续的对比. Hash ...

  7. Java并发编程系列-(4) 显式锁与AQS

    4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...

  8. Java并发编程系列-(3) 原子操作与CAS

    3. 原子操作与CAS 3.1 原子操作 所谓原子操作是指不会被线程调度机制打断的操作:这种操作一旦开始,就一直运行到结束,中间不会有任何context switch,也就是切换到另一个线程. 为了实 ...

  9. Java并发编程系列-(1) 并发编程基础

    1.并发编程基础 1.1 基本概念 CPU核心与线程数关系 Java中通过多线程的手段来实现并发,对于单处理器机器上来讲,宏观上的多线程并行执行是通过CPU的调度来实现的,微观上CPU在某个时刻只会运 ...

随机推荐

  1. Hrbustoj 2252 完全背包

    一个变形的完全背包 题是第一次团队赛的热身题...看别人博客看到这道题忽然就不会了 然后想了半天还是没想出来...上oj找了提交排名..发现自己弄出来的奇怪的办法居然用时最短... 问装m最低要多少的 ...

  2. maven仓库私服配置

    私服访问地址:[[http://192.168.1.252:9080/nexus/content/groups/public/ 地址]] 1. 打开eclipse/myeclipse的maven插件: ...

  3. PHP不能创建csv中文名文件

    使用iconv 将utf-8转换成gb2312即可$name = iconv("utf-8","gb2312",'分销商下载课件记录');

  4. solr4.7中文分词器(ik-analyzer)配置

    solr本身对中文分词的处理不是太好,所以中文应用很多时候都需要额外加一个中文分词器对中文进行分词处理,ik-analyzer就是其中一个不错的中文分词器. 一.版本信息 solr版本:4.7.0 需 ...

  5. 【转载】C内存分配

    一.预备知识—程序的内存分配  一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. ...

  6. CentOS 6 RPM安裝python 2.7

    先说第一种方法,通过rpmbuild编译XXX.src.rpm包([1].[2]): 安装依赖:sudo yum install -y make autoconf bzip2-devel db4-de ...

  7. 实时查看linux网卡流量

    将下列脚本保存为可执行脚本文件,比如叫traff.sh. 1.本脚本可自定义欲查看接口,精确到小数,并可根据流量大小灵活显示单位. 2.此脚本的采集间隔为1秒. 3.此脚本不需要额外再安装软件,可在急 ...

  8. php7安装

    # 配置参数 ./configure --prefix=/usr/local/php7 \ --with-config-file-path=/usr/local/php7/etc \ --with-m ...

  9. B-Tree indexs

    mysql_High.Performance.MySQL.3rd.Edition.Mar.2012 A B-Tree index speeds up data access because the s ...

  10. OpenGL完全教程 第一章 初始化OpenGL

    第一章 初始化OpenGL 无论是什么东西,要使用它,就必须对它进行初始化.如果你之前使用过GDI,你应该也多多少少了解到GDI在绘制图形之前要为之创建渲染环境.OpenGL也一样.本章给出的代码,大 ...