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. hrbust oj 1526+2028 树状数组

    冒泡排序中 如果一个数的后面的某个数和这个数不符合排序规则 那么这个数就会在未来的某次冒泡中与那个数进行交换 这里用到了 树状数组求逆序数的办法来做 需要注意的是2028并不可以改完数组大小后直接套1 ...

  2. Jquery scrollTop animate 實現動態滾動到頁面頂部

    這個方法之前都是用的錨點實現的,但是效果僵硬,動感不足! 之後參考了一些網站,發現都是用的js,於是自己想到用jquery 來做一個插件也來實現以下這個小功能. $.fn.backTop = func ...

  3. SqlServer 高版本数据库 降级

    SQL Server2012 1 首先就来介绍一下SQL Server2012数据库的降级方法,成功之后突然感觉很简单,但是没成功之前很郁闷.废话少说,直接开始. 打开SQL Server2012,连 ...

  4. regardless of how many processors are devoted to a parallelized execution of this program

    https://en.wikipedia.org/wiki/Amdah's_law Amdahl's law is often used in parallel computing to predic ...

  5. mysql和oracle 分页查询(转)

    最近简单的对oracle,mysql,sqlserver2005的数据分页查询作了研究,把各自的查询的语句贴出来供大家学习..... (一). mysql的分页查询 mysql的分页查询是最简单的,借 ...

  6. oracle utf8字符集转gbk(转)

    近日有同事在外面部署系统时,安装数据库时可能选择了UTF-8编码格式,导入insert语句时,一个汉字被认为三个字节,这是不行的. 结合上网搜到的资料,将oracle数据库的编码格式,从utf-8改为 ...

  7. Python 虚拟环境Virtualenv

    本人也是Python爱好者,众所周知,Python扩展多,每次为了测试,安装各种各样的扩展,这样导致本地的Python环境非常混乱,就有人想到搞个隔离环境  和 本地环境没有关系,随时可以删除这个隔离 ...

  8. ajax普通弹窗;Bootstrp弹窗

    1.普通弹窗 主页面: <head> <meta http-equiv="Content-Type" content="text/html; chars ...

  9. python之列表切片(slice)

    使用索引获取列表的元素(随机读取) 列表元素支持用索引访问,正向索引从0开始 colors=["red","blue","green"] c ...

  10. oracle 表查询一

    通过scott用户下的表来演示如何使用select语句,接下来对emp.dept.salgrade表结构进行解说. emp 雇员表字段名称   数据类型       是否为空   备注-------- ...