线程池-Executors

 

在实际项目中,大家或多或少都会遇到多线程设计问题。这是为什么呢?为了尽可能提高系统的吞吐量和性能。创建和使用线程非常简单,这也就导致很多项目在各种任务中频繁的创建线程,虽然与进程相比,线程是一种轻量级工具,但是创建和销毁都需要花费时间,太过于频繁,也会耗尽CPU和内存资源。

大量的使用线程,如果导致系统出现OOM,这就违反我们的初衷了。使用线程,我们要有一个度,真实环境中,应该对线程加以控制和管理。

初识线程池

所谓池,其实道理都是一样,相信大家肯定都使用过数据库连接池吧,为了避免频繁的创建和销毁数据库连接,连接池维护了一些数据库连接,每次使用都是从池中获取连接,而不是重新创建一个新的连接,使用完之后又归还给连接池,不会真正的销毁,这样就可以节约创建和销毁资源的时间。

线程池目的也在于此,为了避免频繁的创建和销毁线程,我们让创建的线程得以复用。

Executors

当然,线程池不需要我们自己去实现,强大的JDK已经为我们提供了支持。在java.util.concurrent核心并发包下,JDK为我们提供了一个线程池工厂类—Executors,现在我们就来认识它吧。

Executors提供了几种类型的线程池,如下:

public static ExecutorService newFixedThreadPool(int nThreads)

public static ExecutorService newWorkStealingPool()

public static ExecutorService newSingleThreadExecutor()

public static ExecutorService newCachedThreadPool()

public static ScheduledExecutorService newSingleThreadScheduledExecutor()

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

关于这几种类型的线程池,我们先来了解一下它们各自的用处:

newFixedThreadPool():返回一个固定数量线程的线程池。当有新任务提交时,若线程中有空闲线程,则立即执行,若无,则会把该任务存入到任务队列,等待空闲线程执行。

newWorkStealingPool():返回指定数量线程的分而治之线程池,不传参,则使用当前计算机可用CPU数量设定大小。采用分治法处理,利用fork()和join()调度任务。

newSingleThreadExecutor():返回只有一个线程的线程池。用法和newFixedThreadPool()一样。

newCachedThreadPool():该方法返回的线程池数量不确定,根据实际情况调整。当有新任务提交时,若有空闲线程则复用,若无,则会创建新的线程处理。当所有任务完成时,线程就会返回线程池等待复用,60s未被使用,就会被销毁。可创建线程数量的最大值:Integer.MAX_VALUE。

newSingleThreadScheduledExecutor():该方法返回一个ScheduledExecutorService对象的线程池。该对象具有定时功能,例如固定延时后执行或周期性执行某个任务。

newScheduledThreadPool():返回指定数量ScheduledExecutorService对象的线程池。用法和newSingleThreadScheduledExecutor()一样。

现在我们具体来说明一下线程池如何使用。

newFixedThreadPool

示例代码:

ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(System.currentTimeMillis()+" current thread id:"+Thread.currentThread().getId());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}
executorService.shutdown();

上述代码中,我们创建了带有5个固定线程的线程池,然后模拟现在有10个任务时,每个任务打印出当前线程id以及时间,方便我们观察执行结果。结果如下:

1545452504719 current thread id:15
1545452504719 current thread id:14
1545452504720 current thread id:16
1545452504720 current thread id:18
1545452504720 current thread id:17
1545452505719 current thread id:15
1545452505720 current thread id:16
1545452505720 current thread id:14
1545452505720 current thread id:17
1545452505720 current thread id:18

从10个任务的执行情况来看,前五个任务和后五个任务的执行时间刚好相差1s,并且前五个线程的id和后五个线程的id是一致的,这就很好的说明了该固定线程池的作用。

在这里我们使用的是newFixedThreadPool(),大家可以把第一行换成 ExecutorService executorService = Executors.newCachedThreadPool(),再看看结果如何。这里大家注意一下,使用shutdown关闭线程池,该方法不会立即终止所有任务,而是等待所有任务执行完成后,再关闭线程池。执行shutdown()后,线程池不能再接受新的任务。

newSingleThreadScheduledExecutor

定时任务,该线程池返回一个ScheduledExecutorService对象,通过查看源码,我们发现该对象有三种不同的方法来执行任务,如下:

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,long period,TimeUnit unit);

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay, long delay,TimeUnit unit);

通过注释我们了解到,schedule()方法会在给定时间执行一次,scheduleAtFixedRate和scheduleWithFixedDelay会对任务进行周期性调度,但是它两又有点小小的区别。scheduleAtFixedRate会按照initialDelay->initialDelay+period->initialDelay+2*period这样的频率执行,也就是下一次任务会在上一次开始执行时间上经过period后执行。scheduleWithFixedDelay会在上一次任务结束后再延时delay后执行。

