线程池那些事

最近看了一下公司同事写的代码,发现有些童鞋直接使用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状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
 
4.一个任务进入线程后的流程:
  • 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  • 线程数量达到了corePools,则将任务移入队列等待
  • 队列已满,新建线程(非核心线程)执行任务
  • 队列已满,总线程数又达到了maximumPoolSize,就会由上面那位星期天(RejectedExecutionHandler)抛出异常

5.常见四种线程池:

    ☆CachedThreadPool():可缓存线程池

  1. 线程数无限制
  2. 有空闲线程则复用空闲线程,若无空闲线程则新建线程
  3. 一定程序减少频繁创建/销毁线程,减少系统开销

        创建方法:ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

   

    ☆FixedThreadPool():固定数目线程池

  1. 可控制线程最大并发数(同时执行的线程数)
  2. 超出的线程会在队列中等待

        创建方法:ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);

  

    ☆SingleThreadExecutor():单线程化的线程池

  1. 有且仅有一个工作线程执行任务
  2. 所有任务按照指定顺序执行,即遵循队列的入队出队规则

        创建方法:ExecutorService singleThreadPool = Executors.newSingleThreadPool();

    

    ☆ScheduledThreadPool():定时线程池

  1. 支持定时及周期性任务执行。
  创建方法:ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);

总结:其实建议大家在开发的过程中尽量不要用线程池去处理任务,因为线程池使用不当,容易造成内存溢出。涉及异步处理的建议使用消息中间件去处理,例如RabbitMq、Kafka等。



 
 
 
posted @ 2019-04-12 15:36  小柴胡颗粒  阅读(168)  评论(0编辑  收藏  举报