线程池知识点扫盲

1、为什么需要线程池

在实际的项目开发中,使用线程会非常的消耗资源,如果对线程管理不善,还有可能引发系统问题,因此,在很多并发框架中都会使用线程池,使用线程池可以有以下几个好处:

  • 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  • 提高响应速度,当任务到达时,不需要等待线程的创建

  • 提高线程的可管理性,通过线程可以对线程进行合理的管理,根据系统的承受能力调整可运行的线程数量

2、线程池的继承结构

2.1继承结构

在jdk1.8环境下


  • Executor是用于线程并发执行的一个框架,这个框架将任务的提交与执行分离开来,里面仅有一个execute方法,它的作用是将任务提交到线程池中,线程池调用一个空闲的线程来执行该任务

void execute(Runnable command);

图解


  • ExecutorService中提供了管理线程终止的方法以及可以跟踪一个或多个异步任务进度的方法,如shutdown方法和shutdownNow方法,它们的作用是依次关闭正在执行的线程(线程是可中断的)

  • AbstractExecutorService提供了ExecutorService接口中方法的默认的实现。

  • ScheduledExecutorService主要提供了任务的可调度方法,比如任务的延时/定时执行,如schedule方法

public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
  • ThreadPoolExecutor是Executor框架中最核心的类,它是线程池的实现类,主要由核心线程池、最大线程池、阻塞队列和饱和策略等几部分组成

  • ScheduledThreadPoolExecutor主要用于在给定的延迟之后运行任务,或者定期执行任务。

2.2线程池的实现流程

当我们向线程池提交一个任务后,它是怎样实现的呢?


当调用者提交一个任务时,会进行以下的流程

(1)判断当前的核心线程池是否已满,核心线程池就是我们分配的线程数量,如果不满,则为该任务分配一个线程,否则进入(2)

(2)判断队列是否满。如果不满,则将提交的任务存储到阻塞队列里,否则进入(3)

(3)判断线程池是否已满,线程池是系统分配的可容纳的最大线程数量,默认为Integer.MaxValue,如果线程池未满,则在线程池中创建一个线程执行该任务,否则则按照一定的策略来处理无法执行的任务

2.3线程池的分类

线程池有个重要的实现类,ThreadPoolExecutor是线程池实现的核心类,是我们使用最广泛的,ScheduledThreadPoolExecutor主要用于定时任务的场景中。Executors根据每个实现类的具体的场景不同又细分为不同的类型,在下面的文章中,我们来一个个的进行讲解。

3、ThreadPoolExecutor讲解

我们以一个构造函数来具体讲解下ThreadPoolExecutor

  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
      //省略
    }
  • corePoolSize:核心线程数

当调用者提交一个任务时,会先判断核心线程池中是否有空闲的线程,如果有,则直接创建线程来处理任务,如果没有,则需要线程池中创建线程来执行任务

  • maximumPoolSize:线程池中最大允许的线程数量

线程总数=核心线程数量+非核心线程数量,举个例子,假如一个工地允许容纳的最大工人数量为40人,固定成员有25人(核心线程),一旦工作比较忙时,就需要招聘临时工(非核心线程)来加快工期,这里的固定成员可以理解为核心线程,临时人员理解为非核心线程。

  • KeepAliveTime:允许非核心线程闲置的时长

这个参数是对非核心线程闲置时长的定时,一旦该线程在没有任务的情况下超过了该时长,就会被销毁

  • TimeUnit unit:KeepAliveTime的单位

单位可以为毫秒、秒、分、时、天

  • workQueue

线程池中的任务队列,维护着等待执行的Runnable对象,当没有核心线程空闲时,就需要将新添加的任务放到这个队列中来等待处理,如果队列满了,则新建非核心线程来执行任务,常见的有以下几个队列

①ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO的原则对元素进行排序

②LinkedBlockingQueue:基于链表结构的阻塞队列,按FIFO的原则进行排序

③SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态

④PriorityBlockingQueue:具有优先级的无线阻塞队列,默认情况下采用自然顺序升序排列,也可自定义类实现compareTo()方法来指定元素的排序规则

  • threadFactory

用于设置创建线程的工厂

  • RejectExecutionHandler

饱和策略,当队列和线程池都满了,说明线程池处于饱和状态,必须采取一种策略来处理提交的任务,默认为AboutPolicy,jdk提供了以下4种策略

①AbortPolicy:直接抛出异常

