线程池基础知识整理

950 阅读10分钟

转载请标明出处: www.jianshu.com/p/a38113f92…

1.为什么需要线程池

在面向对象编程中 ,创建和销毁对象是很耗时的,因为创建一个对象要获取内存资源或者其他更多资源.所以在日常编程中才会有意的避免过多的创建并不必要的对象.

线程的创建和销毁也是同样,而且相比于普通的对象更为消耗资源.线程池技术的引入,就是为了解决这一问题.

1.1 线程池简介

线程池是指在初始化一个多线程应用程序过程中创建的一个线程集合,线程池在任务未到来之前,会创建一定数量的线程放入空闲队列中.这些线程都是处于睡眠状态,即均未启动,因此不消耗CPU,只是占用很小的内存空间.当请求到来之后,线程池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理.

当预先创建的线程都处于运行状态时,线程池可以再创建一定数量的新线程,用于处理更多的任务请求.

如果线程池中的最大线程数使用满了,则会抛出异常,拒绝请求.当系统比较清闲时,也可以通过移除一部分一直处于停用状态的线程,线程池中的每个线程都有可能被分配多个任务,一旦任务完成,线程回到线程池中并等待下一次分配任务.

使用线程池可以提升性能,减少CPU资源的消耗,同时还可以控制活动线程,防止并发线程过多,避免内存消耗过度.

1.2 线程池优点总结

  • 复用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销
  • 能有效控制线程池的最大并发数量,避免大量线程之间因互相抢占系统资源而导致的阻塞现象.
  • 能够对线程进行简单的管理,并提供定时执行,指定间隔,循环执行等功能.

2.Executor的继承关系图

黄色为接口 , 蓝色为类


Executor继承关系图.png

体系成员简介:

  • Executor
    线程池体系的顶层接口,只有一个execute()方法用于执行Runnable任务.该体系内的所有类,接口都默认实现/继承 此接口,并在此基础上进行分类扩展.

    //源码
    public interface Executor {
    
      /**
       * @param command Runnable任务
       * @throws RejectedExecutionException 如果任务无法继续执行,则抛出此异常
       * @throws NullPointerException 如果传入的Runnable为null,则抛出此异常
       */
      void execute(Runnable command);
    }
  • ExecutorService
    Executor的扩展接口,用于定义一些Runnable管理相关的方法,比如

    • void shutdown(); 有序关闭已经提交的任务,但是不再接受新的任务,重复shotdown无效.而且此方法不会等待已提交任务的执行完毕.
    • List<Runnable> shutdownNow(); 尝试停止所有正在执行的任务,终止所有处于等待队列中的任务,并将这些等待被执行的任务返回给调用者
    • boolean isShutdown(); 判断线程池是否已关闭
    • boolean isTerminated(); 当调用了showdown()方法后,所有任务是否已执行结束.注意:如果不事先调用showdown()方法,则此方法永远返回false.
    • boolean awaitTermination(long timeout, TimeUnit unit); 当调用shotdown()方法后,调用此方法可以设置等待时间,等待执行中的任务全部结束,全部结束返回true.如果超时,或线程中断导致未全部结束则返回false.
    • <T> Future<T> submit(Callable<T> task); 提交有返回值的Runnable任务.
    • <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) 执行传入的任务,当所有任务执行结束或超时后,返回持有任务状态和结果的Future集合.注意:一个任务结束有两种情况:1.正常执行完成;2.抛出异常.
    • <T> T invokeAny(Collection<? extends Callable<T>> tasks); 执行传入的任务,只要有任何一个任务成功执行完成(不抛出异常),一旦有结果返回,或抛出异常,则其他任务取消.
  • ScheduledExecutorService
    ExecutorService的子接口,定义了延迟或周期性执行Runnable任务的方法.

  • AbstractExecutorService
    ExecutorService的默认抽象实现类,对ExecutorService进行了简单实现,开发者可以参考并重写这些方法.

  • SerialExecutorService
    ExecutorService的子接口,标记型接口,该类型的线程池会以队列(先进先出)的顺序执行提交的任务.

  • ThreadPoolExecutor
    AbstractExecutorService子接口的默认实现类,可以使用这个类自定义线程池使用.系统提供的几种常用线程池,最终都是通过此类来创建.

  • ScheduledThreadPoolExecutor
    ScheduledExecutorService子接口的实现类,继承ThreadPoolExecutor,用于延迟或周期性执行Runnable任务.

3.如何创建线程池

Executor是Java中的一个接口,其默认实现类是ThreadPoolExecutor,构造方法如下

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

ThreadPoolExecutor一共有4个重载的构造方法,上述代码中的后三位参数都是可选参数.
通过构造方法即可自定义线程池.然后通过execute()来执行Runnable任务.

涉及到的几个参数解释如下:

  • corePoolSize

线程池的核心线程数,默认情况下,核心线程会在线程池中一直存活,即使它们处于闲置状态.如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的[核心线程]在等待新任务到来时会有超时策略,这个时间间隔由keepAliveTime所指定,当等待时间超过keepAliveTime所指定的时长后,核心线程就会被终止.

  • maximumPoolSize

线程池所能容纳的最大线程数,当活动线程数达到这个数值后,后续的新任务将会被阻塞.

  • keepAliveTime

非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收.当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时, keepAliveTime同样会作用于核心线程.

这个[保活时间]设计的巧妙之处在于:当线程处于闲置状态时,并不马上销毁,而是在指定时间段内将其缓存在线程池中,以方便在限定的时间段内如果再有任务来临,能够快速的重新启用等待中的线程.处于闲置状态的空闲线程并不会占用多少内存,而且这样就能显著减少频繁的创建,销毁线程造成的内存消耗及性能下降.

  • unit

