线程池——治理线程的最大法宝

概述

线程池在处理线程一个非常有用的工具,学好线程池,我们对线程的掌控能力将上一个大台阶

1.线程池的自我介绍

2.创建和停止线程池

3.常见线程池的特点和用法

4.任务太多,怎么拒绝

5.钩子方法,给线程池加点料

6.实现原理、源码分析

7.使用线程池的注意点

一、线程池的自我介绍

1.1、线程池的重要性

想成为一个好的java工程师的话,要非常好的掌握线程池这个知识。

有很多问题都因为没有使用好线程池所导致的,即便找工作线程池在面试中也是常见的,为对于面试官而言,对于线程池的提问是层层递进的,有很多的方面或者很多的注意点都可以作为问题,来问面试者,也可以从中考察出面试者是不是对此有深入的思考,所以线程池也是面试的高频考点。

1.2、什么是 “池”

——软件中的 “池”,也可以理解为计划经济

“池”不仅仅出现在线程池,还有连接池…这里的 “池” 都是同一个意思。

举个栗子:在以前计划经济,那个时候还没有改革开放,那个时候买东西是要粮票、米票,为什么它会有这样的设定呢,就是因为那个时候我们资源还不是特别充沛,如果我们不按照这样计划去实施的话,可能有钱人把东西全都拿走,其他人拿不到,所以当时就有了这样的政策。

同样类比到 “池” ,实际比较相似的,我们的资源是有限的,比如说只有10个线程,我们就创造10个线程的线程池,可能你的任务是非常多的,比如说有一千个、两千个任务,但是我们总共的资源只有这10个线程,你就要把这两千个任务放到10个线程里面去,我慢慢的执行,最终肯定会完成你的任务,但是资源总量10个线程是被控制住的,不仅如此还有一个好处,有了这10个线程之后,我就不需要再去创建特别多的线程了,因为创建每一个线程,有很大开销的,我这10个线程,不仅总量把它控制住,还可以复用每一个线程,相当于是我们的工人,在经历了一定的训练之后,它的敏捷度和熟练度都提高了,那么这10个线程,实际上在执行的时候效率是很高的,因为它不需要去重新的创建、也不需要销毁,不停的执行任务就可以了,所以说线程池总体来讲有两个好处。

第一个可以复用每一个线程,第二个可以控制资源的总量。

1.3、如果不使用线程池,每个任务都新开一个线程处理

假设我们对线程池一无所知的话,有很多个的任务,比如说我们的任务采用处理的方法是,每来一个任务就新开一个线程,这也是最直观的思想。如果只有一个任务的话 ,自然创建一个线程就够,这个非常简单,如果比较多,比如说10个任务,10个的话把这个代码复制10份是不可取的,我们就用for循环去创建,for循环也是可以满足需求的。

实例代码(1):

package threadpool;

/**
 * 一个线程
 */
public class EveryTaskOneThread {
    public static void main(String[] args) {
        Thread thread = new Thread(new Task());
        thread.start();

    }

    static class Task implements Runnable {
        @Override
        public void run(){
            System.out.println("执行了任务");
        }
    }
}

实例代码(2):

package threadpool;

/**
 * 十个线程
 */
public class ForLoop {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Task());
            thread.start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run(){
            System.out.println("执行了任务");
        }
    }
}

这样做,表面上看,在初级阶段也没有太大的问题,毕竟满足了业务需求。

假设我们任务数量上升到1000个,怎么办?用For循环方法,确实是可以完成的,也就是说用这种方法,代码比较笨拙,但是效果是基本满足,可是为什么说它是笨拙的方法。