②CallerRunsPolicy:只用调用者所在的线程来运行任务

③DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务

④DiscardPolicy:不处理,丢弃掉

ThreadPoolExecutor执行execute()方法的流程为如图所示


执行顺序分别按1->2->3->4

下面我们讲解下ThreadPoolExecutor在不同场景下的使用

3.1newFixedThreadPool

newFixedThreadPool被称为可重用的固定线程数的线程池,采用的是无界队列来运行这些线程,创建时需要指定线程数,这是为了更好的管理资源,构造newFixedThreadPool有两种方式,分别为

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)

两者的区别在于是否需要使用线程工厂,我们以不使用线程工厂为例

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • nThreads:指定的线程数量

  • 0L:指的的KeepAliveTime,即空线程等待任务的最长时间,设置为0L表明多余的空线程会立即被终止

  • TimeUnit.MILLISECONDS:等待时间以分钟为单位

  • LinkedBlockingQueue:使用的是链表阻塞队列

我们编写一段程序来使用newFixedThreadPool

测试程序

  • 主程序


public class FixedThreadPoolDemo {
    public static void main(String[] args){
        ExecutorService executorService= Executors.newFixedThreadPool(5);
        for(int i=0;i<7;i++){
            executorService.execute(new Task());
        }
        executorService.shutdown();
    }
}
  • Task程序

public class Task implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"  running");
    }   
}

测试结果

结果表明执行任务的线程数量是固定的

3.2newCachedThreadPool

newCachedThreadPool会根据需要创建线程,是大小无解的线程池,适用于执行短期异步的任务,它和newFixedThreadPool一样,也有两种方法,区别在于是否使用了线程工厂

public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

以newCachedThreadPool()来讲解

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

  • maximumPoolSize被设置为Integer.MAX_VALUE,表明最大线程池是无界的

  • KeepAliveTime被设置为60s

  • 采用的是没有容量的SynchronousQueue作为线程池的工作队列

因为线程池的最大容量时被设置为无界的,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度,可能会导致创建线程过道而导致耗尽CPU的内存资源

我们编写代码来测试下

测试代码

public class CacheThreadPoolDemo {
    public static void main(String[] args){
        ExecutorService executorService= Executors.newCachedThreadPool();
        for(int i=0;i<7;i++){
            executorService.execute(new Task());
        }
        executorService.shutdown();
    }
}

测试结果

我们在实验中提交了7个任务,线程池中就构造了7个线程

3.3newSingleThreadExecutor

newSingleThreadExecutor使用的是单个任务线程,它适用于顺序的执行各个任务,它同样有两种方法来进行构造

public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)

以newSingleThreadExecutor()为例讲解

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

  • corePoolSize被设置为1

  • maximuumPoolSize被设置为1

  • KeepAliveTime被设置为1,不等待

  • 采用的是链表阻塞队列

因为仅有一个线程,所以任务之间的执行都是顺序执行的

测试代码

public class SingleThreadPoolDemon {
    public static void main(String[] args){
        ExecutorService executorService= Executors.newSingleThreadExecutor();
        for(int i=0;i<7;i++){
            executorService.execute(new Task());
        }
        executorService.shutdown();
    }
}

测试结果

测试结果表明无论加入多个任务,都是由同一个线程来执行的

4、ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor主要用于任务的延迟/定期执行,主要有两类实现方法

4.1ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor主要用于给定延迟之后运行任务,有两种方法

public ScheduledThreadPoolExecutor(int corePoolSize)
public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory)

以ScheduledThreadPoolExecutor(int corePoolSize)讲解

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

该队列的很多参数采用的是ThreadPoolExecutor,但是队列变为了DelayWorkQueue,即使用延迟队列作为缓存队列

任务提交方式


我们编写一段代码来测试下

测试代码

public class ScheduledThreadPoolDemo {
    public static void main(String[] args){
        ScheduledExecutorService scheduledExecutorService= Executors.newScheduledThreadPool(3);
        System.out.println(System.currentTimeMillis());
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟执行三秒钟");
                System.out.println(System.currentTimeMillis());
            }
        },3, TimeUnit.SECONDS);
        scheduledExecutorService.shutdown();
    }
}

测试结果


参考文献

[1]方腾飞.java并发编程的艺术

[2]https://www.jianshu.com/p/a166944f1e73

[3]https://www.jianshu.com/p/9beab78a3afe

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值