线程池总结一

为什么要线程池

java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。 多线程的情况下确实可以最大限度发挥多核处理器的计算能力。但是如果随意使用线程,对系统的性能反而有不利影响。 在简单应用里面看起来没有问题,如果创建了一个线程,并且在run()方法结束后自动回收该线程。但是如果在真实系统里面,可能会由于业务情况,开启了很多线程,当线程数量多大时,反而会耗尽cpu和内存资源。
A 创建和销毁线程也需要时间,假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,那么会得不偿失。
B 线程也需要占用内存空间,大量的线程会抢占宝贵的内存资源,可能会导致out of memory异常。且大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间。大量的线程也会抢占cpu的资源,cpu不停的在各个线程上下文切换中,反而没有时间去处理线程运行的时候该处理的任务。

什么是线程池?

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程,不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
通俗的理解:将线程池看成一个水缸,当你需要用水时,你可以拿,多了你可以放回水缸当中,当然也有些不恰当 ,水不可以被复用,就当水可以二次利用。
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。因此为了避免频繁的创建和销毁线程,让创建的线程进行复用,就有了线程池的概念。线程池里会维护一部分活跃线程,如果有需要,就去线程池里取线程使用,用完即归还到线程池里,免去了创建和销毁线程的开销,且线程池也会对线程的数量有一定的限制。线程池本质是对线程资源的复用

线程池的优势

1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。
4)线程池可以进行统一的分配,调优和监控

线程池架构

在这里插入图片描述
概括一下:

  • Executor是最基础的执行接口;
  • ExecutorService接口继承了Executor,在其上做了一些shutdown()、submit()的扩展,可以说是真正的线程池接口;
  • AbstractExecutorService抽象类实现了ExecutorService接口中的大部分方法;
  • TheadPoolExecutor继承了AbstractExecutorService,是线程池的具体实现;
  • ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;
  • ScheduledThreadPoolExecutor既继承了TheadPoolExecutor线程池,也实现了ScheduledExecutorService接口,是带"周期执行"功能的线程池;
  • Executors是线程池的静态工厂,其提供了快捷创建线程池的静态方法。
Executor接口
void execute(Runnable command);

可以用来执行已经提交的Runnable任务对象,这个接口提供了一种将“任务提交”与“任务执行”解耦的方法。

ExecutorService接口

“执行者服务”接口,可以说是真正的线程池接口,在Executor接口的基础上做了一些扩展,主要是

任务终止相关方法
/**
* 启动一次有序的关闭,之前提交的任务执行,但不接受新任务
* 这个方法不会等待之前提交的任务执行完毕
*/
void shutdown();
/**
* 试图停止所有正在执行的任务,暂停处理正在等待的任务,返回一个等待执行的任务列表
* 这个方法不会等待正在执行的任务终止
*/
List<Runnable> shutdownNow();
/**
* 如果已经被shutdown,返回true
*/
boolean isShutdown();
/**
* 如果所有任务都已经被终止,返回true
* 是否为终止状态
*/
boolean isTerminated();
/**
* 在一个shutdown请求后,阻塞的等待所有任务执行完毕
* 或者到达超时时间,或者当前线程被中断
*/
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
提交任务相关方法

可以生成用于追踪一个或多个异步任务执行结果的Future对象的 submit()相关方法

/**
* 提交一个可执行的任务,返回一个Future代表这个任务
* 等到任务成功执行,Future#get()方法会返回null
*/
Future<?> submit(Runnable task);
/**
* 提交一个可以执行的任务,返回一个Future代表这个任务
* 等到任务执行结束,Future#get()方法会返回这个给定的result
*/
<T> Future<T> submit(Runnable task, T result);
/**
* 提交一个有返回值的任务,并返回一个Future代表等待的任务执行的结果
* 等到任务成功执行,Future#get()方法会返回任务执行的结果
*/
<T> Future<T> submit(Callable<T> task);
ScheduledExecutorService接口
/**
* 在给定延时后,创建并执行一个一次性的Runnable任务
* 任务执行完毕后,ScheduledFuture#get()方法会返回null
*/
public ScheduledFuture<?> schedule(
    Runnable command,
    long delay, 
    TimeUnit unit);

