1.前言
高并发场景下不可避免需要使用多线程来处理任务,为了高性能的使用多线程,就需要用线程池来帮我们管理多个线程。简单介绍一下线程池的参数和用法,以及项目中的实例和优化建议。
2.线程池参数解释
引用:ThreadPoolExecutor线程池核心参数详解
ThreadPoolExecutor与线程相关的几个成员变量是:keepAliveTime、allowCoreThreadTimeOut、poolSize、corePoolSize、maximumPoolSize,它们共同负责线程的创建和销毁。
1、corePoolSize:核心线程数
* 核心线程会一直存活,及时没有任务需要执行
* 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
* 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
2、queueCapacity:任务队列容量(阻塞队列)
* 当核心线程数达到最大时,新任务会放在队列中排队等待执行
3、maxPoolSize:最大线程数
* 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
* 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
4、 keepAliveTime:线程空闲时间
* 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
* 如果allowCoreThreadTimeout=true,则会直到线程数量=0
5、allowCoreThreadTimeout:允许核心线程超时
6、rejectedExecutionHandler:任务拒绝处理器
* 两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
* 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
* ThreadPoolExecutor类有几个内部实现类来处理这类情况:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
* 实现RejectedExecutionHandler接口,可自定义处理器
新提交一个任务时的处理流程:
1、如果当前线程池的线程数还没有达到基本大小(poolSize < corePoolSize),无论是否有空闲的线程新增一个线程处理新提交的任务;
2、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列未满时,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);
3、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列满时;
3.1、当前poolSize<maximumPoolSize,那么就新增线程来处理任务;
3.2、当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。
**注意:** 在压力很大的情况下,线程池中的所有线程都在处理新提交的任务或者是在排队的任务,这个时候线程池处在忙碌状态。如果压力很小,那么可能很多线程池都处在空闲状态,这个时候为了节省系统资源,回收这些没有用的空闲线程,就必须提供一些超时机制,这也是线程池大小调节策略的一部分。通过corePoolSize和maximumPoolSize,控制如何新增线程;通过allowCoreThreadTimeOut和keepAliveTime,控制如何销毁线程。
3.线程池用法实例–Guava
1.创建和初始化线程池
初始化线程池
private static ThreadPoolExecutor doThreadPoolExecutor(int coreSize, int maxSize, int queueSize, String threadName, long timeOut) {
//线程名
String threadNameStr = new StringBuilder(threadName).append("-%d").toString();
//**ThreadFactoryBuilder**:线程工厂类就是将一个线程的执行单元包装成为一个线程对象,比如线程的名称,线程的优先级,线程是否是守护线程等线程;guava为了我们方便的创建出一个ThreadFactory对象,我们可以使用ThreadFactoryBuilder对象自行创建一个线程.
ThreadFactory threadNameVal = new ThreadFactoryBuilder().setNameFormat(threadNameStr).build();
//线程池
return new ThreadPoolExecutor(coreSize, maxSize, timeOut, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(queueSize), threadNameVal, new ThreadPoolExecutor.AbortPolicy());
}
使用guava提供的MoreExecutors来装饰线程池,以提供简便而强大的回调功能
public static volatile ThreadPoolExecutor executor;
public static volatile ListeningExecutorService listeningExecutor;
/** * 当前可用CPU数 */
//如何合理地估算线程池大小?http://ifeve.com/how-to-calculate-threadpool-size/
private static final int PROCESSORS = Runtime.getRuntime().availableProcessors();
private void initTaskThreadPool() {
executor = doThreadPoolExecutor(PROCESSORS, PROCESSORS * 4, QUEUE_SIZE, THREAD_NAME, 400L);
listeningExecutor = MoreExecutors.listeningDecorator(executor);
}
//**ListeningExecutorService** :由于普通的线程池,返回的Future,功能比较单一;Guava 定义了 ListenableFuture接口并继承了JDK concurrent包下的Future 接口,ListenableFuture 允许你注册回调方法(callbacks),在运算(多线程执行)完成的时候进行调用。
利用@PostConstruct注解完成项目启动时的线程初始化(只执行一次)
/**
* 初始化 开启线程
*/
@PostConstruct
private void init() {
//执行抓取任务线程池
initTaskThreadPool();
log.info("初始化通用线程池成功...");
}
@PostConstruct 注解解释:
@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序:
Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
2.定义任务处理器
任务处理器使用一个实现了Callable接口的call方法的内部类。(Runnable方法不能提供返回值)
class Crawler implements Callable<TaskResult> {
private CrawlRequestBase event;
public Crawler(CrawlRequestBase event) {
this.event = event;
}
@Override
public TaskResult call() throws Exception {
Stopwatch started = Stopwatch.createStarted();
try {
//任务处理过程
} catch (Exception e) {
//处理异常
log.error("Crawler call error:{}", e.getMessage());
} finally {
//处理资源日志监控
}
return TaskResult.fail("Crawler_Call_Exception");
}
}
3.向线程池提交任务
import com.google.common.util.concurrent.Futures;
Futures.addCallback(listeningExecutor.submit(new Crawler(requestparam)), new FutureCallback<TaskResult>() {
//成功时的回调方法
@Override
public void onSuccess(TaskResult result) {
}
//失败时的回调方法
@Override
public void onFailure(Throwable t) {
}
}, executor);
}
上面的例子用的是**Futures.addCallback(futureTask,callback,executorService)**方法;
这个方法,FutureCallback操作将会执行在单独的线程,这个线程由传入的ExecutorService参数提供,在这个线程池中进行排队。适合回调处理过程占用CPU高,处理时间较长的情景。
回调过程比较快的则可以考虑另一个回调函数
Futures.addCallback(futureTask,callback);这样回调函数就是在 ListenableFuture实例执行的线程将上执行FutureCallback操作,即任务将在调用者的线程上运行。
4.线程池中需要注意的运行时参数
/**
* 线程池信息
*
* @return
*/
public static String getThreadInfo() {
return "CPU数:" + PROCESSORS + ", 当前线程:" + Thread.currentThread().getName() + ", 线程池中线程数目:" + executor.getPoolSize() + ",队列中等待执行的任务数目:" +
executor.getQueue().size() + ",已执行完毕的任务数目:" + executor.getCompletedTaskCount();
}
4.线程优化的切入点
能进行优化的关键是了解线上线程池的运行状况;所以第一步就是要对线程池进行监控。
一个是数据的监控,一个是时间的监控。
有了记录的参数后,我们要做的是性能瓶颈的分析
瓶颈可能但不限于出现在一下几个地方:
核心线程池数量过小,处理时间过长,导致阻塞队列堆积过大:可以根据机器性能适当调节核心线程数的大小。同是控制任务提交的评论,能处理多少就提交多少。
机器重启导致任务丢失:控制任务的提交频率,尽量不要排队;或者条件允许,采用可靠的任务提交模式。将未处理完成的任务都记录下来,只有收到处理成功的消息,再删除掉原数据。