线程池用法--Guava

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.线程优化的切入点

能进行优化的关键是了解线上线程池的运行状况;所以第一步就是要对线程池进行监控。
一个是数据的监控,一个是时间的监控。

有了记录的参数后,我们要做的是性能瓶颈的分析
瓶颈可能但不限于出现在一下几个地方:
核心线程池数量过小,处理时间过长,导致阻塞队列堆积过大:可以根据机器性能适当调节核心线程数的大小。同是控制任务提交的评论,能处理多少就提交多少。
机器重启导致任务丢失:控制任务的提交频率,尽量不要排队;或者条件允许,采用可靠的任务提交模式。将未处理完成的任务都记录下来,只有收到处理成功的消息,再删除掉原数据。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值