/**
* 在给定延时后,创建并执行一个ScheduledFutureTask
* ScheduledFuture 可以获取结果或取消任务
*/
public <V> ScheduledFuture<V> schedule(
    Callable<V> callable,
    long delay, 
    TimeUnit unit);

/**
* 创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期
* 也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推
* 如果执行任务发生异常,随后的任务将被禁止,否则任务只会在被取消或者Executor被终止后停止
* 如果任何执行的任务超过了周期,随后的执行会延时,不会并发执行
*/
public ScheduledFuture<?> scheduleAtFixedRate(
    Runnable command,
    long initialDelay,
    long period,
    TimeUnit unit);

/**
* 创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟
* 如果执行任务发生异常,随后的任务将被禁止,否则任务只会在被取消或者Executor被终止后停止
*/
public ScheduledFuture<?> scheduleWithFixedDelay(
    Runnable command,
    long initialDelay,
    long delay,
    TimeUnit unit);

其中schedule方法用于单次调度执行任务。这里主要理解下后面两个方法

  • scheduleAtFixedRate:该方法在initialDelay时长后第一次执行任务,以后每隔period时长,再次执行任务。注意,period是从任务开始执行算起的。开始执行任务后,定时器每隔period时长检查该任务是否完成,如果完成则再次启动任务,否则等该任务结束后才再次启动任务,看下图示例
    也就是说在一定的时间内完成,管你剩余时间多少在这里插入图片描述
  • scheduleWithFixDelay:该方法在initialDelay时长后第一次执行任务,以后每当任务执行完成后,等待delay时长,再次执行任务,看下图示例
    完成之后等待一定的时间然后在进行下一个在这里插入图片描述
ThreadPoolExecutor ThreadPoolExecutor构造参数
public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)

corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用
workQueue
workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能
几种排队的策略:
同步队列
将任务直接交给线程处理而不保存它们,可使用SynchronousQueue
如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中(corePoolSize–>maximumPoolSize扩容)Executors.newCachedThreadPool()采用的便是这种策略
无界队列
可以使用LinkedBlockingQueue(基于链表的有界队列,FIFO),理论上是该队列可以对无限多的任务排队将导致在所有corePoolSize线程都工作的情况下将新任务加入到队列中。这样,创建的线程就不会超过corePoolSize,也因此,maximumPoolSize的值也就无效了
有界队列
可以使用ArrayBlockingQueue(基于数组结构的有界队列,FIFO),并指定队列的最大长度
使用有界队列可以防止资源耗尽,但也会造成超过队列大小和maximumPoolSize后,提交的任务被拒绝的问题,比较难调整和控制。

threadFactory

创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名

/**
* The default thread factory
*/
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +poolNumber.getAndIncrement() +"-thread-";
    }
public Thread newThread(Runnable r) {
        Thread t = new Thread(
            group, 
            r,
            namePrefix + threadNumber.getAndIncrement(),
            0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
       }
}

Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”

RejectedExecutionHandler(饱和策略)

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

ThreadPoolExecutor线程池执行流程

在这里插入图片描述
(1)如果线程池中的线程数量少于corePoolSize,就创建新的线程来执行新添加的任务;
(2)如果线程池中的线程数量大于等于corePoolSize,但队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将队列中的任务交付给空闲的线程执行);
(3)如果线程池中的线程数量大于等于corePoolSize,且队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;
(4)如果线程池中的线程数量等于了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理
总结,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize
另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池

Executors创建常用线程池
newFixedThreadPool:固定线程数量线程池
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue <Runnable>());
    }

    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue <Runnable>(),
                threadFactory);
    }

