多线程系列(四)线程池

前言

上一篇文章我们叙述了阻塞队列的概念,以及Java所提供的几种阻塞队列的使用以及区别,那么阻塞队列的应用场景除了生产者消费者还有那些呢?其实线程池内部核心就是通过阻塞队列来实现的,每种线程池的差异基本都体现在其内部阻塞队列的不同,这篇文章我为大家详细叙述Java中的线程池。

1 概述

什么是线程池呢?在编程领域中提到“池”这个概念,一般都跟缓存概念相似。我们先来看一下线程池的官方解释:

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。

什么意思呢?我用白话跟大家叙述一遍:"如果存在任务需要用线程去执行,那么可以将任务封装成Runnable存入到一个队列当中,当线程池启动后队列中的任务将会被取出然后逐个执行",这样和直接用线程执行任务有什么区别呢?我们都知道,多线程执行任务时需要调用Thread的start()方法来开启一个线程,而是用线程池只需要往里面加入Runnable任务即可。那用线程池执行任务又有什么好处呢?我们假设有一个需求,需要重复的开启线程去执行任务,这样就会创建大量的线程从而影响运行效率,这个时候我们就可以将任务交给线程池,由线程池内部所维护的线程去执行任务。上面我们提到,"池"的概念一般都和缓存相似,线程池也不例外,它可以对线程进行复用。

说完概念我们具体列一下线程池的优点:

  • 复用线程池中的线程,避免创建大量的线程而带来的性能开销
  • 能够有效的控制线程的并发数
  • 能够对线程进行简单管理

上面我们也提到了,线程池内部维护的是有线程的,那么Java中的线程池维护了几个线程?这个就不好说了,可能有一个、多个,也可能没有,这些可以在创建线程池的时候进行定义,具体怎么定义我会逐个用代码进行体现。

2 Java中的线程池

2.1 线程池的创建

在Java中创建线程池一般是通过ThreadPoolExecutor这个类来实现,我们来看一下这个类的几个构造方法:

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

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
         ...
         ...
         ...
    }

我们可以看到,最终调用的是拥有七个参数的构造方法,那这七个参数分别代表什么意思呢?别急,我来为大家一一解析

  • corePoolSize:核心线程数,默认情况下核心线程会一直存活,除非设置
    allowCoreThreadTimeOut为true后。
  • maximumPoolSize:线程池能容纳的最大线程数量,包括核心线程和非核心线程。
  • keepAliveTime:线程的存活时间,当线程闲置超过这个时间非核心线程会被回收,当核心线程设置allowCoreThreadTimeOut为true后超过这个时间也会被回收。
  • TimeUnit :闲置时间的时间单位。
  • BlockingQueue:阻塞队列,当线程池的线程数达到最大的线程数且无闲置线程,这时到来的任务会被放到阻塞队列当中。
  • ThreadFactory :线程工厂,用来创建线程。
  • RejectedExecutionHandler :当当前线程数达到maximunPoolSize并且阻塞队列也无空余空间会使用该策略拒接任务。它有以下四种取值:
    (1)ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    (2)ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但不抛出异常。
    (3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后尝试执行此任务(重复此过程)。
    (4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
    开发者实也可以现接口RejectedExecutionHandler定制自己的策略。

2.2 线程池的使用

Java为我们封装了四个线程池可以直接使用的,分别是:FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduleThreadPool。

FixedThreadPool

FixedThreadPool:核心线程数等于最大线程数,也就是说只有核心线程,它的keepAliveTime为0意味着内部不存在闲置的线程,阻塞队列使用的是无界阻塞队列LinkedBlokingQueue。

使用方法:

//声明最大核心线程数为3
ExecutorService service = Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
       final int finalI = i;
       //执行任务
       service.execute(new Runnable() {
               @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"-----+"+ finalI);
                }
            });
 }

打印结果

pool-1-thread-2-----+1
pool-1-thread-1-----+0
pool-1-thread-3-----+2
pool-1-thread-1-----+4
pool-1-thread-2-----+3
pool-1-thread-2-----+7
pool-1-thread-1-----+6
pool-1-thread-3-----+5
pool-1-thread-1-----+9
pool-1-thread-2-----+8

十个任务由三个线程执行,这样就提升了线程的利用率并且避免了创建过多线程产生的效率问题。一下几种线程池使用方法基本相同,就不一一演示。

CachedThreadPool

CachedThreadPool:corePoolSize为0意味着没有核心线程,maximumPoolSize为Integer.MAX_VALUE,keepAliveTime为60L,所以线程的闲置时间为60s,阻塞队列使用的是SynchronousQueue,上一节我们说到,这个队列时不进行存储内容的,所以这个线程池特点就是任务一过来就会马上去执行,如果没有空闲线程就创建新的线程去执行任务。

SingleThreadExecutor

SingleThreadExecutor:线程池中只有一个核心线程,也就是说同时只执行一个任务,其他任务会保存在队列中。用的队列是LinkedBlokingQueue。

ScheduleThreadPool

ScheduleThreadPool:最大的特点是可以延时启动任务,并且可以一直重复执行,核心线程数固定,非核心线程没有限制。用的队列是DelayedQueue。

2.3 线程池执行任务流程

通过前面几个小节我们了解到线程池的基本概念以及线程池的使用,那么线程池内部是怎样控制线程和执行任务的呢?

现在来了一个任务需要执行:

  • 当未超过核心线程数时,就用核心线程去执行。
  • 当超过核心线程数并且核心线程都处于非闲置状态时,就会将任务存放在阻塞队列当中。
  • 当阻塞队列存满后,会开启非核心线程。
  • 当总线程数超过最大线程数时会触发RejectedExecutionHandler策略
  • 具体的流程应该是corePoolSize->workQueue->miximumPoolSize

2.4 线程池中常见的方法

  • 1.shutdown:当执行该方法的时候会停止接收任何新任务,等待已经提交的任务执行完成(已提交的任务包括正在执行的和队列中等待的),当所有提交的任务执行完毕后会关闭ExecutorService。调用了shutDown方法后就不能添加任务了,如果继续添加任务会抛出RejectedExecutionExeception异常
  • 2.awaitTermination:这个方法有两个参数,一个是timeout超时时间,另一个是unit时间单位。这个方法会使线程等待timeout时间,当等待结束后会检测ExecutorService是否已经关闭,如果关闭返回true,否则放回false。
  • 3.shotdownNow:这个方法会强制关闭ExecutorService,取消所有正在执行和队列中的任务,并且会返回一个list列表,这个列表中是队列中的任务。
  • 4.isTerminated:这个方法会校验ExecutorService是否处于关闭状态,是的话返回true,否则返回false,
  • 5.isShotdown:ExecutorService关闭后返回true,否则返回false。

需要注意,这几个方法都是属于ExecutorService。

总结

线程池存在很多优点,但并不是说所有任务都可以用线程池去执行,如果碰到需要频繁创建线程的需求,可以优先考虑用线程池去执行任务,但是一个非常简单的任务就没有必要开启一个线程池去执行了(杀蚂蚁没必要用炮轰对吧!)。至此,本系列文章关于线程基础方面的知识点叙述完毕,下面几篇文章我会从源码的角度去分析一些基于线程实现的优秀框架。下一篇文章为大家带来《多线程系列(五)Handler源码详细解析》

你可能感兴趣的:(多线程系列(四)线程池)