线程池的实现原理

概念

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

为什么需要线程池:线程分为两种状态,有用户态和内核态,而Java线程依赖于内核线程,线程创建和销毁的时候都要进行用户态和内核态的切换,线程的上下文切换,消耗资源。可以使用线程池(可以理解为线程缓存池)重复利用线程执行多个任务,减少资源消耗。

使用线程池的好处:

1.减少创建和销毁线程上所花的时间以及系统资源的开销 

2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行

3.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存

线程池的处理流程

 

 

1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

2. 如果运行的线程等于或多于corePoolSize,则将任务加入Blocking-Queue

3. 如果无法将任务加入Blocking-Queue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

4. 如果创建新线程将使当前运行的线程超出maximumPoolSize任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution () 方法。

 

注意:ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

线程池的使用

创建

创建线程池有两种方式

// 第一种方式——快捷方式
ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);

// 第二种方式——标准方式
// 设定各种参数
ThreadPoolExecutor pool2 = new ThreadPoolExecutor(args);

1. 快捷创建线程池很简单,只需要调用Executors中相应的静态工厂方法即可,比如:

Executors.newFixedThreadPool(int nThreads)
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newSingleThreadExecutor() 创建只有一个线程的线程池
newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

 

 

 

 

但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。小程序使用这个快捷方法没什么问题,对于服务器需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor 的构造方法。

2. 使用ThreadPoolExecutor 创建线程池

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle(空闲)状态,也不会回收。
  int maximumPoolSize, // 线程数的上限
  long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                     // 超过这个时间,多余的线程会被回收。
  BlockingQueue<Runnable> workQueue, // 任务的排队队列
  ThreadFactory threadFactory, // 新线程的产生方式
  RejectedExecutionHandler handler) // 拒绝策略
// 传入指定参数即可创建
new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,milliseconds,
runnableTaskQueue, 
handler)