示例代码:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("当前时间:" + System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
System.out.println("开始时间:" + System.currentTimeMillis());
//在一秒后执行
scheduledExecutorService.schedule(runnable,1, TimeUnit.SECONDS);
//在延时一秒后FixedRate执行
//scheduledExecutorService.scheduleAtFixedRate(runnable,1,1, TimeUnit.SECONDS);
//在延时一秒后FixedDelay执行
//scheduledExecutorService.scheduleWithFixedDelay(runnable,1,1, TimeUnit.SECONDS);

大家可以一步一步解开注释,看看执行结果是什么,会不会是我们上面说的那样执行呢。注意一点,当我们把sleep()设置为2s后,scheduleAtFixedRate是否还是按照原有的逻辑执行呢?答案是否,当上一次任务的执行时间超过指定等待时间后,就会在上次任务执行后立即执行。

在执行周期性任务时,大家应该注意异常处理,如果在执行过程中抛出异常,整个调度任务将会停止,后续任务将不会执行。

ThreadPoolExecutor

在了解了各个类型线程池的使用之后,我们来看看它们内部到底有什么不同呢?通过源码分析,我们发现它们内部都是使用的ThreadPoolExecutor,不同的线程池只是在ThreadPoolExecutor中由不同的参数获得,下面就来看看ThreadPoolExecutor的强大之处:

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

参数含义如下:

corePoolSize:线程池中核心线程数量。

           maximumPoolSize:线程池最大线程数量。

keepAliveTime:超出核心线程数量时,多出的空闲线程的存活时间。

unit: keepAliveTime的单位。

workQueue:任务队列,当没有空闲线程时,任务存储在该队列。

threadFactory:线程工厂,被用于创建线程。一般使用Executors.defaultThreadFactory()即可。若想清楚的了解自己任务的线程,可以利用该工厂定制化线程。

handler:拒绝策略,当任务太多,如何拒绝任务。

上面参数,我们重点说一下workQueue和handler,其它的参数都比较简单。

任务队列

workQueue是一个BlockingQueue接口,通常使用的队列按照功能划分为:有界队列、无界队列、直接提交队列、优先任务队列。

分类

实现

特点

有界队列

ArrayBlockingQueue

创建时必须指定容量。当有新任务时,如果使用线程数量 < corePoolSize,则会创建新的线程执行,如果 > corePoolSize,则会将新的任务加入到任务队列中。当任务队列已满,如果线程数量<maximumPoolSize,则创建新的线程执行任务,否则执行拒绝策略。

无界队列

LinkedBlockingQueue

该队列没有界限,除非是系统资源耗尽,否则它不会出现入队失败情况。当新的任务提交,如果使用线程数量<corePoolSize,线程池会创建新的线程执行任务,当线程数量=corePoolSize时并且没有空闲线程时,任务就会添加到任务队列中。像newFixedThreadPool()、newSingleThreadExecutor()就是使用的此队列。

直接提交队列

SynchronousQueue

该队列没有容量,也就是说它不存储任务。当有新的任务来时,如果没有空闲的线程,则会创建新的线程,如果线程数量已经达到最大值,就会执行拒绝策略。newCachedThreadPool()就是使用改队列,使用该队列时,应该把最大线程数量设置尽量大,否则很容易就会执行拒绝策略,newCachedThreadPool()就设置的为Integer.MAX_VALUE。

优先任务队列

PriorityBlockingQueue

控制任务的执行顺序,它属于无界队列,但是比较特殊。不论是无界还是有界队列都是按照先进先出的顺序处理任务,而PriorityBlockingQueue则可以根据任务自身的优先级顺序执行,它总能确保高优先级的任务先执行。

不知道大家有没有看过阿里巴巴开发手册,大家可以去看一看,里面全都是精华。其中有一条就是强制大家不要去使用Executors创建线程池,这是为什么呢?上面介绍到newFixedThreadPool()使用的是LinkedBlockingQueue,该线程池的corePoolSize=maximumPoolSize,所以线程数量不会有变动,当新的任务提交频繁时,该队列会一直存储,直到资源耗尽。再来看看newCachedThreadPool(),它使用的是SynchronousQueue,该队列不会存储任务,所以它会迫使线程池一直创建线程直到达到线程数量最大值,而线程数量最大值为Integer.MAX_VALUE,所以它也可能导致系统资源耗尽。

由上所述,大家应该知道为什么强制大家不要使用Executors创建线程池,而是使用ThreadPoolExecutor根据应用的具体情况自定义合适的线程池。

拒绝策略

当线程池使用线程已经达到最大数量,并且任务队列也已满时,为了缓解系统压力,就要使用拒绝策略。JDK内置了四种拒绝策略,如下:

分类

特点

AbortPolicy

直接抛出异常,阻止应用正常工作。(默认使用该策略)

CallerRunsPolicy

不会真正丢弃任务,当线程池未关闭,该策略直接在调用者线程中运行当前丢弃任务。

DiscardPolicy

丢弃无法处理的任务,不做任何处理。

DiscardOldestPolicy

丢弃最老的任务,也就是即将执行的任务,并尝试提交当前任务。

上述拒绝策略都实现了RejectedExecutionHandler,如果上述四种拒绝策略无法满足当前应用,大家可以实现该接口自定义拒绝策略。该接口定义如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

rejectedExecution()就是具体执行拒绝任务方法,其中r是当前请求执行的任务,executor为当前线程池。

这里由于篇幅问题,就不展示自定义拒绝策略代码,大家可以自行写写,比较简单。

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值