Java中的线程池理解

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
最近项目用频繁用到它,有必要总结下。
首先看它的好处:

  1. 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。但是,要做好合理利用线程池,必须对其实现原理了如指掌。

以装修公司类比线程池
以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的“装修公司”,代劳各种脏活累活。

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 正式工数量
  int maximumPoolSize, // 工人数量上限,包括正式工和临时工
  long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
  BlockingQueue<Runnable> workQueue, // 排期队列
  ThreadFactory threadFactory, // 招人渠道
  RejectedExecutionHandler handler) // 拒单方式
1 基础知识:

Executors创建线程池
Java中创建线程池很简单, 只需要调用 Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads), 方便的同时也隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Execute创建线程池便捷方法列表:

  • newFixedThreadPool(int nThreads) 创建固定大小的线程池
  • newSingleThreadExecutor() 创建只有一个线程的线程池
  • newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行。

小程序使用这些快捷方法都没什么问题,对于服务器需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor
ThreadPoolExecutor构造方法
Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
  int maximumPoolSize, // 线程数的上限
  long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                     // 超过这个时间,多余的线程会被回收。
  BlockingQueue<Runnable> workQueue, // 任务的排队队列
  ThreadFactory threadFactory, // 新线程的产生方式
  RejectedExecutionHandler handler) // 拒绝策略

这些参数中,比较容易引起问题的有 corePoolSize,maxmumPoolSize, workQueue以及handler:

  1. CorePoolSize 和maxumumPoolSize设置不当会影响效率,甚至耗尽线程。
  2. workQueue 设置不当容易导致OOM
  3. handler 设置不当会导致提交任务时抛出异常。

线程池的工作顺序:

If fewer than corePoolSize threads are running, the Exector always prefers adding a new thread rather than queuing. If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread. If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.

2.线程池的使用:
2.1 线程池的创建

new ThreadPoolExecutor(corePoolSize,maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue,handler);

2.2 向线程池提交任务

threadPool.execute(new Runnable() {
@override
public void run(){
    //TODO auto generated method stub
}
});

execute() 方法用于提供不需要返回值的任务。
submit() 方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过futureget()方法来获取返回值,get()方法会阻塞当前线程知道任务完成。而使用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask);
try{
    Object s = future.get();
}catch(InterruptedException e){
   //处理中断异常
}catch(ExecutionException e){
   //处理无法执行任务异常
}finally{
   //关闭线程池
     executor.shutdown():
}

2.3 关闭线程池
可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。

2.4 合理地配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。

  1. 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。

  2. 任务的优先级:高、中和低。

  3. 任务的执行时间:长、中和短。

  4. 性质的依赖性:是否依赖其他系统资源、如数据库连接。

    性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置N+1个线程的线程池。由于IO密集型任务线程并不是一致在执行任务,则应配置尽可能多的线程,如2*N。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
    优先级不同的任务可以使用优先级队列ProprityBlockingQueue来处理。它可以让优先级高的任务先执行。

  1. 避免使用无界队列。

不要使用Executors.newXXXThreadPool() 快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM, 我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:

ExecutorService executorService = new ThreadPoolExecutor(2,2,
  0,TimeUnit.SECONDS,
  new ArrayBlockingQueue<>(512),//使用有界队列,避免OOM
  new ThreadPoolExecutor.DiscardPolicy()):// 指定拒绝策略
  1. 明确拒绝任务时的行为
    任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:
 public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

线程池给我们提供了几种常见的拒绝策略:
Java中的线程池理解_第1张图片
Java中的线程池理解_第2张图片
线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
3. 获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionExceptionsubmit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:

ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者
        }
    });
    
try {
  Object result = future.get();
} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

2.5 线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控。快速定位问题,可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性。

  • taskCount: 线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount.
  • largestPoolSize:线程池理曾经创建国的最大线程数量。
  • getPoolSize:线程池的数量
  • getActiveCount:获取活动的线程数。
    通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute,afterExecutetermineated方法,也可以在任务执行前,执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。
 protected void beforeExecute(Thread t, Runnable r){}

总结:
Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。

参考:
1) https://www.cnblogs.com/CarpenterLee/p/9558026.html
2) 第9章 Java中的线程池

你可能感兴趣的:(JAVA)