Java中的线程池——ThreadPoolExecutor的使用

转自:https://blog.csdn.net/u010723709/article/details/50377543

开发过程中,合理地使用线程池可以带来3个好处:

降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。

提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

 

1 线程池的创建

ThreadPoolExecutor有以下四个构造方法

 

  • ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)
  • ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler)
  • ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory)
  • ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

这里面需要几个参数

 

1  corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

 

2  workQueue(任务队列) : 用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:

 

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,按FIFO原则进行排序
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,吞吐量高于ArrayBlockingQueue。静态工厂方法Excutors.newFixedThreadPool()使用了这个队列
  • SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,静态工厂方法Excutors.newCachedThreadPool()使用了这个队列
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

3  maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没用了。

4 threadFactory(线程工厂):可以通过线程工厂为每个创建出来的线程设置更有意义的名字,如开源框架guava

5 RejectedExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略还处理新提交的任务。它可以有如下四个选项:

  • AbortPolicy:直接抛出异常,默认情况下采用这种策略
  • CallerRunsPolicy:只用调用者所在线程来运行任务
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
  • DiscardPolicy:不处理,丢弃掉

    更多的时候,我们应该通过实现RejectedExecutionHandler 接口来自定义策略,比如记录日志或持久化存储等。

6 keepAliveTime(线程活动时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程利用率。

 

7 TimeUnit(线程活动时间的单位):可选的单位有天(Days)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

 

2 提交任务

 

可以使用execute和submit两个方法向线程池提交任务

 

(1)execute方法用于提交不需要返回值的任务,利用这种方式提交的任务无法得知是否正常执行

 
  1. threadPoolExecutor.execute(new Runnable() {

  2.  
  3. @Override

  4. public void run() {

  5. try {

  6. Thread.sleep(5000);

  7. } catch (InterruptedException e) {

  8. e.printStackTrace();

  9. }

  10. }

  11. });

(2) submit方法用于提交一个任务并带有返回值,这个方法将返回一个Future类型对象。可以通过这个返回对象判断任务是否执行成功,并且可以通过future.get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。

 
  1. Future future=threadPoolExecutor.submit(futureTask);

  2.  
  3. Object value=future.get();


 

 

3 关闭线程池

 

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无响应中断的任务可能永远无法停止。但是他们存在一定的区别,shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有正在执行的任务。

 

只要调用了这两个关闭方法的一个,isShutdown就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这是调用isTerminated方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定执行完,则可以调用shutdownNow方法。

 

4 合理配置线程池

要想合理地配置线程池,首先要分析任务特性

  • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  • 任务的优先级:高、中和低。
  • 任务的执行时间:长、中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应该配置尽可能少的线程,如配置N+1个线程,N位CPU的个数。而IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*N。混合型任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量

优先级不同的任务可以交给优先级队列PriorityBlcokingQueue来处理。

执行时间不同的任务可以交给不同规模的线程池来处理。

依赖数据库的任务,因此线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间越长,那么线程数应该设置的越大,这样能更好滴利用CPU。

 

5 线程池应用示例

1 示例1 验证shutdown和shutdownNow的区别

 

(1)首先构造一个线程池,用ArrayBlockingQueue作为其等待队列,队列初始化容量为10。该线程池核心容量为 10,最大容量为20,线程存活时间为1分钟。

 

 
  1. static BlockingQueue blockingQueue=new ArrayBlockingQueue<>(10);

  2.  
  3. static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);

 

 

(2)另外构造了一个实现Runable接口的类TaskWithoutResult,其逻辑很简单,睡眠1秒

 
  1. /**

  2. * 无返回值的任务

  3. * @author songxu

  4. *

  5. */

  6. class TaskWithoutResult implements Runnable

  7. {

  8. private int sleepTime=1000;//默认睡眠时间1s

  9. public TaskWithoutResult(int sleepTime)

  10. {

  11. this.sleepTime=sleepTime;

  12. }

  13. @Override

  14. public void run()

  15. {

  16. System.out.println("线程"+Thread.currentThread()+"开始运行");

  17. try {

  18. Thread.sleep(sleepTime);

  19. } catch (InterruptedException e) {//捕捉中断异常

  20.  
  21. System.out.println("线程"+Thread.currentThread()+"被中断");

  22. }

  23. System.out.println("线程"+Thread.currentThread()+"结束运行");

  24. }

  25.  
  26.  
  27.  
  28. }


(3)验证

 
  1. /**

  2. * 中断测试

  3. */

  4. public static void test1()

  5. {

  6. for(int i=0;i<10;i++)

  7. {

  8. Runnable runnable=new TaskWithoutResult(1000);

  9. threadPoolExecutor.submit(runnable);

  10. }

  11. //threadPoolExecutor.shutdown();//不会触发中断

  12. threadPoolExecutor.shutdownNow();//会触发中断

  13. }


分别测试shutdown和shutdownNow()方法,结果shutdown()方法的调用并不会引发中断,而shutdownNow()方法则会引发中断。这也正验证前面所说的,shutdown方法只是发出了停止信号,等所有线程执行完毕会关闭线程池;而shutdownNow则是立即停止所有任务。

 

 

2 示例2 验证线程池的扩容

在本例中想要验证线程池扩容到核心数量,然后再扩容到最大数量,最后再缩小到核心数量的过程。

 

(1)首先构造一个线程池,用ArrayBlockingQueue作为其等待队列,队列初始化容量为1。该线程池核心容量为 10,最大容量为20,线程存活时间为1分钟。

  

 
  1. static BlockingQueue blockingQueue=new ArrayBlockingQueue<>(1);

  2.  
  3. static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);

 

 (2)另外构造了一个实现Runable接口的类TaskBusyWithoutResult类,其模拟一个繁忙的任务

 

 
  1. class TaskBusyWithoutResult implements Runnable

  2. {

  3. public TaskBusyWithoutResult()

  4. {

  5. }

  6. @Override

  7. public void run()

  8. {

  9. System.out.println("线程"+Thread.currentThread()+"开始运行");

  10. int i=10000*10000;

  11. while(i>0)

  12. {

  13. i--;

  14. }

  15. System.out.println("线程"+Thread.currentThread()+"运行结束");

  16. }

  17.  
  18.  
  19.  
  20. }

 (3)测试验证,向线程池提交20个任务

 
  1. /**

  2. * 扩容测试

  3. */

  4. public static void test2()

  5. {

  6. for(int i=0;i<20;i++)

  7. {

  8. Runnable runnable=new TaskBusyWithoutResult();

  9. threadPoolExecutor.submit(runnable);

  10. }

  11. }

(4)验证结果

 

 在VisualVM中观察线程的变化,在任务提交的瞬间,线程池完成了预热到扩容到最大线程,之所以这么迅速是因为本例中的等待队列长度只有1,可以适当地增加队列长度,但不并不一定能看到扩大最大容量,其原因将在下一节中讲到。在线程池中任务都运行完毕后,可以看到线程池回收了多余的线程,但并没有完全回收,而是保持在核心线程数量。从这里也可以看出,合理地设置核心线程的数量可以减少线程的频繁创建和回收,而这才是线程池的真正作用。

 

你可能感兴趣的:(多线程)