参数解释:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务书大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择一下几个阻塞队列:
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法-Executors.newFixedThreadPool () 使用了这个队列
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool 使用了这个队列
    • PriorityBlockingQueue:一个具有优先级无限阻塞队列。
  • maximumPoolSize(线程池的最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
  • RejectedExecutionHandler(饱和策略\拒绝策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK1.5中Java线程池框架提供了以下4种策略:
    • AbortPolicy:直接抛出异常(默认策略)。

    • CallerRunsPolicy:只用调用者所在线程来运行任务。

    • DiscardOldestPolicy:丢弃队列里最老的一个任务,尝试为当前提交的任务腾出位置 。

    • DiscardPolicy:不处理,丢弃掉。

   当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。

  • AliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。、
  • TimeUnit(线程保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS),千分之一微秒。

以运营一家装修公司做个比喻。

    公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单

线程池就是程序中的 “装修公司”。

向线程池提交任务

1. 可以向线程池提交的任务有两种:Runnable和Callable,Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。二者的区别如下:

  • 方法签名不同void Runnable.run(), V Callable.call() throws Exception

  • 是否允许有返回值,Callable 允许有返回值。

  • 是否允许抛出异常,Callable 允许抛出异常。、

2. 可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。

  • execute()只能提交Runnable类型的任务,无返回值,所以无法判断任务是否执行成功。且在执行任务时,如果遇到异常会直接抛出
  • submit()既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,通过这个future对象可以判断任务是否执行成功;但当任务类型是Runnable时,返回值为null。(这与Callable和Runnable接口要重写的函数有关)。并且在执行任务时,遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时才会抛出异常
    • future的get方法在任务为执行完未获得返回值(这个返回值就是执行call()完成后的返回值)之前一直阻塞,可以使用future的isDone()方法判断任务是否执行完成,然后再决定是否get。
    • 如果使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候任务有可能还未执行完

 1 public class TestThreadPoolBegin {
 2 
 3     public static void main(String[] args) throws Exception{
 4         ExecutorService es = Executors.newSingleThreadExecutor();
 5         Callable callable = new Callable() {
 6             @Override
 7             public String call() throws Exception {
 8                 System.out.println("线程处理开始...");
 9                 int a = 2;
10                 int b = 3;
11                 System.out.println("3/2的结果为:" + b/a);
12                 System.out.println("线程处理结束...");
13                 return "0";
14             }
15         };
16         Future<String> future = es.submit(callable);
17         while(true) {
18             //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。
19             if(future.isDone()) {
20                 System.out.println("任务执行完成:" + future.get());
21                 break;
22             }
23         }
24         es.shutdown();
25     }
26 }

更多示例见:https://www.cnblogs.com/jxxblogs/p/11882381.html

获取结果

1. 获取单个结果

通过submit()向线程池提交任务后会返回一个Future类型的对象,调用V Future的get()方法能够阻塞等待任务执行的结果,V get(long timeout, TimeUnit unit)方法可以指定等待的超时时间。

2. 获取多个结果

  • 如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future的get()获得。
  • 但对于这种场景,我们更应该使用ExecutorCompletionService该类的take()方法总是阻塞等到某一个任务的完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调用相同次数的take()方法,就能获取所有任务的执行结果,获取顺序是取决于任务的完成顺序。
 1 void solve(Executor executor, Collection<Callable<Result>> solvers)
 2    throws InterruptedException, ExecutionException {
 3    
 4    CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器
 5    
 6    for (Callable<Result> s : solvers)// 提交所有任务
 7        ecs.submit(s);
 8        
 9    int n = solvers.size();
10    for (int i = 0; i < n; ++i) {// 获取每一个完成的任务
11        Result r = ecs.take().get();
12        if (r != null)
13            use(r);
14    }
15 }
批量提交任务的伪代码

关闭线程池

 两种方法:shutdown或shutdownNow

原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

区别:

  • shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
  • shutdown将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminated 方法会返回 true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow 方法。

线程池的相关类

  • Executor 是一个顶层接口(类似一个标记接口),在它里面只声明了一个方法 execute (Runnable),返回值为 void,参数为 Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的;

  • ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、invokeAll、invokeAny 以及 shutDown 等;这个是线程池真正的接口。

  • 抽象类 AbstractExecutorService 实现了 ExecutorService 接口,基本实现了 ExecutorService 中声明的所有方法;

  • ThreadPoolExecutor 继承了类 AbstractExecutorService

线程池状态

1. RUNNING:这是最正常的状态,接收新的任务,处理等待队列中的任务。线程池的初始化状态时RUNNING。线程池一旦被创建,就处于RUNNINNG状态,并且线程池中的任务数为0.

2. SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown方法时,线程池由RUNNING -> SHUTDOWN。

3. STOP:不接受新的任务的提交,也不再处理等待队列中的任务。中断正在执行任务的线程。调用线程池的shotdownNow方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4. TIDYING:所有的任务都销毁了,workCount为0,线程池的状态在转换为TIDYING时会调用terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理,可以通过重载terminated()函数实现。

5. TERMINATED:线程池处于TIDYIN状态时,执行完terminated()之后,就会由TIDYING -> TERMINATED。

注:

  • 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN->TIDYING.
  • 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

合理配置线程池(合理设置线程池大小)

 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。

  • CPU密集型任务:主要是执行计算机任务,需要大量的运算,而没有阻塞,CPU一直全速运行,这种任务cpu的利用率很高,线程个数为CPU核数。这几个线程可以并行执行,不存在线程切换的开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗。注意只有在真正的多核CPU上才可能得到加速,在单核CPU上,无论开几个模拟线程,该任务都不可能得到加速。
  • IO密集型任务:即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要是利用了被浪费掉的阻塞时间。

性质不同的任务可以交给不同规模的线程池执行:

  • CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数;
  • IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1;
  • 混合型任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,就没必要拆分了。

若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时等待的时间越长,则CPU空闲的时间越长,那么线程数应设置得越大,才能更好的利用CPU。

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

可以得出一个结论: 
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 
以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。

可以通过 Runtime.getRuntime ().availableProcessors () 方法获得当前设备的 CPU 个数。

posted on 2020-07-21 20:13  小小糖果tt  阅读(116)  评论(0)    收藏  举报