线程池的使用场景以及java中ThreadPoolExecutor类的讲解

目录

为什么要使用线程池?

Java中的线程池

 workQueue参数分析

handler参数分析

execute方法与submit方法

合理的选择线程池大小


为什么要使用线程池?

对于java初学者来说,首先接触到的创建线程的方法就是new Thread,或者实现Runnable接口,重写run方法来实现多线程。虽然简单,但是一句话:谁来帮你管理线程???

若不使用线程池:

1、线程的创建销毁都要自己来完成

2、没有统一的管理,若每次请求都开启多个线程,无限制的请求袭来,可能造成资源耗尽

3、不够灵活

线程池的出现能让你双手游离于多线程之外专注于其他代码,帮你管理线程。所以如果是一个大型系统,建议不论何种场景,都直接使用线程池。

举个例子:你的面前有三台电脑,你可能同时用三台,也可能同时用两台或者一台。但是分配给你的电脑每次都是随机的。假设三台电脑编号123,第一次:【你要用1台分配到1号,然后开机->使用->关机】,第二次:【你要用1台分配到2号,然后开机->使用->关机】,以此类推。假设你的使用时间是10秒,而开机关机是20秒,然后这个过程要重复100次,每次都随机分配。那么无疑开机和关机的过程浪费的大量的时间和资源。

每次使用一个线程,都要经历三个步骤:创建线程->使用线程->销毁线程,类似上述使用场景,频繁的使用线程,频繁的开启和关闭,频繁的浪费时间和资源,额...........如果这时候你面前的三台电脑虽然每次都随机分配,但是却从不关机,坐那就能用,就好了,yes!线程池就是完成了这样的操作!包括:数据库连接池也是同样的道理。线程池中每次创建的线程,如果执行完毕,不会立即进行销毁,而是处于等待状态,下一个任务来了以后无需开启直接使用,方便快捷!!!【线程池可以使已经开启的线程长期处于激活状态,节省创建和销毁线程的时间,实现线程复用!

Java中的线程池

四大方法、7大参数、四种拒绝策略

java中,Executor是java.util.concurrent包下的一个线程池的鼻祖接口,只有一个抽象方法execute;ExecutorService仍然是一个接口,extends Executor,新增了一些接口例如submit;AbstractExecutorService是一个抽象类,implements ExecutorService;ThreadPoolExecutor是我们最常使用的线程池类,extends AbstractExecutorService;

Executors类提供了四种创建线程池的方法,源码如下,如果还不能满足你,那你只能自定义线程池了。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

package java.util.concurrent.ScheduledThreadPoolExecutor 
extends ThreadPoolExecutor implements ScheduledExecutorService----->>>>

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

newFixedThreadPool:创建一个固定大小的线程池,提交一个任务就在线程池中创建一个线程,直到线程数量达到线程池最大限制。线程池一旦到最大就不会再改变,除非有已经开启的线程出现异常,再提交任务时会继续新建线程。这个创建线程池的方法是在实际项目中比较常用的,由于corePoolSize和maximumPoolSize两个参数是相同大小,所以到了线程池最大容量后,如果有任务完成让出占用线程,那么此线程就会一直处于等待状态,而不会消亡,直到下一个任务再次占用该线程。此方法的弊端是:使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无线增大,使服务器资源迅速耗尽。

newCachedThreadPool:创建一个可根据实际情况调整大小的线程池,线程数量不确定,只要有空闲线程空闲时间超过keepAliveTime,就会干掉,再来新任务,先使用空闲线程,若不够,再新建线程。线程池没有最大线程数量限制,所以当大量线程蜂拥而至,会造成资源耗尽。

newSingleThreadExecutor:创建一个容量为1的线程池,被提交任务按优先级依次执行。

newScheduledThreadPool:创建一个定长线程池,长度为输入参数自定义,支持定时周期任务执行,可根据时间需要在指定时间对线程进行调度。

可以看到 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor均使用的ThreadPoolExecutor构造器,下面分析该构造方法的入参:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

 this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);走的是该类中另一个构造方法:如下:

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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

7大参数: 

corePoolSize:线程池中的核心线程数量,超过该数量的空闲线程空闲时间超过keepAliveTime则会被销毁,在该数量内的空闲线程则不会被销毁,在下一个任务来临之前一直处于等待状态。超过核心线程数的任务会进入队列等待
maximumPoolSize:线程池支持的最大线程数。当队列满了,会创建临时线程帮我们处理任务
keepAliveTime:超过corePoolSize的空闲线程的存活时间。临时线程执行完毕当前任务,会主动去队列中获取任务,如果经过keepAliveTime没有获取到,则销毁临时线程;核心线程默认不超时
unit:keeyAliveTime的单位。
workQueue:任务队列,当提交的任务超过线程池支持的最大线程数,则进入该队列排队等待。该队列的选择会对线程池的性能有重大影响。
threadFactory:线程工厂,用于创建线程,一般使用默认,也可自定义。
handler:当提交任务数量超过线程池容量,并且超过排队队列容量后的拒绝策略。

即:若corePoolSize=2,workQueue=5,maximumPoolSize=3 

