欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

线程池入门

程序员文章站 2022-11-22 09:27:12
什么是池,我们在开发中经常会听到有线程池啊,数据库连接池等等。那么到底什么是池?其实很简单,装水的池子就叫水池嘛,用来装线程的池子就叫线程池(废话),就是我们把创建好的N个线程都放在一个池子里面,如果有需要,我们就去取,不用额外的再去手动创建了为什么要用线程池按照正常的想法是,我们需要一个线程,就去... ......

什么是池,我们在开发中经常会听到有线程池啊,数据库连接池等等。
那么到底什么是池?
其实很简单,装水的池子就叫水池嘛,用来装线程的池子就叫线程池(废话),就是我们把创建好的n个线程都放在一个池子里面,如果有需要,我们就去取,不用额外的再去手动创建了

为什么要用线程池

按照正常的想法是,我们需要一个线程,就去创建一个线程,这样的想法是没错的,但是如果需要有n多个线程呢?那把创建线程的代码复制n多份?或者用for循环来创建?no,这样不是不行,但是不好。
因为线程也是有生命周期的,创建与销毁线程都会对系统资源有很大的开销,创建线程需要向系统申请相应的资源,销毁线程又会对垃圾回收器造成压力

使用线程池的好处

  1. 加快响应速度,需要线程我们就去取,不用额外的创建,可以反复利用。
  2. 合理的利用cpu与内存资源,因为cpu与内存都不是无限的
  3. 可以把线程进行一个统一的管理

线程池的适用场景

在实际开发中,如果需要5个以上的线程,那么就应该使用线程池来完成工作

线程池的创建与停止

我们先来说线程池的创建,我们都知道,在java中的对象都是有构造方法的,有些可以使用无参的构造方法来创建,有些就需要使用有参的构造方法来创建,线程池的创建就必须要往构造方法中传入参数,我们就先来了解一下线程池中的构造参数都是一些什么含义吧,否则你怎么知道你创建的线程池是一个什么样的运行规则呢

  1. corepoolsize:(int)

    核心线程数
    --------------
    在线程池创建后默认是没有线程的,当有新的任务来的之后,线程池就会创建一个新的线程去执行这个任务
    假设我们把这个参数设置为5,然后有5个任务提交了过来,就会创建5个线程去执行对应的任务
    在任务执行完成之后,这5个线程并不会被收回,而是会一直保留在线程池中,等待下一个任务的到来
    ------------------
    注意:当线程池中的线程数量少于我们设定的值时,不管之前创建的线程是否空闲,有新任务来时都会创建一个新的线程去执行
  2. workqueue:(blockingqueue)

    任务存储队列
    -----------------
    任务队列有很多种:介绍常见的3种
    1.synchronousqueue:这种队列内部是没有容量的,任务过来后会直接转交给线程去执行
    2.linkedblockingqueue:*队列,没有容量限制,如果内存足够,可以无限扩展,如果线程处理任务的速度跟不上任务提交过来的速度,很容易造成内存浪费和内存溢出的现象
    3.arraybolockingqueue:有界队列,可以设置队列的大小
    -----------------
    在我们核心线程数都被占满并且都不空闲的时候,再有新的任务过来时,就会把新的任务存储在任务队列里面
  3. maxpoolsize:(int)

    最大线程数
    ----------------
    线程池中最大的线程数量,什么意思呢,我们用一个案例来解释它的作用
    假设,核心线程数为5,任务队列可以存储的任务数量是100,最大线程数我们设置为10
    当我们核心线程都不空闲时,而任务队列又被堆满了,也就是我们一共提交了105个任务过来,并且一个都没有执行完
    这个时候如果再有新的任务过来,那最大线程数就派上用场了。
    这个时候,我们会额外的再去创建新的线程来执行新的任务,那额外的线程可以有多少呢
    就是最大线程数-核心线程数之后得到的数量啦
    如果线程池中的线程数量达到了最大线程数,再来新的任务,就会被拒绝
    -------------------
    注意:如果创建了额外的线程,那么会先从队列中取出位于队列头位置中的任务去执行,而不是新加进来的任务
    额外创建的线程在任务执行完之后是会被销毁的,并不会一直存在,这点跟核心线程数不同
  4. keepalivetime:(long)

    存活时间
    ---------------------
    这个存活时间就是说的额外线程执行完任务,空闲的时间超过了keepalivetime之后,就会被回收了
  5. threadfactory:(threadfactory)

    创建线程的工厂
    
  6. handler:(rejectedexecutionhandler)

    拒绝策略
    

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

