了解Java的ThreadPoolExecutor。。

381 阅读4分钟

边了解边写该文章

1.了解Java的ThreadPoolExecutor

1.ThreadPoolExecutor的七大参数

1.int corePoolSize

* @param corePoolSize the number of threads to keep in the pool, even
*        if they are idle, unless {@code allowCoreThreadTimeOut} is set

核心线程数,如果该数值为3,即便线程池中有3个闲置,没有任务的核心线程,这3个核心线程也会存在线程池中,不会被销毁。allowCoreThreadTimeOut(默认值为false)是一个boolean值,如果该布尔值为true,这3个核心线程也会因为闲置太久而被销毁。

2.int maximumPoolSize

* @param maximumPoolSize the maximum number of threads to allow in the
*        pool

最大线程数,当核心线程都不闲置,且workqueue已经满了装不下,则会创建一些新的线程来处理任务,如果线程池中总线程数达到最大线程数就不会创建

3.long keepAliveTime

* @param keepAliveTime when the number of threads is greater than
*        the core, this is the maximum time that excess idle threads
*        will wait for new tasks before terminating.

和参数2和参数4结合一起理解

image-20211002101326863.png

4.TimeUnit unit

* @param unit the time unit for the {@code keepAliveTime} argument

第3个参数的时间单位

5.BlockingQueue<Runnable> workQueue

* @param workQueue the queue to use for holding tasks before they are
*        executed.  This queue will hold only the {@code Runnable}
*        tasks submitted by the {@code execute} method.

阻塞队列,这只是一个接口,创建线程池时需要传入他的实现类,作用就是在线程池在线程数大于核心线程数时,会把任务放到这个队列中。

如果队列为空,调用 take() 会被阻塞。如果队列满了,调用 put() 会被阻塞。阻塞队列还有很多方法

部分实现类如下

  1. LinkedBlockingQueue<E> 基于链表的阻塞队列
  2. SynchronousQueue<E> 没有容量的队列,线程调用 put() 时如果没有其他线程调用 take() 则阻塞,线程调用 take() 时如果没有其他线程调用 put() 则阻塞
  3. DelayedWorkQueue<E> 基于堆的阻塞队列,根据时间,需要先执行的任务会在堆顶

6.ThreadFactory threadFactory

* @param threadFactory the factory to use when the executor
*        creates a new thread

线程工厂,也是一个接口,需要传入接口的实现类。用处(存疑),给线程取名字吗?线程优先级?

7.RejectedExecutionHandler handler

* @param handler the handler to use when execution is blocked
*        because the thread bounds and queue capacities are reached

拒绝策略,也是一个接口,需要传入接口的实现类。在队列满了之后,且线程池里的线程达到最大线程数,执行的拒绝策略。

Java自带四种拒绝策略:

  1. AbortPolicy 抛出异常
  2. CallerRunsPolicy 让线程池的调用者执行这个任务
  3. DiscardOldestPolicy 丢弃队列中队头的任务,然后再次提交任务试试
  4. DiscardPolicy 默默地丢弃这个任务

也可以自己实现这个接口,自定义拒绝策略

2.关于设置线程数

线程有时候会需要I/O,比如说读取磁盘的数据,或者是RPC,所以希望线程在等待数据的时候把cpu让给其他线程利用。

比如cpu1核情况下,一个任务50%时间用cpu计算,50%时间需要等待,这时可以设置2个线程。

在一些书中或博客上有一些估算设置线程池线程数的公式,但到底设置多少线程可以发挥机器最佳性能,还是需要实际测试。

3.submit和execute

  1. public void execute(Runnable command) execute方法只能提交Runnable对象。如果执行任务有异常,会打印错误信息

  2. public Future<?> submit(Runnable task) 如果向submit传入的是Runnable,线程执行成功future.get()返回null,执行异常future.get()抛异常。(注意:如果submit()之后,没有调用future.get(),任务执行出异常,控制台不会打印任何错误信息的)

    Future<?> future = pool.submit(() -> {
    });
    System.out.println(future.get());//这里会打印null
    //=================================================
    Future<?> future = pool.submit(() -> {
        int i = 0 / 0;
    });
    future.get();//这个get方法会抛出异常
    
  3. public <T> Future<T> submit(Runnable task, T result) 和上面那个差不多,不过执行成功返回的是传入的result

  4. public <T> Future<T> submit(Callable<T> task) 执行成功 future.get() 就返回task的返回值,失败 future.get() 就抛异常

    Future<?> future = pool.submit(() -> {
    	return "lzp";
    });
    System.out.println(future.get());//打印lzp
    //==============================
    Future<?> future = pool.submit(() -> {
        int i=0/0;
        return "lzp";
    });
    try {
        future.get();
    }catch (Exception e){
        System.out.println("抓住了");
    }
    //打印结果:抓住了
    

4.Java提供的一些线程池

Executors.newFixedThreadPool(int nThreads)

特点:

  1. 核心线程数等于最大线程数
  2. 阻塞队列是 LinkedBlockingQueue<>(Integer.MAX_VALUE),队列很大,有内存溢出的风险。(存疑:为什么不设置个兜底的值防止内存溢出呢?)

Executors.newSingleThreadExecutor()

特点:

  1. 核心线程数等于最大线程数,且都是1
  2. 阻塞队列是 LinkedBlockingQueue<>(Integer.MAX_VALUE),队列很大,有内存溢出的风险
  3. 对比起自己创建一个线程有什么优点呢?如果线程池执行某个任务失败,会新创建一个线程,保持线程池的正常运行

Executors.newCachedThreadPool()

特点:

  1. 核心线程数为0,最大线程数为Integer.MAX_VALUE
  2. 线程池中的线程闲置60s就被销毁
  3. 阻塞队列是非公平模式的SynchronousQueue<>(),(如果是公平模式就是基于一个先进先出队列来存放多余的生产者或消费者,如果是非公平模式就是基于先进后出的栈来存放多余的生产者和消费者,暂时不知道为什么要用非公平模式)

Executors.newScheduledThreadPool(int corePoolSize)

前面提到三个线程池基本都是ThreadPoolExecutor类的对象,只是参数不太一样。这个是继承了ThreadPoolExecutor,有一些额外的功能。

特点:

  1. 最大线程数为Integer.MAX_VALUE,救急线程闲置直接销毁

  2. 阻塞队列是DelayedWorkQueue()

  3. 延时执行任务,注意使用细节,该延时并不精确,看代码

    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    pool.submit(()->{
        try {
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    //这里并不会在1s后打印,因为线程池只有一个线程,而且他在一个执行一个超长的任务,会影响后续的任务
    pool.schedule(()->{
        System.out.println("lzp");
    },1L, TimeUnit.SECONDS);
    
  4. 还有周期任务的api可以用,也都有可能因为线程池线程过于忙碌,而被延误