美文网首页
Java线程池初探

Java线程池初探

作者: 风小扬 | 来源:发表于2020-06-19 19:49 被阅读0次

    Java线程池初探

    1. 为什么要用线程池?


    多核CPU时代,为更好利用资源以获取更高的性能,多线程编程早已普通应用。手工创建和销毁线程有以下弊端。

    • 线程上下文的切换需要JVM和操作系统的参与,若频繁操作势必造成CPU和内存资源的浪费。
      举个栗子。若为每个请求都创建线程来处理,当有大量恶意请求到来时,内存轻意就被攻占了。此时你是否会向运维同学央求...
    • 程序员需要处理线程的异常状态,如线程因为出错导致异常等。否则,不知哪天用户就会微笑着对你说...
    • 良好的软件设计不建议手工创建和销毁线程。

    幸运的是,线程池可以提供一条龙服务,帮助我们管理线程的创建、执行和销毁。还能根据系统承受能力,合理利用已有线程,减轻负担。一名话概括:集中管理线程,以期达到收益最大化,风险最小化

    2. 线程池如何使用?


    2.1 类结构
    • 线程池从JDK1.5版本开始引入,在JDK1.7中线程池框架的核心类是ThreadPoolExecutor,其关键类结构如下图:
    类结构
    2.2 ThreadPoolExecutor构造方法
    • JDK源码中该类的构造方法有4个,但最终都是调用以下构造方法来创建一个线程池。

      ThreadPoolExecutor (
          int corePoolSize,
          int maximumPoolSize,
          long keepAliveTime,
          TimeUnit unit,
          BlockingQueue<Runnable> workQueue,
          ThreadFactory threadFactory,
          RejectedExecutionHandler handler
      )
      
      • corePoolSize:核心线程数量。达到该数量后新提交的线程会进入任务队列中排队。
      • maximumPoolSize:池中允许的最大线程数量。需要与任务队列的类型结合使用。
      • keepAliveTime:池中线程数量超过corePoolSize时,空闲线程会在多长时间内被回收。
      • unitkeepAliveTime的时间单位。
      • workQueue:任务队列。它负责暂时保存未被执行的任务。
      • threadFactory:创建线程的工厂。
      • handler:拒绝策略,即当提交到池中的线程数量超过其最大容量时,如何拒收。
    • 看了上面这一本正经的定义,是不是有点懵圈?! 下面用一个春运购票的栗子来说明下。

      • 假定春运期间你想要购买K6124次列车。通常情况下该列车有座位1000个(corePoolSize)。
      • 但春运期间车票异常抢手,初始的1000张票秒光,可怜的你没有抢到。不过12306推出一项“高端服务”:候补。即没买到票的人可以进入一个队列等待,这个队列就是workQueue。你选择候补,随即进入该队列等待。考虑到实际情况队列不应过大,假定其大小设为300。
      • 随着候补的人起来越多,没多久候补队列也满了。12306充分考虑到了大家回家的迫切需求,决定多加2个车厢来提供额外的200张票。这样就能提总计1200张票maximumPoolSize
      • 没过多久,额外提供的票也全部卖出。此时候补列队仍然是满的,售票系统则不再接受新的候补请求(拒绝策略handler)。
      • 春运结束后票也不太紧张了。若连续5天该车卖出的票数不超过1000张,则当初额外追加的2个车厢就被收回了。这里的5天就相当于keepAliveTime,天就是它的单位unit
    2.3 线程池工厂Executors

    如果不想用上面ThreadPoolExecutor看似复杂的构造方法去创建线程池,可以使用JDK提供的线程池工厂Executors。它提供了几种常用线程池的创建方法,主要有以下几种。

    • newFixedThreadPool(n): 创建固定长度的线程池,任务队列大小为Integer.MAX_VALUE,即相当于是无界队列。
    • newSingleThreadExecutor: 创建只有一个线程的线程池,任务队列大小也为Integer.MAX_VALUE
    • newScheduledThreadPool: 创建一个支持定时及周期性任务执行的线程池。它的任务队列采用数组来存储元素,其初始大小为16,但会动态扩容。其maximumPoolSizeInteger.MAX_VALUE
    • newCachedThreadPool: 创建一个可根据实际情况动态调整线程数量的线程池,任务处理采用不排队直接提交的方式。其maximumPoolSizeInteger.MAX_VALUE,空闲线程在60秒内会被自动回收。

    3. 线程池内部的原理如何?


    3.1 工作流程

    当主线程向线程池提交新任务时,工作流程概括如下所示:

    工作流程
    1. 若池中线程数量小于corePoolSize,则创建新的线程来执行此新添加的任务。
    2. 若池中线程数量大于等于corePoolSizeworkQueue还有空间,则将该任务放入队列。
    3. 若池中线程数量大于等于corePoolSizeworkQueue已满,但池中线程数量小于maximumPoolSize,则会创建新的线程来执行此新添加的任务。
    4. 若池中线程数量达到了maximumPoolSize,则不再授受新任务,并调用拒绝策略来处理。
    3.2 任务队列

    线程池使用阻塞式BlockingQueue来缓存暂时未处理的任务,以生产-消费者模式进行任务的存取,目的是在保证安全并发的前提下提高效率。BlockingQueue是一种特殊的集合,其主要类图如下:

    任务队列

    存取数据操作有3组

    • add/remove: 添加未能成功抛出异常。
    • offer/poll: 添加或获取未成功时即刻返回
      此外还提供带超时时间的版本,在指定时间内有限阻塞,若超时仍未成功则返回。
    • put/take: 添加或获取未成功时,会无限阻塞,直到条件满足。

    几种阻塞式队列

    • ArrayBlockingQueue:内部结构基于数组,生产端和消费端共用一个锁,创建时必须指明队列长度。
    • LinkedBlockingQueue:内部基于链表,生产端和消费端使用各自独立的锁控制存取。创建时若未指明长度,则默认为Integer.MAX_VALUE。线程池工厂中的newFixedThreadPoolnewSingleThreadExecutor就是使用的该队列。
    • SynchronousQueue: 无缓冲队列,内部只能容纳一个元素。添加元素时后会阻塞,直到元素被取走后才能继续添加。newCachedThreadPool使用该队列。
    3.3 拒绝策略

    当线程池不能再授受新任务的提交时,会采用以下几种策略进行处理。

    拒绝策略
    • AbortPolicy: 丢弃任务并抛出异常,这也是JDK线程池中的默认策略。
    • CallerRunsPolicy: 交由调用线程处理该任务。
    • DiscardOldestPolicy: 丢弃队列头部的任务,重新提交被拒绝的任务。
    • DiscardPolity: 丢弃任务但不抛出异常,相当于什么也没发生。

    4. 灵魂拷问?


    • 并发情况下的共享资源访问

    多线程应用程序的复杂性会有所提升,尤其是涉及到共享资源的访问时,如若处理不当则系统出BUG的概率将上升。竞态下的共享资源访问需要用到锁机制来确保程序运行的正确性,这又是一个可以探讨的话题。

    • 使用线程池工厂的注意事项

    JDK中线程池工厂可以帮助我们快速创建线程池,使用虽然很便利,但我们要清楚地知道每种线程池背后的原理。如newFixedThreadPool,其线程池容量是有限的,但其任务队列却是无界的。如果提交的任务过多,可能会造成任务列队快速增长,最终导致内存溢出。

    又如newCachedThreadPool,其使用的是无缓冲的任务队列,但其maximumPoolSize却是Integer.MAX_VALUE(相当于是无限的)。如果任务处理的速度远远落后于任务提交的速度,同样可能会因为创建过多线程而导致内存溢出。

    相关文章

      网友评论

          本文标题:Java线程池初探

          本文链接:https://www.haomeiwen.com/subject/quubxktx.html