建议手动创建,可以更加明确线程池的运行规则,避免资源耗尽的情况
我们看看自动创建会带来哪些问题
自动创建其实就是使用jdk已经创建好并提供给我们的一些线程池

newfixedthreadpool (中文意思:固定的线程池)

public class main {

    public static void main(string[] args) {

        
        //创建一个newfixedthreadpool线程池,并设置它的线程数量为4
        executorservice executorservice = executors.newfixedthreadpool(4);
        
        //提交一千个任务去给它执行,结果就是它会不断打印线程1-线程4的名字
        for(int i = 0 ; i < 1000 ; i ++){
            executorservice.execute(new test());
        }
    }
}

//打印当前线程的名字
class test implements runnable{

    @override
    public void run() {
        try {
            thread.sleep(500);
        } catch (interruptedexception e) {
            e.printstacktrace();
        }
        system.out.println(thread.currentthread().getname());
    }
}

前面我们说了,线程池的构造函数有好几个,为什么这里只要传一个就行了,并且前面还说了,有核心线程数和最大线程数,满足一定条件下会创建出额外的线程,那为什么只会打印线程1到线程 4的名字呢,我们去看看这个线程池的源码吧

public static executorservice newfixedthreadpool(int nthreads) {
        return new threadpoolexecutor(  nthreads,     //核心线程数
                                        nthreads,     //最大线程数
                                        0l,         //存活时间 
                                        timeunit.milliseconds, //时间单位,这里是毫秒
                                        //任务队列,这里使用的是*队列
                                        new linkedblockingqueue<runnable>()
                                );
    }

可以看出,内部new了一个threadpoolexecutor,里面的参数我也给打上注释了,核心线程数与最大线程数都是我们传进来的4这个参数,所以,无论有多少个任务进来,最多也就会有4个线程在进行工作,同时我们也看到了这里使用的*队列,*队列的特点就是没有容量限制,只要内存足够,可以无限扩展,所以不管有多少个任务进来,都会被存储到任务队列里面去。
仔细思考一下,使用这种线程池会有什么问题呢,当任务过多,线程处理不过来,就会不断的堆积到任务队列里面,这就造成了内存浪费,当队列向系统申请不到更多的内存时,还有新的任务提交过来,就会造成内存溢出。
可能会说,不是还有handler拒绝策略嘛,那我们再翻到前面看看,拒绝的条件是什么,当核心线程数不够用了,最大线程数也不够用了,并且队列已经满了的时候,新的任务才会被拒绝,这里是使用的*队列,就不存在队列满了这么一个情况

newsinglethreadexecutor(单独的线程池)

public class main {

    public static void main(string[] args) {

        executorservice executorservice = executors.newsinglethreadexecutor();
        for(int i = 0 ; i < 1000 ; i ++){
            executorservice.execute(new test());
        }
    }
}

class test implements runnable{

    @override
    public void run() {
        try {
            thread.sleep(500);
        } catch (interruptedexception e) {
            e.printstacktrace();
        }
        system.out.println(thread.currentthread().getname());
    }
}

这个线程池不用传参数,从字面意思上就看看出,这个线程池里面只有一个线程,我们看看它的源码

public static executorservice newsinglethreadexecutor() {
        return new finalizabledelegatedexecutorservice
            (new threadpoolexecutor(1, 1,
                                    0l, timeunit.milliseconds,
                                    new linkedblockingqueue<runnable>()));
    }

