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. POJ 2299 树状数组+离散化求逆序对

    给出一个序列 相邻的两个数可以进行交换 问最少交换多少次可以让他变成递增序列 每个数都是独一无二的 其实就是问冒泡往后 最多多少次 但是按普通冒泡记录次数一定会超时 冒泡记录次数的本质是每个数的逆序数 ...

  2. visual studio 中使用的插件介绍

    Highlight all occurrences of selected word 高亮代码 Indent Guides 代码的开头结尾连接竖线..是代码更清洗 PHP Tools for visu ...

  3. Apache服务器安装配置

    Apache服务器安装 1.Apache服务器安装      在Linux系统下,apache服务器的安装方式比较灵活,可以使用二进制包安装,比如:rpm包.deb包.已编译好的包.也可以简单的使用y ...

  4. 图片lightbox2

    1. 官网下载 http://lokeshdhakar.com/projects/lightbox2/ 2.引入 css  jquery  js 3. HTML格式 <a href=" ...

  5. Translation Lookaside Buffer

    COMPUTER ORGANIZATION AND ARCHITECTURE DESIGNING FOR PERFORMANCE NINTH EDITION In principle, then, e ...

  6. Flink - Generating Timestamps / Watermarks

    https://ci.apache.org/projects/flink/flink-docs-release-1.0/apis/streaming/event_timestamps_watermar ...

  7. svn update 失败 且 clean up失败解决方法

    当出现这种情况时,大多数是因为svn的数据库还有队列没处理完.而我们要做到仅仅是把队列清除即刻. 具体做法是: 1.下载sqlite3.exe下载地址:sqlite官网http://www.sqlit ...

  8. php源码安装

    要用swoole,首先需要有PHP环境.由于swoole的某些特性,最好是能够从源码编译安装PHP,这样在使用过程中可以避免很多不必要的错误.PHP下载地址:http://php.net/在这里挑选你 ...

  9. SQL Server存储机制

    1.区段 区段(extent)是用来为表和索引分配空间的基本存储单元.它由8个连续的64KB数据页组成. 基于区段(而不是实际使用空间)分配空间的概念的要点: 一旦区段已满,那么下一记录将要占据的空间 ...

  10. 汇编查看StackFrame栈帧

    INCLUDE Irvine32.inc myProc PROTO, x:DWORD, y:DWORD .data .code main proc mov eax,0EAEAEAEAh mov ebx ...