java ThreadPoolExecutor 详解

构造方法

public ThreadPoolExecutor(
	int corePoolSize,
	int maximumPoolSize,
	long keepAliveTime,
	TimeUnit unit,
	BlockingQueue workQueue,
	ThreadFactory threadFactory,
	RejectedExecutionHandler handler
)

任务提交

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

参数解释

  • corePoolSize
    核心线程数,当提交一个任务的时候,线程池创建一个新的线程执行任务,直到当前线程等于corePoolSize。如果当前线程数到了corePoolSize, 继续提交的任务则被保存到阻塞队列中,等待着被执行。线程池启动的时候不会初始化 corePoolSize 个线程,而是来任务的时候才会创建线程,如果想要在创建线程池的时候启动,需要执行线程池的prestartAllCoreSize。

  • maximumPoolSize

    线程池中允许的最大线程数。如果当前的阻塞队列满了,并且继续提交任务,则会创建新的线程去执行任务,前提是当前线程数小于 maximumPoolSize。

  • keepAliveTime

    线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用。

  • TimeUnit

    keepAliveTime的时间单位

  • workQueue

    用于保存等待执行的任务的阻塞队列,在开发中,我们尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响:

  1. 当线程池的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize 。

  2. 由于1 ,使用无界队列 maximumPoolSize 将是 一个无效参数。

  3. 由于1和2, 使用无界队列 时 keepAliveTime 将是一个无效参数。

  4. 最重要的是,使用无界queue 可能会耗尽系统资源,有界队列则有助于防止资源耗尽,即使使用有界队列,也要尽量控制队列大小在一个合适的范围内。

    JDK 提供了以下阻塞队列:
    1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
    2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene
    3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
    4、priorityBlockingQuene:具有优先级的无界阻塞队列;

  • threadFactory
    创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。

    Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。
    可以通过线程工厂给每个创建出来的线程设置更有意义的名字。线程池命名时通过给这个factory 增加组前缀来实现的。在虚拟机栈分析时,就可以知道线程任务由哪个线程工厂产生的。

    使用 Google Guava 设置线程名字:
    new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

    自定义实现ThreadFactory

	public class NamedThreadFactory implements ThreadFactory {
    private static final AtomicInteger threadNumber=new AtomicInteger(1);
    private final AtomicInteger mThreadNum = new AtomicInteger(1);
    private final String prefix;
    private final boolean daemoThread;
    private final ThreadGroup threadGroup;
   public NamedThreadFactory() {
        this("rpcserver-threadpool-" + threadNumber.getAndIncrement(), false);
    }
    public NamedThreadFactory(String prefix) {
        this(prefix, false);
    }

    public NamedThreadFactory(String prefix, boolean daemo) {
        this.prefix = StringUtils.isNotEmpty(prefix) ? prefix + "-thread-" : "";
        daemoThread = daemo;
        SecurityManager s = System.getSecurityManager();
        threadGroup = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup();
    }
    @Override
    public Thread newThread(Runnable runnable) {
        String name = prefix + mThreadNum.getAndIncrement();
        Thread ret = new Thread(threadGroup, runnable, name, 0);
        ret.setDaemon(daemoThread);
        return ret;
    }
  • RejectedExecutionHandler

    线程池的饱
    和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
    (1)AbortPolicy:直接抛出异常,默认策略;
    (2)CallerRunsPolicy:用调用者所在的线程来执行任务;
    (3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    (4)DiscardPolicy:直接丢弃任务;不推荐使用。

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

友好的拒绝策略:

  1. 保存到数据库进行削峰填谷。在 空闲时再提取出来执行
  2. 转向某个提示页
  3. 打印日志

使用流程

java ThreadPoolExecutor 详解_第1张图片

java ThreadPoolExecutor 详解_第2张图片

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

** 从源码上分析**
从上面流程分析让我们很直观了解线程池的工作原理,让我们再通过源码来看看是如何实现的,线程池执行任务的方法如下:

public void execute(Runnable command) {
        int c = ctl.get();
        // 如果线程数小于基本线程数,则创建线程并执行当前任务
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 如果线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许线程数量
        // 则创建一个线程执行任务
        else if (!addWorker(command, false))
            reject(command);
    }

关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

你可能感兴趣的:(Java,并发编程,线程池)