我们对于Java语言而言,每个Java中的线程,直接对应的操作系统中,这样一来相当于在操作系统中创建了1000个线程,这个会带来很大的开销,线程它的生命周期是非常高的,线程创建和销毁不是没有代价的,它需要我们的JVM和操作系统提供一些辅助操作,在这种大量线程创建的时候,会消耗很多的资源,尤其是内存,而且这些线程创建之后还要被回收,给垃圾回收器带来压力,并且我们系统程序它所创建线程的上限是存在的,如果说那1000个变成2万个怎么办?或者说2万个变成30万个怎么办?任务数量是不受我们控制的,或者说任务数量都不止30万,几百万个都有可能。它线程数量是有上限的, 所以这种情况下,如果你真的采取这种方法的话,很有可能超过我们的上限,导致出错,或者爆出OOM异常,也就是内存不足的异常。

有任务过来,不是说每一个任务都需要、或者说有必要去建一个线程来执行的。

演示异常发生:

1.4、为什么要使用线程池

问题一:反复创建线程开销大

问题二:过多的线程会占用太多内存

解决以上两个问题的思路

  • 用少量的线程——避免内存占用过多
  • 让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗

1.5、线程池的好处

①加快响应速度

  • 不再需要反复创建和收回,而且我们消除了线程创建带来的延迟之后,对用户体验也是增强的,因为速度更快

②合理利用CPU和内存

  • 更加合理的统筹,我们都知道CPU和内存它根本不是无限的,所以灵活调整我们线程的数量,以便于我们线程数量既不是太多导致溢出,也不会太少去浪费我们CPU资源,浪费我们内存资源,也就是我们通过线程池,非常灵活的掌握这个数量,达到一个平衡 ,达到效率最高点

③统一管理

  • 因为我们假设有很多个任务,我们需要一个统一的管理器来管理它们,比如说这3000个任务,执行一半我们不想执行怎么办,不能把刚才那3000个线程全部一个一个停止对不对!假设这个时候有线程池,我们就可以统一的去操作,统一的指挥它们,不仅方便管理,还方便数据统计

1.6、线程池适合应用的场合

  • 服务器接收到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率
  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

二、创建和停止线程池

2.1、创建线程池

①构造方法的参数

  • 创建它的时候,它有构造函数,那构造函数呢,线程池而言,参数比较复杂,像其他的类的构造函数比较简单,比如定义一个mapper,构造函数里面参数不传都可以,线程池呢,有非常多的构造函数,并且每一个的含义必须需要了解清楚才能非常良好的创建出来。

②线程池应该手动创建还是自动创建

  • 自动创建是非常方便的,但是对于线程池而言,自动创建有一些弊端 ,在一定情况下,我们手动创建会更好

③线程池里的线程数量设定为多少比较合适?

  • 假设有1000个任务,要用几个线程去执行它?

停止线程池的正确方法

参数名 类型 含义
corePoolSize int 核心线程数,详解见下文
maxPoolSize int 最大线程数,详解见下文
keepAliveTime long 保持存活时间
workQueue BlockingQueue 任务存储队列
threadFactory ThreadFactory 当线程池需要新的线程池的时候,会使用threadFactory来生成新的线程
Handler RejectedExecutionHandler 由于线程池无法接受你所提交的任务的拒绝策略

参数中的corePoolSize和maxPoolSize

  • corePoolSize指的是核心线程数
    • ——线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,在创建新线程去执行任务
    • 假设corePoolSize设置的是5,于是你突然放过来5个任务,那我就创建5个线程。
  • 最大量maxPoolSize
    • ——在核心线程的基础上,额外增加的线程数的上限
    • 同一个线程池,它可能在今天过来执行的任务非常非常的多,在明天可能就很少,这个不均匀的事情是常态的,在这种情况下,我们就引入maxPoolSize,来帮助我们更好应对情况。
    • 在刚才我们假设corePoolSize是5,并且已经创建了5个线程,那么这5个线程通常一直存活,因为它是我们的核心,它是core,核心的数量就是保持在5,所以即便是这段时间没有任务进来不会把线程数量减少在5以下,除非发生异常的情况,这种情况我们先忽略,那么有的时候这5个线程不足以去处理这么的任务,突然来到很多的任务的话,我们就需要用到更多的线程,这个更多的线程的也是需要有一个上限,因为这个多也不能是无穷无尽的多,所以这个上限就是我们的maxPoolSize