当来了2个线程:正好打开线程池的2个核心线程

当来了第3个线程,由于已经超过核心线程数,则先进入队列等待,此时队列容纳个数为1

当来了第6个线程,超过核心线程继续放入队列,但此时队列已满,于是迫不得已再次开辟线程池中新的线程

当来了第8个线程,线程池中所有线程全部打开,此时线程池已经达到最大线程数,且队列已满

当来了第9个线程,由于已经到了最大容载量,于是直接开始丢弃策略。

 workQueue参数分析

 workQueue是一个BlockingQueue接口的实现类,存放Runnable对象

阻塞队列:在任意时刻,不管多高的并发,永远只有一个线程能够进行入队或出队操作;

当队列已满,只能出队,所有入队操作等待,即阻塞;反之队列为空,只能入队。

一般来说,该队列的选择常用如下四种:

(1)SynchronousQueue【直接提交的队列】容量为0,每次插入都必须等待一个任务的删除操作,每次删除也要等待一个任务的插入操作。使用该队列,由于容量为0,其不会真实的保存任务,而是每次都将新任务提交给线程池,如果线程池满了,则直接执行拒绝策略,简单粗暴。

(2)ArrayBlockingQueue【有界的任务队列,按照先进先出的顺序出队】,该队列的构造方法为public ArrayBlockingQueue(int capacity),初始化时必须设置其容量。当线程池中的实际线程数小于corePoolSize,则优先创建新的线程;如果大于corePoolSize,并且此时恰好没有空闲线程,则优先进入有界队列,队列满了以后,再从队列中出队任务创建线程,直到线程池中的线程数量达到maximumPoolSize。当大maximumPoolSize后,执行拒绝策略。

(3)LinkedBlockingQueue【无界的任务队列,按照先进先出的顺序出队】,当线程池中的实际线程数小于corePoolSize,则优先创建新的线程;如果大于corePoolSize,并且此时恰好没有空闲线程,只要服务器资源足够多,就会无限制的进入入队操作,直到资源耗尽。所以使用了该任务队列的线程池最大支持线程数为corePoolSize,所以使用该队列的创建线程池的方法一般是corePoolSize和maximumPoolSize相等的newFixedThreadPool。

(4)PriorityBlockingQueue【优先任务队列,按照任务的优先级出队】,带有任务执行优先级的队列,可以控制任务执行的先后顺序,在确保系统性能的同时,对质量也有了保证。

handler参数分析

丢弃策略是系统超负荷运行的最后补救措施,线程池满了,并且阻塞队列满了,丢弃策略有如下四种:

(1)ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException异常,丢弃任务,阻止系统正常工作。 【Executors类创建线程池的默认丢弃策略
(2)ThreadPoolExecutor.DiscardPolicy:偷偷的丢弃任务,但是不抛出异常。 
(3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的任务(即队首马上要执行的任务),然后重新尝试执行任务(重复此过程)
(4)ThreadPoolExecutor.CallerRunsPolicy:只要线程池未关闭,直接去调用线程池的线程中处理该任务,可能引起性能的急剧下降。

execute方法与submit方法

前面说了,Executor是java.util.concurrent包下的一个线程池的鼻祖接口,只有一个抽象方法execute,该接口所有源码如下,execute方法入参为Runnable command。

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

该方法实际上是在ThreadPoolExecutor类中首次得到了实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个Runnable任务。所以我们在使用execute方法时,都要new Runnable。


submit()方法是在ExecutorService中声明的方法,如下:

/**
     * Submits a Runnable task for execution and returns a Future
     * representing that task. The Future's {@code get} method will
     * return {@code null} upon <em>successful</em> completion.
     *
     * @param task the task to submit
     * @return a Future representing pending completion of the task
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     * @throws NullPointerException if the task is null
     */
    Future<?> submit(Runnable task);

在AbstractExecutorService类中首次进行了实现,在ThreadPoolExecutor中继承下来并且没有重写,如下:

/**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

submit方法也是用来向线程池提交Runnable任务的,并且最终调用了execute方法,通过Future来获取任务执行结果。


execute和submit方法的不同点:

(1)execute没有返回值;而submit有返回值,方便返回执行结果。

(2)submit方便进行Exception处理,由于返回参数是Future,如果执行期间抛出了异常,可以使用Future.get()进行捕获。

合理的选择线程池大小

线程池设置过大,CPU、内存不够用,可能导致资源耗尽;

线程池设置过小,导致CPU空闲,降低吞吐量。

计算密集型

该任务需要大量CPU运算,没有阻塞,全速进行。

合适的线程池大小:CPU核心数+1

即使偶尔某个线程由于缺页中断或其他原因暂停,还有一个替补。

I/O密集型

I/O密集型,CPU不是一直在运转,还需要等待I/O时间等,因此线程数量应该设置的更大。必须估算出任务的等待时间与计算时间比值

合适的线程池大小:CPU核心数 * CPU利用率 *(1+任务等待时间 / 计算时间)

Runtime.getRuntime().availableProcessors()可以获取当前机器的CPU核数。 

  • 18
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值