创建一个指定工作线程数的线程池,其中参数 corePoolSize 和 maximumPoolSize 相等,阻塞队列基于LinkedBlockingQueue
FixedThreadPool:因为核心线程数是传入且固定的,所以称为有界线程池,一般在后台执行一些辅助性的任务,最大线程数与核心线程数相等;假设核心线程数为3,一次执行20个任务:先启动3个线程,剩下17个任务会进入BlockingQueue排队;因为核心线程数数=最大线程数,所以keepAliveTime这个参数是没有意义的。

  • 它是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源
newSingleThreadExecutor:单个线程的线程池
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue <Runnable>()));
    }

    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue <Runnable>(),
                        threadFactory));
    }

初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行,内部使用LinkedBlockingQueue作为阻塞队列

newCachedThreadPool:可缓存工作线程线程池

创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
**必要的时候创建新线程来处理请求,也会重用线程池中已经处于可用状态的线程。**如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程;当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
此类型线程池特别适合于耗时短,不需要考虑同步的场合

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue <Runnable>());
    }

    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue <Runnable>(),
                threadFactory);
    }

创建一个可缓存工作线程的线程池,默认存活时间60秒,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
在没有任务执行时,当线程的空闲时间超过keepAliveTime,则工作线程将会终止,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销
newCachedThreadPool:核心线程数为0,最大线程数为Integer.MAX_VALUE,所以称之为无界线程池;假设一次执行20个任务,由于corePoolSize为0,所以20个任务全会进入阻塞队列BlockingQueue,启动新线程执行队列中的任务,最多可以启动20个任务,如果20个任务都执行完毕,从线程闲置时开始倒计时60s,超时则关闭线程。

newScheduledThreadPool:周期性执行任务线程池
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据
注意

  • ScheduledExecutorService#scheduleAtFixedRate()指的是“以固定的频率”执行,period(周期)指的是两次成功执行之间的时间
    比如,scheduleAtFixedRate(command, 5, 2, second),第一次开始执行是5s后,假如执行耗时1s,那么下次开始执行是7s后,再下次开始执行是9s后
  • 而ScheduledExecutorService#scheduleWithFixedDelay() 指的是“以固定的延时”执行,delay(延时)指的是一次执行终止和下一次执行开始之间的延迟
    还是上例,scheduleWithFixedDelay(command, 5, 2, second),第一次开始执行是5s后,假如执行耗时1s,执行完成时间是6s后,那么下次开始执行是8s后,再下次开始执行是11s后
Executors的慎用

只要需要创建线程的情况下,即使是在单线程模式下,我们也要尽量使用Executor。即:

  • ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
    //此处不该利用Executors工具类来初始化线程池
    但是,在《阿里巴巴Java开发手册》中有一条
  • 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    Executors 返回的线程池对象的弊端如下:
    FixedThreadPool 和 SingleThreadPool : 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
    CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
    可以看到,这是一个强制性的规则,并且是不允许使用Executors来创建,建议使用ThreadPoolExecutor来创建线程池
    FixedThreadPool是通过LinkedBlockingQueue来实现的。而我们知道LinkedBlockingQueue是一个链表实现的阻塞队列,而如果不设置其容量的话,将会是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。由于Executors中并未设置容量,所以应用可以不断向队列中添加任务,导致OOM错误。
    上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。
如何该利用ThreadPoolExecutor来创建线程池呢?

我们其实可以看到Executors中的newFixedThreadPool其实也是调用ThreadPoolExecutor来实现的。正如手册中所说,当我们不用Executors默认创建线程池的方法,而直接自己手动去调用ThreadPoolExecutor,可以让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。比如我们在Executors.newFixedThreadPool基础上给LinkedBlockingQueue加一个容量,当队列已经满了,而仍需要添加新的请求会抛出相应异常,我们可以根据异常做相应处理。

public static ExecutorService newFixedThreadPool(int nThreads) {
 	return new ThreadPoolExecutor(nThreads, nThreads,
 	0L, TimeUnit.MILLISECONDS,
 	new LinkedBlockingQueue<Runnable>(10)); //添加容量大小
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值