可以看出,这个线程池与newfixedthreadpool线程池很类似,只不过一个需要我们来指定核心线程数与最大线程数,一个不需要指定,已经写死了,就是一个,那么原理也就跟newfixedthreadpool线程池是一样的了。

newcechedthreadpool(缓存的线程池)

public static void main(string[] args) {

        executorservice executorservice = executors.newcachedthreadpool();
        for(int i = 0 ; i < 1000 ; i ++){
            executorservice.execute(new test());
        }
    }

我们直接来看看它的源码吧

public static executorservice newcachedthreadpool() {
        return new threadpoolexecutor(0, //核心线程数
                                      //最大线程数
                                      integer.max_value,
                                      //存活时间 
                                      60l, timeunit.seconds,
                                      //直接交换队列
                                      new synchronousqueue<runnable>());
    }

从源码我们可以看出这种线程池的特性,首先,核心线程数是0,也就是说,这个线程池中没有核心线程数,也就是没有可以一直存活的线程,最大线程数为integer类型的最大值,可以说是没有上限的,你来多少任务我都接着,然后是存活时间,被设置为60秒,如果一个线程空闲的时间超过了60秒,就会被回收,然后是任务队列,这个队列前面介绍过了,里面没有容量,不会存放任务进去,每次一有任务就会立马交给线程去处理。
这个线程池存在什么问题,它会反复的创建与销毁线程,只要一有新的任务进来,就会立马创建一个线程去执行这个任务,如果执行完后没有新的任务交给它,就等待被销毁(等死)。
其次,也是有可能造成内存溢出的错误的

newscheduledthreadpool(支持定时或周期性任务执行的线程池)

这个线程有两种用法
我们先看第一种

public static void main(string[] args) {

    //传入一个int类型的参数,这个参数代表线程池中核心线程的数量
    scheduledexecutorservice executorservice = executors.newscheduledthreadpool(10);
    
    //调用schedule方法,第一个参数是我们要执行的任务,第二个参数时间,第三个时间单位
    //就是说,线程池在间隔5秒之后去执行我们提交的任务
    executorservice.schedule(new test(), 5, timeunit.seconds);

}

第二种

public class main {

    public static void main(string[] args) {

        scheduledexecutorservice executorservice = executors.newscheduledthreadpool(10);
        
        //间隔一秒后开始执行任务,然后每隔3秒再次执行
        executorservice.scheduleatfixedrate(new test(), 1, 3, timeunit.seconds);
    }
}

线程池中的线程数量设定为多少比较合适

根据自己的业务场景不同有不同的规则

cpu计算密集型的任务(计算、加密、hash等),最佳的线程数应该为cpu核心的1-2倍
耗时io型任务(读写数据库、网络读写等),最佳的线程数可以为cpu核心数的很多倍,10、100甚至更多倍都可以的

计算公式:最佳线程数 = cpu核心数 * (1+平均等待时间/平均工作时间)

如何停止线程池

shutdown:优雅的停止线程

executorservice.shutdown();
当我们调用了线程池的这个方法后,线程池就知道了我们需要它停止下来,同时,它并不会再去接收新的任务了
然后线程池就会把当前正在执行的任务和任务队列中的任务都执行完后,进行停止

isshutdown

这个方法返回一个boolean值,就是当我们对线程池调用了shutdown之后,我们想知道它到底有没有接收到
就可以调用这个方法,如果接收到了,会返回一个true,否则就是false

isterminated

这个方法返回一个boolean值,这个方法用于检测,整个线程池是否已经停止工作了

awaittermination

executorservice.awaittermination(3l,timeunit.seconds);
这个方法与isterminated不同,这个方法是说,在我等待的这个时间内,线程池是否已经结束工作了
如果结束返回ture,否则false

shutdownnow:暴力停止线程