用于指定keepAliveTime参数的时间单位,这是一个枚举,常用的有TimeUnit.NANOSECONDS(毫秒),TimeUnit.SECONDS(秒),TimeUnit.MINUTES(分钟)等

  • workQueue

线程池中等待被执行的任务队列,这个队列仅持有通过execute方法提交的Runnable任务.

  • threadFactory

线程工厂,ThreadFactory是个接口,它只有一个方法,·Thread newThread(Runnable r),executor创建新线程时调用

  • RejectedExecutionHandler 当由于线程阻塞,任务队列容量已满等因素导致无法成功执行任务时,这个handler会调用rejectedExecution方法来通知调用者.

4.系统封装的4种线程池

这里要使用到Executors类,它是Executor体系的静态工厂类,类中封装了一些创建线程池的方法.
Executor与其子接口及其实现类负责定义和规范线程池,而Executors负责创建线程池.

4.1 SingleThreadExecutor

SingleThreadExecutor内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行.
它的意义在于统一所有的外界任务到一个线程中,使得在这些任务之间不需要处理线程同步的问题.

内部实现如下:

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

代码测试:

private void singleThreadExecutorTest() {
    //单一线程池
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    //开启5次线程
    for (int i = 0; i < 5; i++) {
        singleThreadExecutor.execute(new MyThread());
    }
    //关闭线程池
    singleThreadExecutor.shutdown();
}

Log输出:

System.out: pool-1-thread-1 执行
System.out: pool-1-thread-1 执行
System.out: pool-1-thread-1 执行
System.out: pool-1-thread-1 执行
System.out: pool-1-thread-1 执行

4.2 FixedThreadPoolExecutor

FixedThreadPoolExecutor是一种线程数量固定的线程池,只有核心线程,没有超时机制,任务队列没有大小限制.
当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲出来.
当线程处于空闲状态时,它们并不会被回收,除非线程池被关闭了.这意味着它能够更加快速的响应外界的请求.

//构造方法
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

代码测试:

/**自定义一个线程类,继承Thread,打印Log*/
class MyThread extends Thread{

    public void run() {
        System.out.println(Thread.currentThread().getName()+" 执行");
    }
}

private void fixedThreadPoolTest() {
    //容量为2的固定线程池
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);

    //创建5条线程
    for (int i = 0; i < 5; i++) {
        fixedThreadPool.execute(new MyThread());
    }
    //关闭线程池
    fixedThreadPool.shutdown();
}

Log输出:

System.out: pool-1-thread-1 执行
System.out: pool-1-thread-2 执行
System.out: pool-1-thread-2 执行
System.out: pool-1-thread-2 执行
System.out: pool-1-thread-1 执行

可以发现,虽然new了5个Thread,但是系统只用两条线程来执行5个任务.

4.3 CachedThreadPool

CachedThreadPool线程数量不定,只有非核心线程,并且其最大线程数为Integer.MAX_VALUE(相当于无限大)
当线程池中的线程都处于活动状态时,线程池会创建新的线程来处理新任务,否则就会利用空闲的线程来处理新任务.

空闲线程都有超时时机,这个超时时长为60秒,限制超60秒就会被回收.

和FixedThreadPoolExecutor不同的是,CachedThreadPool的任务队列其实相当一个空集合,这将导致任何任务都会被立即执行,因为此时SynchronousQueue是无法插入任务的.
CachedThreadPool线程池比较适合执行大量+耗时较少的任务.当整个线程池都处于闲置状态时,会因超时而被停止,此时没有线程的线程池几乎不占用任何系统资源.

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

代码测试:

private void cachedThreadPoolTest() {
    //缓存线程池
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

    //创建5条线程
    for (int i = 0; i < 5; i++) {
        cachedThreadPool.execute(new MyThread());
    }
    //关闭线程池
    cachedThreadPool.shutdown();
}

Log输出:

System.out: pool-1-thread-1执行
System.out: pool-1-thread-1执行
System.out: pool-1-thread-2执行
System.out: pool-1-thread-3执行
System.out: pool-1-thread-4执行

4.4 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor核心线程数固定,非核心线程数没有限制,并且非核心线程闲置时会被回收
主要用于执行定时任务 和 具有固定周期的重复任务.

//Executors静态方法
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

//继续追查ScheduledThreadPoolExecutor源码,会发现最终还是通过ThreadPoolExecutor的构造来创建
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

常用方法:

  • schedule(Runnable command,long delay, TimeUnit unit)XXX时间之后,执行指定的任务
  • scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
    XXX时间后,执行指定任务,每隔XXX时间执行一次

代码测试:

private void scheduledThreadPoolTest() {
    //定时任务线程池
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);

    //3秒后打印一次Log
    scheduledThreadPool.schedule(new Runnable() {
        @Override
        public void run() {
            System.out.println("~~~~~~~~~3秒之后露个脸~~~~~~~~~");
        }
    },3000,TimeUnit.MILLISECONDS);

    //0秒初始化延迟,每秒打印Log
    scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            System.out.println("每隔一秒打印一次");
        }
    },0,1000, TimeUnit.MILLISECONDS);
}

Log输出:

System.out: 每隔一秒打印一次
System.out: 每隔一秒打印一次
System.out: 每隔一秒打印一次
System.out: ~~~~~~~~~3秒之后露个脸~~~~~~~~~
System.out: 每隔一秒打印一次
System.out: 每隔一秒打印一次
System.out: 每隔一秒打印一次

5.总结

可以发现,系统提供的4种线程池,最终都是通过配置ThreadPoolExecutor的不同参数,来巧妙的达到不同的线程管理效果.

以上只是关于线程池的一些基础认知,下一篇文章分析线程池的运行原理.