一篇带你入门Java线程池

Java线程池入门

什么是线程池

我们知道线程创建的多种方式:

  • 继承Thread
  • 实现Runnable
  • 实现Callable

线程池就是创建一个容器,来管理我们的线程,将线程的创建过程交给线程池,我们只需要调用线程池的execute()方法,将需要执行的任务(Runnable的实现类)传进去,线程池就能帮我们完成任务。

为什么要使用线程池

这是我们熟悉的任务管理器,我们打开性能可以看到,目前电脑上有194个进程和3214个线程,其中的线程就是指我们的内核线程

image-20200919210845880

执行普通的方法执行线程10000个线程

public class ThreadPoolDemo1 {

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread(ThreadPoolDemo1::run).start();
        }
    }
    
    public static void run(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们看到,系统线程飙升到13262和之前的3214相比高了10000不止(JVM启动时也会有一定的线程启动)。

实际上过了两秒钟又恢复到3000,程序挂了。

image-20200919211025402

使用线程池

我们使用最简单方式启动一个长度为10的定长线程池,我们启动一下。

public class ThreadPoolDemo1 {

    public static void main(String[] args) {
        Executor executor=Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            executor.execute(ThreadPoolDemo1::run);
        }
    }

    public static void run(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们看到线程变化不大,和之前直接飙升到10000+天壤之别。

image-20200919211217985

首先我们科普两个概念,用户线程和内核线程。我们的用户进程(可以理解为Thread类)不能直接被操作系统直接执行,需要转换为内核线程,然而这个过程比较耗时,这也是线程池出现的原因。

上述程序我们看到,使用线程池可以有效地限制线程的产生。

用户线程与内核线程

image-20200919210831103

线程池的基本思想

首先还是刚刚的例子,我们知道Thread.currentThread().getName()方法可以打印当前正在执行的线程的线程名字,我们从执行结果找找答案:

普通的方法执行20个线程

public class ThreadPoolDemo1 {

    public static void main(String[] args) {
        Executor executor=Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            new Thread(ThreadPoolDemo1::run).start();
            //executor.execute(ThreadPoolDemo1::run);
        }
    }
	//打印当前线程名
    public static void run(){
        System.out.println(Thread.currentThread().getName());
    }
}

结果:

Thread-2
Thread-3
Thread-6
Thread-7
Thread-0
Thread-1
Thread-10
Thread-4
Thread-5
Thread-16
Thread-11
Thread-14
Thread-15
Thread-18
Thread-19
Thread-12
Thread-13
Thread-8
Thread-17
Thread-9

使用线程池执行20个线程

public class ThreadPoolDemo1 {

    public static void main(String[] args) {
        Executor executor=Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            executor.execute(ThreadPoolDemo1::run);
        }
    }
	//打印当前线程名
    public static void run(){
        System.out.println(Thread.currentThread().getName());
    }
}

结果

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

区别:

我们看到普通的方式就是创建20个线程,默认名是Thread加个数,从1开始。

线程池的name被修改过,并且我们可以看到pool-1-thread-1,pool-1-thread-2都有执行两次任务。

所以线程池简单的说就是创建一个线程多次使用,由于用户线程转换为内核线程劳民伤财,不如我们创建好之后反复使用,这就是线程池的基本思想

线程池的执行流程

我们看一下这个图,然后我一一解释图中的各个部分:

image-20200919211340536

简单流程

  • 首先我们要执行一个任务,先尝试交给线程池执行,线程池有一定的容量,图中是5。

  • 如果线程池中的工作线程都在执行任务,那么我们就将任务加入到图中的队列中去等待,一旦线程池中的有一个线程执行完任务,则去队列里面找有没有人在排队,如果有,按照先进先出的顺序获取一个任务去执行。

  • 但是我们的队列也是有一定的容量的,加入当前线程池中工作线程达到10,等待队列也满了,我们再提交一个任务,我们会判断一个这个线程池是否还能再扩容,初始化线程池有个参数可以设置,如果可以扩容,我们创建一个新的工作线程,然后执行我们的新任务。

  • 这时候就走图中的红色框 拒绝策略,也就是我们拒绝这一个任务,拒绝的方式有很多,什么事都不做,抛出异常,或者扩充线程池等等。

补充

别以为线程池的逻辑就这么简答,上述流程只是为了简单的说明线程池的工作流程。实际上线程池的具体业务逻辑实现非常的复杂,比如创建工作线程的方式,各种条件判断,等待队列,还有很多锁,CAS的东西,这里我就不再追加了!

