JAVA线程实用技术(非源码解析)—线程池Executor

一、为什么需要Executor

new Thread()存在缺点

  • 每次new Thread()耗费性能
  • 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。
  • 不利于扩展,比如如定时执行、定期执行、线程中断

采用线程池的优点

  • 重用存在的线程,减少对象创建、消亡的开销,性能佳
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
  • 提供定时执行、定期执行、单线程、并发数控制等功能

Executor有啥用

帮我们管理线程

二、Executor基本知识

2.1 Ececutor体系

图片来源于网络
Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),

ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,以及可跟踪一个或多个异步任务执行状况返回Future的方法

AbstractExecutorService:ExecutorService执行方法的默认实现

ScheduledExecutorService:一个可定时调度任务的接口

ScheduledThreadPoolExecutor:ScheduledExecutorService的实现,一个可定时调度任务的线程池

ThreadPoolExecutor:线程池,可以通过调用Executors以下静态工厂方法来创建线程池并返回一个ExecutorService对象

2.2 线程池核心实现类-ThreadPoolExecutor

Executor框架的最核心的类是ThreadPoolExecutor,它是线程池的实现类。

2.2.1 构造方法

构造参数说明:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) //后两个参数为可选参数
  • corePoolSize:核心线程数,如果运行的线程少于corePoolSize,则创建新线程来执行新任务,即使线程池中的其他线程是空闲的

  • maximumPoolSize:最大线程数,可允许创建的线程数,corePoolSize和maximumPoolSize设置的边界自动调整池大小:
    corePoolSize <运行的线程数< maximumPoolSize:仅当队列满时才创建新线程
    corePoolSize=运行的线程数= maximumPoolSize:创建固定大小的线程池

  • keepAliveTime:如果线程数多于corePoolSize,则这些多余的线程的空闲时间超过keepAliveTime时将被终止

  • unit:keepAliveTime参数的时间单位:

TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

1、直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们特殊的一个队列,只有存在等待取出的线程时才能加入队列,可以说容量为0,是无界队列直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。

2、无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求。

3、有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

  • threadFactory:使用ThreadFactory创建新线程,默认使用defaultThreadFactory创建线程

  • handle:定义处理被拒绝任务的策略,默认使用ThreadPoolExecutor.AbortPolicy,任务被拒绝时将抛RejectExecutorException,有以下四种取值:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:该策略既不会抛弃任务也不会抛出异常,而是在调用execute()的线程中运行任务。
比如我们在主线程中调用了execute(task)方法,但是这时workQueue已经满了,并且也不会创建的新的线程了。这时候将会在
主线程中直接运行execute中的task。

2.1.2 线程池状态

static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;
  • 当创建线程池后,初始时,线程池处于RUNNING状态;

  • 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

  • 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

  • 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

2.1.3 在ThreadPoolExecutor类中有几个非常重要的方法:

execute()
submit()
shutdown()
shutdownNow()
  • execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

  • submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。

  • shutdown()和shutdownNow()是用来关闭线程池的。

shutdown()
将线程池状态置为SHUTDOWN,并不会立即停止:

  • 停止接收外部submit的任务
  • 内部正在跑的任务和队列里等待的任务,会执行完
  • 等到第二步完成后,才真正停止

shutdownNow()
将线程池状态置为STOP。企图立即停止,事实上不一定:

跟shutdown()一样,先停止接收外部提交的任务

  • 忽略队列里等待的任务
  • 尝试将正在跑的任务interrupt中断
  • 返回未执行的任务列表

2.3 任务处理策略

线程池的任务处理策略:
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;

如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;

如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
这里写图片描述

2.4 示例(使用无界队列及AbortPolicy拒绝策略)

package test;

import java.util.concurrent.ThreadPoolExecutor;

public class MyThread implements Runnable {

    private Integer i;

    public Integer getI() {
        return i;
    }

    public void setI(Integer i) {
        this.i = i;
    }

    @Override
    public void run() {
         try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

测试类:

package test;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test17 {

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 10L,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3),
                new ThreadPoolExecutor.AbortPolicy());
        MyThread t = new MyThread();
        for (int i = 1; i < 10; i++) {
            try {
                t.setI(i);
                System.out.println("Thread-" + i + "线程正在运行!");
                executor.execute(t);
                System.out.println("Thread-" + i + "执行结果:成功!");
            } catch (RejectedExecutionException e) {
                System.out.println("Thread-" + i + "执行结果:失败,超过最大线程数,自动拒绝!");
            }
            System.out.println("Thread-"+i+"线程运行结束!");
        }
        executor.shutdown();
    }
}

结果:

Thread-1线程正在运行!
Thread-1执行结果:成功!
Thread-1线程运行结束!
Thread-2线程正在运行!
Thread-2执行结果:成功!
Thread-2线程运行结束!
Thread-3线程正在运行!
Thread-3执行结果:成功!
Thread-3线程运行结束!
Thread-4线程正在运行!
Thread-4执行结果:成功!
Thread-4线程运行结束!
Thread-5线程正在运行!
Thread-5执行结果:成功!
Thread-5线程运行结束!
Thread-6线程正在运行!
Thread-6执行结果:失败,超过最大线程数,自动拒绝!
Thread-6线程运行结束!
Thread-7线程正在运行!
Thread-7执行结果:失败,超过最大线程数,自动拒绝!
Thread-7线程运行结束!
Thread-8线程正在运行!
Thread-8执行结果:失败,超过最大线程数,自动拒绝!
Thread-8线程运行结束!
Thread-9线程正在运行!
Thread-9执行结果:失败,超过最大线程数,自动拒绝!
Thread-9线程运行结束!

2.5 Executors

如果不是非常了解JAVA线程池的知识,那么通过new ThreadPoolExecutor方式生成线程池的时候,很有可能会生成一个不适合自己的线程池。这个时候,Executors类了解一下?Executors负责生成各种类型的ExecutorService线程池实例,这些实例可以满足大部分的场景。

  • newFixedThreadPool(numberOfThreads:int):(固定线程池)创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。实际代码为:
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}
  • newCachedThreadPool():(可缓存线程池)创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
    那么就会回收部分空闲(60秒处于等待任务到来)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池的最大值是Integer的最大值(2^31-1)。源码:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
}
  • new SingleThreadExecutor();(单线程执行器)创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。源码:
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  • new ScheduledThreadPool():创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

  • new SingleThreadScheduledExcutor();线程池中只有一个线程,它按规定时间来执行任务

猜你喜欢

转载自blog.csdn.net/qq_21955179/article/details/80110373