调用这个方法后,不管线程池中的任务是否还在执行,也不管任务队列中是否还有未执行的任务
线程池都会立刻停止工作,并且会把任务队列中未执行的任务进行返回

list<runnable> runnablelist = executorservice.shutdownnow();

任务太多,怎么拒绝

拒绝的时机

  1. 当我们调用了shutdown方法后,如果还有新的线程进来,会直接抛出异常拒绝掉这个任务
  2. 当核心线程数与最大线程数都被占满,并且任务队列也满了的时候,再有新的任务来也会被拒绝,这里说的任务队列是有界队列,容量有限的

拒绝的策略

  1. abortpolicy:直接、暴力拒绝,就是简单粗暴的抛出异常,还有新的任务?我不接收

  2. discardpolicy:默默的丢弃,它不会去执行新的任务,然后也不告诉你它不执行,直接把任务丢掉,也不抛出异常

  3. discardoldestpolicy:以旧换新,将任务队列中存在最久没有被执行的任务丢弃到,把新的任务添加进来

  4. callerrunspolicy:让提交这个任务的线程来执行这个任务
    比方说,主线程向线程池中提交新的任务,但是线程池已经无法再接受新的任务了,它就会对主线程说,这个任务你来完成吧,然后主线程没办法,只好自己去执行这个任务

最后一种拒绝策略是最好的,因为前面三种策略都是有损失的,要么新任务不执行,要么抛弃旧的任务,而最后一种没有损失,同时,这个线程老是给线程池去提交任务,那如果这个任务交由提交的线程去执行,那么这个线程就没有功夫去提交新的任务了,因为它已经被它提交的任务所占据了,只有等它的任务执行完之后,才会继续提交,这同时也降低了任务的提交速度

executor家族的辨析

我们在前面创建线程池的代码中看到,一下是executorservice,一下又是executors,那么它们之间的关系到底是什么呢,线程池不应该是threadpoolexecutor吗?怎么又是executorservice呢,别急,我们下面会详细介绍

我们从底层往上层讲

  1. executor是最底层的,它是一个接口,它里面只有一个方法:execute(runnable command)

    public interface executor {
        void execute(runnable command);
    }
  2. executorservice也是一个接口,并继承了executor接口,同时还扩展了一些其它的方法

    public interface executorservice extends executor {
        void shutdown();
    list<runnable> shutdownnow();
    boolean isshutdown();
    boolean isterminated();
    boolean awaittermination(long timeout, timeunit unit)
    throws interruptedexception;
    //这些方法我们在前面都已经介绍过了,这个接口声明了一些对线程池进行管理的方法
    }
  3. abstractexecutorservice是一个抽象类,它实现了executorservice,但它并没有实现executorservice接口里的方法,因为它自己本身也是一个抽象类,所以可以不用去写实现,它里面只写了一些自己的实例方法

    public abstract class abstractexecutorservice implements executorservice 
    
  4. threadpoolexecutor:它才是最终的线程池类,它继承了abstractexecutorservice,并实现了所有父类的方法

    public class threadpoolexecutor extends abstractexecutorservice
    

好,上面的我们已经介绍清楚了,那么executors又是什么呢,executors其实是一个工具类,里面有很多的方法,包括创建前面我们使用的那几种线程池

public static executorservice newfixedthreadpool(int nthreads) {
        return new threadpoolexecutor(nthreads, nthreads,
                                      0l, timeunit.milliseconds,
                                      new linkedblockingqueue<runnable>());
    }
    
    这是我们前面所使用到的一种固定线程数的线程池,方法返回的是executorservice,
    但是实际里面返回的是executorservice的子类:threadpoolexecutor

使用线程池的注意事项

  1. 避免任务堆积
  2. 避免线程数过多
  3. 需要排查线程数,是否与预期一致,因为有时候线程不会被正常回收,有可能是我们的任务逻辑有问题,导致任务一直无法完成,线程一直无法停止工作等情况