ThreadPoolExecutor探究

引言

阿里的 Java开发手册,上面有线程池的一个建议:
 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式, 这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。那么到底线程池创建为什么需要使用 ThreadPoolExecutor方式 Executors创建又是怎么回事呢。

1.不使用线程池-Thread

1.1 线程的使用

 new Thread(
 new Runnable() {  public void run() {  System.out.println("start");  }  }  ).start();

如上就是Thread的使用方式了在Runnable中编写线程的运行代码,调用start即可完成线程开发的代码.

注意start方法只是说明线程已经准备启动,实际的启动需要CPU分配运行时间.

Runnable是一个接口,实现接口如果不执行start永远不可能线程被执行.

使用 new Thread 方式创线程的缺点:

a. 每次new Thread新建对象性能差。

b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。

c. 缺乏更多功能,如定时执行、定期执行、线程中断。

1.2 为什么要用线程池


1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。 

3.提供定时执行、定期执行、单线程、并发数控制等功能。

2.常见四大线程池

2.1 cachedThreadPool(可缓存的线程池)

 /**
 * 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,  * 当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,  * 线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。  */  ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  for (int i = 1; i <= 10; i++) {  final int index = i;  Thread.sleep(index * 1000);  cachedThreadPool.execute(() -> {  String threadName = Thread.currentThread().getName();  System.out.println("执行:" + index + ",线程名称:" + threadName);  });  }

image.png

2.2 fixedThreadPool(固定大小的线程池)

/**
 *创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。  * 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。  */  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  for (int i = 1; i <= 10; i++) {  final int index = i;  fixedThreadPool.execute(() -> {  try {  String threadName = Thread.currentThread().getName();  System.out.println("执行:" + index + ",线程名称:" + threadName);  Thread.sleep(2000);  } catch (InterruptedException e) {  e.printStackTrace();  }  });  }

image.png

2.3 scheduledThreadPool(定时任务线程池)

 /**
 * 创建一个定长线程池,支持定时及周期性任务执行。延迟执行  */  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  scheduledThreadPool.schedule(() -> System.out.println("表示延迟3秒执行。"), 3, TimeUnit.SECONDS);  scheduledThreadPool.scheduleAtFixedRate(() -> System.out.println("表示延迟1秒后每3秒执行一次。"), 1, 3, TimeUnit.SECONDS);

image.png

2.4 singleThreadExecutor(单线程化的线程池

/**
 * 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。  */  ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  for (int i = 1; i <= 5; i++) {  final int index = i;  singleThreadExecutor.execute(() -> {  try {  String threadName = Thread.currentThread().getName();  Thread.sleep(2000);  System.out.println("执行:" + index + ",线程结束名称:" + threadName);  } catch (InterruptedException e) {  e.printStackTrace();  }  });  }

image.png

3.手工创建线程池-ThreadPoolExecutor

3.1 ThreadPoolExecutor参数

image.png

corePoolSize - 线程池核心池的大小。
maximumPoolSize - 线程池的最大线程数。 keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。 unit - keepAliveTime 的时间单位。 workQueue - 用来储存等待执行任务的队列。 threadFactory - 线程工厂。 handler - 拒绝策略。
corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,
默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程 的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池 中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就 会把到达的任务放到缓存队列当中;  maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线 程;  keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于 corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的 线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不 超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于 corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;  workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大 影响,一般来说,这里的阻塞队列有以下几种选择:  java.lang.IllegalStateException: Queue full 方法 抛出异常 返回特殊值 一直阻塞 超时退出 插入方法 add(e) offer(e) put(e) offer(e,time,unit) 移除方法 remove() poll() take() poll(time,unit)   ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。 LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。 PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。 DelayQueue: 一个使用优先级队列实现的无界阻塞队列。 SynchronousQueue: 一个不存储元素的阻塞队列。 LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。 LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。 

3.2 线程池参数拒绝策略

RejectedExecutionHandler提供了四种方式来处理任务拒绝策略
1、直接丢弃(DiscardPolicy) 2、丢弃队列中最老的任务(DiscardOldestPolicy)。 3、抛异常(AbortPolicy) 4、将任务分给调用线程来执行(CallerRunsPolicy)。  这四种策略是独立无关的,是对任务拒绝处理的四中表现形式。最简单的方式就是直接丢弃任务。 但是却有两种方式,到底是该丢弃哪一个任务,比如可以丢弃当前将要加入队列的任务本身(DiscardPolicy) 或者丢弃任务队列中最旧任务(DiscardOldestPolicy)。丢弃最旧任务也不是简单的丢弃最旧的任务,而是有一些 额外的处理。除了丢弃任务还可以直接抛出一个异常(RejectedExecutionException),这是比较简单的方式。 抛出异常的方式(AbortPolicy)尽管实现方式比较简单,但是由于抛出一个RuntimeException,因此会中断调用 者的处理过程。除了抛出异常以外还可以不进入线程池执行,在这种方式(CallerRunsPolicy)中任务将有调用者 线程去执行。

3.3 线程池参数之间的对比

image.png

可以看到上述线程池之间的参数对比,其中threadFactory 参数默认都可以修改其余可以修改的参数只有FixedThreadPool的核心连接数和最大连接数,ScheduledThreadPool的核心连接数。

3.4 默认四大线程池的弊端

1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。因为使用的是 LinkedBlockingQueue(Integer.MAX_VALUE)作为缓冲队列,可以缓存int的最大值的线程。  2)newCachedThreadPool 和 newScheduledThreadPool: 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

3.5 ThreadPoolExecutor使用示例

3.6 线程池的线程大小设置问题

本节来讨论一个比较重要的话题:如何合理配置线程池大小,仅供参考。
一般需要根据任务的类型来配置线程池大小:  a.如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1 b.如果是IO密集型任务,参考值可以设置为2*NCPU  当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值, 再观察任务运行情况和系统负载、资源利用率来进行适当调整。

猜你喜欢

转载自www.cnblogs.com/reload-sun/p/12216829.html