我们看这张图,是这样理解的,从左侧开始的容量,我们的任务进来就会创建线程,直到达到corePoolSize,比如说是5个,这是它核心处理线程,然后呢这个时候,又有一些线程过来了,它会放在它的存储队列中,我们知道在最开始的参数中,有一个是存储队列,这个队列就是用来存放这些任务的,也就是说它不想轻易突破这个corePoolSize,也就是说来了5个任务,这5个任务都在线程工作,又来了新的5个任务,它就会把这新的5个任务放在队列中,等到之前的5个谁处理完了,就再去拿1个进行处理,直到处理完所有任务。

所以这个时候还没有进入到中间块,也没有突破corePoolSize,可是如果我们的队列满了,因为我们的队列在一定的情况下是可以给它设定一定的容量的,假设这个容量是10,那么假设我们现在核心线程5个都在进行处理,并且存储队列容量10都已经被塞满了,还有更多任务还想放入到这个线程池里去执行,这个时候它的扩展性体现出来了,线程池就会继续在这个数量基础之上,去增加新的线程,来帮助我们执行

因为它觉得这些线程已经不足以去处理这么多的任务里,于是上面的Current size(当前线程池的容量)就会大于Core pool size并且逐渐往外扩展,最多扩展到max pool size

添加线程规则

1.如果线程数小于corePoolSize,创建一个新线程来运行新任务。

2.如果线程数等于(或大于)corePoolSize但少于maximumPoolSize,则将任务放入队列。

3.如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程。

4.如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝。

  • 是否需要增加线程的判断顺序是:
    • corePoolSize
    • workQueue
    • maxPoolSize
  • 比喻:烧烤店的桌子
    • 比如说我们去吃烧烤,吃烧烤的时候,在秋天,通常天气不是特别冷,也不是特别热,选择在店里面吃,店里面有5个桌子,这5个桌子是始终存在的(等同于corePoolSize),里面的桌子不够用了,有更多的客人来了, 就要拓展,把一些临时的桌子搬到外面去,因为外面的容量也是有限的,外面还能放5个桌子(等同于maxPoolSize),等到收摊的时候外面的桌子会被收回来的,而里面的桌子是不会被处理的,因为里面的是始终存在。

举个栗子

  • 线程池:核心池大小为5,最大池大小为10,队列为100。
  • 因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建最新的线程maxPoolSize ,最多到10个线程,如果再来任务,就拒绝。

增减线程的特点

1.通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池。

2.线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。

3.通过设置maximumPoolSize为很高的值,可以允许线程池容纳任意数量的并发任务。

4.只有在队列填满时才创建多于corePoolSize的线程,如果使用的是无界队列,那么线程数就不会超过corePoolSize。


keepAliveTime

  • 如果线程池当前的线程数多于corePoolSize,那么如果多于的线程空闲时间超过keepAliveTime,它们就会被终止。

ThreadFactory 用来创建线程

  • 默认使用Execution.defaultThreadFactory()
  • 创建出来的线程都在同一个线程组
  • 如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。

工作队列

  • 有3种最常见的队列类型:
    • 1)直接交接:SynchronousQueue
    • 2)无界队列:LinkedBlockingQueue
    • 3)有界的队列:ArrayBlockingQueue

总结

  • corePoolSize:表示线程池中的核心线程数量,创建的核心线程在没有被调用时,也不会被回收
  • maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
  • keepAliveTime:如果线程池当前的线程数多于核心线程数(corePoolSize),那么多余的线程空闲时间超过keepAliveTime后会被终止
  • handler:是一种拒绝策略,可以在任务满了之后,拒绝执行多余的任务

守护线程

守护线程 作用:给用户线程提供服务 在Java中有两类线程: User Thread(用户线程)、Daemon

 

 

 

 

 

 

 

 

Java

Spring boot配置AOP统一处理Web请求日志

2020-8-6 17:33:51

Java

守护线程

2020-8-11 17:16:05

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索