zoukankan      html  css  js  c++  java
  • 线程池总结

    ref:https://www.cnblogs.com/dongguacai/p/6030187.html

         http://www.360doc.com/content/15/0511/14/12726874_469670444.shtml

         https://blog.csdn.net/honghailiang888/article/details/51690711

         http://www.open-open.com/lib/view/open1406778349171.html

       https://blog.csdn.net/chaofanwei2/article/details/51393794

         http://www.360doc.com/content/15/0511/14/12726874_469670444.shtml

    什么是线程池?

      诸如web服务器、数据库服务器、文件服务器和邮件服务器等许多服务器应用都面向处理来自某些远程来源的大量短小的任务。构建服务器应用程序的一个过于简单的模型是:每当一个请求到达就创建一个新的服务对象,然后在新的服务对象中为请求服务。但当有大量请求并发访问时,服务器不断的创建和销毁对象的开销很大。所以提高服务器效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这样就引入了“池”的概念,“池”的概念使得人们可以定制一定量的资源,然后对这些资源进行复用,而不是频繁的创建和销毁。

      线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,即均为启动,不消耗CPU,而只是占用较小的内存空间。当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。

    线程池简介    

      多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。       

      假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
      如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
                    一个线程池包括以下四个基本组成部分:
                    1、线程池管理器(ThreadPool):用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
                    2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
                    3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
                    4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。                
      线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
      线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
      假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。

      代码实现中并没有实现任务接口,而是把Runnable对象加入到线程池管理器(ThreadPool),然后剩下的事情就由线程池管理器(ThreadPool)来完成了。

    线程池的创建

    复制代码
    1 public ThreadPoolExecutor(int corePoolSize,
    2                               int maximumPoolSize,
    3                               long keepAliveTime,
    4                               TimeUnit unit,
    5                               BlockingQueue<Runnable> workQueue,
    6                               RejectedExecutionHandler handler) 
    复制代码

      corePoolSize:线程池核心线程数量

      maximumPoolSize:线程池最大线程数量

      keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

      unit:存活时间的单位

      workQueue:存放任务的队列

      handler:超出线程范围和队列容量的任务的处理程序

    线程池的实现原理

      提交一个任务到线程池中,线程池的处理流程如下:

      1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

      2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

      3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

    图1 线程池的主要处理流程

    java类库中提供的线程池简介

      java提供的线程池更加强大,相信理解线程池的工作原理,看类库中的线程池就不会感到陌生了。

    图2 JDK类库中的线程池的类框图 

    图3 Executors类的生成ExecutorService实例的静态方法

      Java通过Executors提供四种线程池,分别为:
      newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
      newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
      newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
      newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

      (1) newCachedThreadPool
      创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例代码如下:

      Java代码  
      1.   package test;  
      2.   import java.util.concurrent.ExecutorService;  
      3.   import java.util.concurrent.Executors;  
      4.   public class ThreadPoolExecutorTest {  
      5.    public static void main(String[] args) {  
      6.     ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
      7.     for (int i = 0; i < 10; i++) {  
      8.      final int index = i;  
      9.      try {  
      10.       Thread.sleep(index * 1000);  
      11.      } catch (InterruptedException e) {  
      12.       e.printStackTrace();  
      13.      }  
      14.      cachedThreadPool.execute(new Runnable() {  
      15.       public void run() {  
      16.        System.out.println(index);  
      17.       }  
      18.      });  
      19.     }  
      20.    }  
      21.   }  

      线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

      (2) newFixedThreadPool
      创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:

      Java代码  
        1.   package test;  
        2.   import java.util.concurrent.ExecutorService;  
        3.   import java.util.concurrent.Executors;  
        4.   public class ThreadPoolExecutorTest {  
        5. public static void main(String[] args) {  
        6.  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  
        7. for (int i = 0; i < 10; i++) {  
        8. final int index = i;  
        9.   fixedThreadPool.execute(new Runnable() {  
        10. public void run() {  
        11. try {  
        12.   System.out.println(index);  
        13.   Thread.sleep(2000);  
        14.  } catch (InterruptedException e) {  
        15.    e.printStackTrace();  
        16.    }  
        17.   }  
        18.   });  
        19.  }  
        20.  }  
        21. }  

      因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
      定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。

      (3)  newScheduledThreadPool
      创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

      Java代码  
        1.   package test;  
        2.   import java.util.concurrent.Executors;  
        3.   import java.util.concurrent.ScheduledExecutorService;  
        4.   import java.util.concurrent.TimeUnit;  
        5.   public class ThreadPoolExecutorTest {  
        6. public static void main(String[] args) {  
        7.  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
        8. scheduledThreadPool.schedule(new Runnable() {  
        9. public void run() {  
        10.   System.out.println("delay 3 seconds");  
        11.  }  
        12. }, 3, TimeUnit.SECONDS);  
        13.  }  
        14. }  

      表示延迟1秒后每3秒执行一次。

      (4) newSingleThreadExecutor
      创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:

      Java代码  
        1.   package test;  
        2.   import java.util.concurrent.ExecutorService;  
        3.   import java.util.concurrent.Executors;  
        4.   public class ThreadPoolExecutorTest {  
        5.    public static void main(String[] args) {  
        6.     ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  
        7.     for (int i = 0; i < 10; i++) {  
        8.      final int index = i;  
        9.      singleThreadExecutor.execute(new Runnable() {  
        10.       public void run() {  
        11.        try {  
        12.         System.out.println(index);  
        13.         Thread.sleep(2000);  
        14.        } catch (InterruptedException e) {  
        15.         e.printStackTrace();  
        16.        }  
        17.       }  
        18.      });  
        19.     }  
        20.    }  
        21.   }  

      结果依次输出,相当于顺序执行各个任务。

    线程池技术要点

      从内部实现上看,线程池技术可主要划分为如下6个要点实现:

    图4 线程池技术要点图

    •  工作者线程worker:即线程池中可以重复利用起来执行任务的线程,一个worker的生命周期内会不停的处理多个业务job。线程池“复用”的本质就是复用一个worker去处理多个job,“流控“的本质就是通过对worker数量的控制实现并发数的控制。通过设置不同的参数来控制 worker的数量可以实现线程池的容量伸缩从而实现复杂的业务需求。
    • 待处理工作job的存储队列:工作者线程workers的数量是有限的,同一时间最多只能处理最多workers数量个job。对于来不及处理的job需要保存到等待队列里,空闲的工作者work会不停的读取空闲队列里的job进行处理。基于不同的队列实现,可以扩展出多种功能的线程池,如定制队列出队顺序实现带处理优先级的线程池、定制队列为阻塞有界队列实现可阻塞能力的线程池等。流控一方面通过控制worker数控制并发数和处理能力,一方面可基于队列控制线程池处理能力的上限。
    • 线程池初始化:即线程池参数的设定和多个工作者workers的初始化。通常有一开始就初始化指定数量的workers或者有请求时逐步初始化工作者两种方式。前者线程池启动初期响应会比较快但造成了空载时的少量性能浪费,后者是基于请求量灵活扩容但牺牲了线程池启动初期性能达不到最优。
    • 处理业务job算法:业务给线程池添加任务job时线程池的处理算法。有的线程池基于算法识别直接处理job还是增加工作者数处理job或者放入待处理队列,也有的线程池会直接将job放入待处理队列,等待工作者worker去取出执行。
    • workers的增减算法:业务线程数不是持久不变的,有高低峰期。线程池要有自己的算法根据业务请求频率高低调节自身工作者workers的 数量来调节线程池大小,从而实现业务高峰期增加工作者数量提高响应速度,而业务低峰期减少工作者数来节省服务器资源。增加算法通常基于几个维度进行:待处 理工作job数、线程池定义的最大最小工作者数、工作者闲置时间。
    • 线程池终止逻辑:应用停止时线程池要有自身的停止逻辑,保证所有job都得到执行或者抛弃。

     线程池的拒绝策略

      池子有对象池如commons pool的GenericObjectPool(通用对象池技术)也有java里面的线程池ThreadPoolExecutor,但java里面的线程池引入了一个叫拒绝执行的策略模式,感觉比GenericObjectPool好一点,意思也就是说当池子满的时候该如何执行还在不断往里面添加的一些任务。 

      像GenericObjectPool只提供了,继续等待和直接返回空的策略。而ThreadPoolExecutor则提供了一个接口,并内置了4中实现策略供用户分场景使用。
      ThreadPoolExecutor.execute(Runnable command)提供了提交任务的入口,此方法会自动判断如果池子满了的话,则会调用拒绝策略来执行此任务,接口为RejectedExecutionHandler,内置的4中策略分别为AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。

    图5 拒绝策略关系图

      AbortPolicy

      为java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。

      DiscardPolicy

      直接抛弃,任务不执行,空方法

      DiscardOldestPolicy

      从队列里面抛弃head的一个任务,并再次execute 此task。

      CallerRunsPolicy

      在调用execute的线程里面执行此command,会阻塞入口。

      用户自定义拒绝策略

      实现RejectedExecutionHandler,并自己定义策略模式。

      再次需要注意的是,ThreadPoolExecutor.submit() 函数,此方法内部调用的execute方法,并把execute执行完后的结果给返回,但如果任务并没有执行的话(被拒绝了),则submit返回的future.get()会一直等到。 

      future 内部其实还是一个runnable,并把command给封装了下,当command执行完后,future会返回一个值。

    简单线程池的设计

      一个典型的线程池,应该包括如下几个部分:
      1、线程池管理器(ThreadPool),用于启动、停用,管理线程池
      2、工作线程(WorkThread),线程池中的线程
      3、请求接口(WorkRequest),创建请求对象,以供工作线程调度任务的执行
      4、请求队列(RequestQueue),用于存放和提取请求
      5、结果队列(ResultQueue),用于存储请求执行后返回的结果

      线程池管理器,通过添加请求的方法(putRequest)向请求队列(RequestQueue)添加请求,这些请求事先需要实现请求接口,即传递工作函数、参数、结果处理函数、以及异常处理函数。之后初始化一定数量的工作线程,这些线程通过轮询的方式不断查看请求队列(RequestQueue),只要有请求存在,则会提取出请求,进行执行。然后,线程池管理器调用方法(poll)查看结果队列(resultQueue)是否有值,如果有值,则取出,调用结果处理函数执行。通过以上讲述,不难发现,这个系统的核心资源在于请求队列和结果队列,工作线程通过轮询requestQueue获得人物,主线程通过查看结果队列,获得执行结果。因此,对这个队列的设计,要实现线程同步,以及一定阻塞和超时机制的设计,以防止因为不断轮询而导致的过多cpu开销。

    图6 线程池工作模型

    线程池的优点

      1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。

      2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

    线程池的注意事项

      虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。

      (1)线程池大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与CPU 数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能。

      (2)并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。

      (3)线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。

  • 相关阅读:
    Zookeeper(1)---初识
    golang的一些零散笔记
    ELK使用过程中遇到的一些问题
    ECharts系列:玩转ECharts之常用图(折线、柱状、饼状、散点、关系、树)
    MySQL系列:Docker安装 MySQL提示错误:Access denied for user'root'@'localhost' (using password:yes)
    HTML+CSS系列:登录界面实现
    Apollo系列(二):ASP.NET Core 3.1使用分布式配置中心Apollo
    Apollo系列(一):分布式配置中心Apollo安装(Linux、Docker)
    为你的应用加上skywalking(链路监控)
    工作中,你是如何开始搭建一套容器云环境的呢?
  • 原文地址:https://www.cnblogs.com/banjinbaijiu/p/9022855.html
Copyright © 2011-2022 走看看