线程池那些事
最近看了一下公司同事写的代码,发现有些童鞋直接使用Executors去创建线程池,比如:ExecutorService executorService = Executors.newCachedThreadPool();
阿里开发手册中明文规定禁止这样操作,为啥不能这样呢?人家也说了很清楚。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
线程池的作用:使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。频繁创建线程和销毁线程需要时间,线程池的出现,大大提高了系统的效率。
1.线程池中最核心的一个类java.uitl.concurrent.ThreadPoolExecutor,要了解这个类,就要看他的源码了。
public class ThreadPoolExecutor extends AbstractExecutorService { //构造方法1 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } //构造方法2 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } //构造方法3 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } //构造方法4 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } }
ThreadPoolExecutor有四个构造器,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。这四个构造方法就是江湖上所说的重载(Overloading),方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。下面我们就来说说构造方法中的这些参数都是啥意思。
2.构造方法中的七大金刚(参数)
- corePoolSize:核心池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒
- workQueue:一个阻塞队列,用来存储等待执行的任务。很关键的一个参数,常见的队列有如下几种:
ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue;
ArrayBlockingQueue是有界队列;而LinkedBlockingQueue则是无界队列,无界队列的意思是能一直往里面塞值,但最终会出现oom,所以要小心;
public LinkedBlockingDeque() { this(Integer.MAX_VALUE); }
SynchronousQueue队列很有意思,它不会去存储存入的任务,而是每进来一个,它就会去开启一个线程去处理任务,也容易导致oom,也要小心。
- threadFactory:线程工厂,主要用来创建线程;
- handler:表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
3.ThreadPoolExecutor类中几个重要的方法:
execute():通过这个方法可以向线程池提交一个任务,交由线程池去执行。
submit():这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果
shutdown():当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
shutdownNow():线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
- 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量达到了corePools,则将任务移入队列等待
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maximumPoolSize,就会由上面那位星期天(RejectedExecutionHandler)抛出异常
5.常见四种线程池:
☆CachedThreadPool():可缓存线程池
- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程
- 一定程序减少频繁创建/销毁线程,减少系统开销
创建方法:ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
☆FixedThreadPool():固定数目线程池
- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
创建方法:ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
☆SingleThreadExecutor():单线程化的线程池
- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
创建方法:ExecutorService singleThreadPool = Executors.newSingleThreadPool();
☆ScheduledThreadPool():定时线程池
- 支持定时及周期性任务执行。
创建方法:ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
总结:其实建议大家在开发的过程中尽量不要用线程池去处理任务,因为线程池使用不当,容易造成内存溢出。涉及异步处理的建议使用消息中间件去处理,例如RabbitMq、Kafka等。