线程池的创建方式

使用Executors方法(不推荐)

我们的Executors工具类提供了四个创建线程的方法,每种方法对应一种线程池,我们可以根据业务需求选择我们要是用的线程池,但是阿里巴巴编码规范明确表明不推荐使用这种现成的线程池。

image-20200919212456946

我们以比较常用的定长线程池为例:

我们看到定长的线程池把最大长度和初始容量都定义为10,也就是定长,但是我们的等待队列LinkedBlockingQueue的长度就是Integer的最大值,也就是说如果我们的业务代码有问题,导致任务都卡在里面,其他的任务都在这里等,由于容量巨大,那么我们系统资源会迅速耗尽。

image-20200919221227277

其他几种线程池都有类似把范围扩展到Integer.MAX_VALUE的迷惑操作。

ThreaPoolExecutor定制线程池

实际上,到了公司上班,我们一般自己定制线程池而不是是用Executors的方法创建,为什么呢?

很多人一直对线程池产生恐惧是因为参数比较多,源码复杂。其实我们是基本原理还是很简单的,如果停留在应用层面,创建一个类总不能难死你吧。

image-20200919221929510

  • corePoolSize 核心线程池容量

  • maximumPoolSize 最大线程池容量

  • keepAliveTime 空闲线程存活时间

  • TimeUnit 空间线程存活时间单位

  • BolckingQueue<Runnable> 工作队列

  • threadFactory 线程工厂(可选)

  • RejectedExecutionHandler 拒绝策略(可选)

我们结合图来分析:

image-20200919222238197

  • corePoolSize 图中值为5,表示线程池可以有5个核心线程

  • BolckingQueue 工作队列,当我们第6个任务来了,而前5个任务还在执行,我们的新任务都去队列里等待

    如果队列也满了,则对线程池进行扩容(创建一个工作线程),最大容量为 maximumPoolSize 。

  • 如果 最大线程池容量也满了,根据你的 RejectedExecutionHandler 拒绝策略来选择如何处理这个新任务,报错还是什么事情都不干?

  • 我们知道核心线程数是5,假如我们线程池中有空闲的线程,并且线程数大于核心线程数,我们把闲置的线程给清除掉,这也是我们线程池的优化策略。当然我们的优化策略是可以调整的,我们的参数 keepAliveTime 就表示一个线程闲置多久我们把它清除,TimeUnit 是我们的计时单位,有以下各种时间单位.

  • threadfactory是我们创建线程的工厂,比如把线程加工为守护线程,等等,一般我们不去添加这个参数。

    image-20200919222948106

线程池的基本操作

线程池有很多操作,查看当前工作线程数,等待队列的一些情况等等,对于我们而言,最最最基本的操作就是执行一个任务,我们用线程池的方式代替new Thread(new Runnable{…})

public class ThreadPoolDemo1 {

    public static void main(String[] args) {
        Executor executor=Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            //new Thread(ThreadPoolDemo1::run).start();
            //代替Thread的方式创建线程
            executor.execute(ThreadPoolDemo1::run);
        }
    }

    public static void run(){
        System.out.println(Thread.currentThread().getName());
    }
}

定制线程池的基准

我们看到Executors自带的线程有非常严重的安全问题,那么我们应该如何定制线程池呢?

以一下结果参考因数:

  • 电脑配置
  • 并发级别
  • 任务执行时间
  1. 如果电脑配置比较好,又追求极致的性能,我们的初始容量可以大一点。请更多的工人,付更多的工资。

    电脑配置很差,核心线程数量调小一点,因为核心线程是内核线程,耗时相应增加。

  2. 一般而言,以电商为例。有点时候一秒的并发量达到百万甚至千万,但是每一个任务执行时间都非常的短暂。高峰期低峰期差异明显,如果我们把核心线程数加的很大,即便一万个线程也满足不了我们高峰期的爆发的任务量。所以我们应该把等待队列容量改的打一下,然后通过计算,选择合理的核心线程数,比如一天平均2亿订单,我们平均一秒就要处理2000多个订单,而我们的一个任务只需要0.2秒就执行完毕,那么我们可以设置500个核心线程,保证一天只能能处理完所有的订单

  3. 对于低并发,但是任务时长比较长的,举个例子,一个任务时10分,如果我们的核心线程过少,比如10个,那么我们执行100个任务平均下来算要花费100分钟,所以我们对于时间长的任务应该让他们尽量都被直接执行,相应的我们可以把等待队列缩小,缩小到0则没有等待队列,不够就加。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值