Java多线程学习总结之---线程池

2021年11月24日 阅读数:7
这篇文章主要向大家介绍Java多线程学习总结之---线程池,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

前言:html

   本文基于jdk1.8。 前段时间换工做,面试时候每次都会问线程的问题,本身对多线程方面的知识没有花时间研究过,因此一问到线程就懵了,最近特意买了方腾飞老师的《Java并发编程的艺术》这本书学学这方面的知识。这篇随笔主要是我对线程池学习的总结,若有写的很差或不对的地方欢迎指出!面试

一、线程池的基本概念数据库

  线程池能够理解为一种管理线程的容器,是由咱们根据本身的需求建立出来的。使用线程池能够下降系统资源开销、提升响应速度并帮咱们管理线程。编程

二、线程池的主要参数数组

  int  corePoolSize:核心池大小,线程池正常保持存活的线程数,默认状况下,当咱们建立一个线程池,它不会马上建立线程,而是等到有任务提交时才会建立,固然咱们也能够调用线程池的prestartAllCoreThreads()方法,让线程池在建立时就建立corePoolSize数目的线程;缓存

  int  maximuxPoolSize:最大线程池大小,线程池所容许建立的最大线程数;服务器

  long  keepAliveTime:线程存活时间,当线程池中的线程数量大于核心池大小后,多出来的线程在空闲时间达到keepAliveTime后会被中断,若是任务比较多,而且每一个任务执行时间短,那能够调大这个参数,以提升线程的利用率;多线程

  TimeUnit  timeUnit:keepAliveTime的单位,值有:DAYS、HOURS、MINUTES、SECONDS、MILLISECONDS(毫秒)、MICROSECONDS(微秒)、NANOSECONDS(纳秒);并发

  BlockingQueue  workQueue:任务队列,主要实现类有:异步

    1)、LinkedBlockingQueue:基于链表的无界(最大值为Integer.MAX_VALUE)阻塞队列,按FIFO(先进先出)的规则对任务进行排序,使用了此队列的线程池中maximuxPoolSize和keepAliveTime这两个参数就没有意义了(缘由下文解释);

    2)、ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO的规则对任务进行排序,可传入参数来自定义队列大小;

    3)、DelayedWorkQueue:基于堆的延迟队列,静态工厂Executors.newScheduledThreadPool(...)中使用了该队列;

    4)、PriorityBlockingQueue:具备优先级的阻塞队列;

    5)、SynchronousQueue:不存储任务的阻塞队列,每个插入对应一个取出。

    吞吐量:SynchronousQueue > LinkedBlockingQueue > ArrayBlockingQueue 

  ThreadFactory  threadFactory:线程工厂,用来建立线程,能够经过线程工厂给新建立的线程设置更合理的名字、设置优先级等;

  RejectedExecutionHandler  handler:拒绝任务的接口处理器;

    拒绝策略有:a、AbortPolicy:拒绝任务并抛出异常,默认的策略;

          b、DiscardPolicy:直接拒毫不抛出异常;

          c、DiscardOldestPolicy:丢弃队列中最远的一个任务(最早进入队列的,FIFO),并执行当前任务;

          d、CallerRunsPolicy:只用调用者所在的线程来执行任务,无论其余线程的事。

          e、固然也能够自定义拒绝策略,来处理如记录日志、持久化等已有拒绝策略不能实现的功能,需实现RejectedExecutionHandler接口,重写rejectedExecution()方法。

三、线程池的工做原理

  线程池经过调用execute()方法工做,固然也能够调用submit()方法,主要区别是submit()方法能够返回任务执行的结果future对象,而execute()没有返回值,execute()方法的源码以下:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) { // 一、若是当前线程数小于核心池,则建立线程并执行当前任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) { // 二、若是条件1不知足则将任务放进任务队列
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command)) // 若是线程池不处于运行状态,则拒绝
                reject(command);
            else if (workerCountOf(recheck) == 0) // 三、若是线程数没超过最大池数则建立线程并执行任务
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) // 四、若是任务没法放入队列,则拒绝
            reject(command); // 拒绝
    }

线程池工做原理图:

以上源码中一、2 、3 注释就对应线程池工做原理图中的一、二、3步的判断。

  这里注意,当线程池中的线程数量小于核心池量,而且这时线程池中还有空闲线程(以前执行任务的线程已经完成工做了),若是这时候有任务提交仍是会建立新线程,由于execute()方法中只要当前线程池中线程数量小于核心池就调用addWorker()建立线程执行当前任务,这个彷佛有一点不合理,不知 Doug Lea大神之后会不会改进。

  下面举个小例子来和线程池工做原理比较一下:有一个小工厂,最多能容纳20(maximumPoolSize)个工人(线程)干活,目前老板只招了10(corePoolSize)个工人,老板规定无论有没有活都要来上班,活很少时候能够一 部分人干 一部分人歇着,反正都是老员工老板养的起(核心池中一部分线程空闲,但不会被中断),工厂还有个小仓库(任务队列BlockingQueue),有时候活多了干不完,原料(任务)就堆到仓库里,仓库要是堆满了,老板就想办法了,因为老板比较抠门,就招了5个零时工(大于corePoolSize那部分),这批活作的差很少了,老板不想多养几个闲人就辞掉3个零时工(空闲线程达到设定的存活时间,中断),这时又来了一批活,量很大,因而老板又招了8个零时工,这时工厂的工位满了(线程数达到 maximumPoolSize),如今再有活来老板就拒绝了(RejectedExecutionHandler)。

  在介绍线程池参数时有说过若是任务队列是LinkedBlockingQueue,线程池大小和存活时间这两个参数就失效了,这里若是工厂的仓库是无限容量的,老板就不用担忧活干不完啦,干不完的活直接扔进仓库就行了,而且老板还能够根据客户要求的期限对任务进行排序,这样就不用再招零时工,天然也没有辞退空闲零时工的事了。

四、经常使用线程池

    1)、FixedThreadPool   

    固定大小的线程池,它的核心池数和最大线程池数都是传入的参数的值,存活时间为0,即无任务时当即中断,任务队列是 LinkedBlockingQueue。

    优势:可控制并发数量,多出新近的任务会在队列中等待,任务能够无限多。

    缺点:线程池大小固定,随着业务量的变化,改起来不方便,但能够写在配置文件里。

    适用场景:必定数量的任务,执行所需时间长,为了知足管理资源的需求,而须要限制当前线程数量的应用场景,它适用于负载较轻的服务器。

源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
使用示例:
public static void fixedThreadPoolTest(){
    ExecutorService fixdThreadPool = Executors.newFixedThreadPool(3);
    for (int i = 0;i < 10; i++){
        int index = i;
        fixdThreadPool.execute(() -> System.out.println(Thread.currentThread().getName()+":"+index));
    }
}
运行结果:
pool-1-thread-2:1
pool-1-thread-3:2
pool-1-thread-1:0
pool-1-thread-3:4
pool-1-thread-2:3
pool-1-thread-3:6
pool-1-thread-1:5
pool-1-thread-3:8
pool-1-thread-2:7
pool-1-thread-1:9

  2)、CachedTheadPool

    可缓存的线程池,核心池为0,最大线程池数为Integer.MAX_VALUE,空闲线程的存活时间60秒,任务队列是SynchronousQueue。

    优势:可根据须要灵活建立线程数量,空闲60秒就中断,节约系统资源。

    缺点:若使用场景不当,如任务不多,偶尔(60秒以上)来一个任务,那就每次都须要建立线程,这样就很消耗系统资源。

    适用场景:适用于执行大量短时间异步任务或者负载较轻的服务器。

源码:
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
使用示例:
public static void cachedThreadPoolTest(){
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0;i < 10; i++){
        int index = i;
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName()+":"+index));
    }
}

运行结果:
pool-1-thread-1:0
pool-1-thread-1:1
pool-1-thread-1:2
pool-1-thread-1:3
pool-1-thread-1:4
pool-1-thread-1:5
pool-1-thread-1:6
pool-1-thread-1:7
pool-1-thread-1:8
pool-1-thread-1:9

  3)、SingleThreadExecutor

    只有一个线程的线程池,核心池和最大线程池大小都是1,空闲线程存活时间是无心义的参数,任务队列是LinkedBlockingQueue。

    优势:线程池中有且只有一个线程一直存在着,任务按顺序执行,后来的任务在队列里排队等待。

    缺点:不适合并发场景。

    适用场景:任务须要按顺序而且无并发的执行。

源码:
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
                      0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
使用示例:
public static void singleThreadExecutorTest(){
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    for (int i = 0;i < 10; i++){
        int index = i;
        singleThreadExecutor.execute(() -> System.out.println(Thread.currentThread().getName() + ":"+index));
    }
}
运行结果:
pool-1-thread-1:0
pool-1-thread-1:1
pool-1-thread-1:2
pool-1-thread-1:3
pool-1-thread-1:4
pool-1-thread-1:5
pool-1-thread-1:6
pool-1-thread-1:7
pool-1-thread-1:8
pool-1-thread-1:9

  4)、ScheduledThreadPool

    可执行定时或周期性任务的线程池,核心池为传入的参数值,最大线程池为Integer.MAX_VALUE,空闲线程存活时间为0,任务队列为DelayedWorkQueue。

    优势:可执行定时和周期性任务,书上说比Timer效果好,有时间测一下。

    缺点:暂时没想到。

    适用场景:有定时、周期性批量任务需求时,如银行批量代收付交易、处理对帐、批量放款等。

源码:
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
使用示例:
public static void scheduledThreadPoolTest(){
   ScheduledExecutorService scheduledExecutorPool = Executors.newScheduledThreadPool(3);
     scheduledExecutorPool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName()+
       ":delay 1 seconds,and execute every 3 seconds"),1,3,TimeUnit.SECONDS);
}
运行结果: pool
-1-thread-1:delay 1 seconds,and execute every 1 seconds pool-1-thread-1:delay 1 seconds,and execute every 1 seconds pool-1-thread-2:delay 1 seconds,and execute every 1 seconds pool-1-thread-2:delay 1 seconds,and execute every 1 seconds pool-1-thread-2:delay 1 seconds,and execute every 1 seconds pool-1-thread-2:delay 1 seconds,and execute every 1 seconds pool-1-thread-3:delay 1 seconds,and execute every 1 seconds pool-1-thread-3:delay 1 seconds,and execute every 1 seconds pool-1-thread-3:delay 1 seconds,and execute every 1 seconds

  5)、自定义线程池ThreadPoolExecutor

    若是上述四种由Executors工厂类提供的经常使用的线程池知足不了你的业务需求,你能够自定义ThreadPoolExecutor,每一个参数均可以按照你的须要设置。

源码:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), handler); } 使用示例: public static void threadPoolExecutorTest(){ int corePoolSize = 3, maximumPoolSize = 5; long keepAliveTime = 1; TimeUnit unit = TimeUnit.SECONDS; BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue(1); RejectedExecutionHandler rejectedExecutionHandler = (Runnable r, ThreadPoolExecutor executor) -> System.out.println("其实我是拒绝的"); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,keepAliveTime,unit,workQueue,rejectedExecutionHandler); for (int i = 0; i < 10; i++){ int index = i; threadPoolExecutor.execute(() -> System.out.println(Thread.currentThread().getName() + ":"+index)); } } 运行结果: pool-1-thread-2:1 pool-1-thread-4:4 pool-1-thread-3:2 其实我是拒绝的 pool-1-thread-1:0 pool-1-thread-1:7 pool-1-thread-5:5 pool-1-thread-2:3 其实我是拒绝的 pool-1-thread-4:8

五、合理线程池的参数

  1)、CPU密集型任务(如压缩和解压缩,这种须要CPU不停的计算的任务)应配置尽量小的线程,如配置CPU个数+1 数量的线程池;

  2)、IO密集型任务,线程并非一直在执行任务,则应配置尽量多的线程,如2倍的CPU数;

  3)、混合性任务,若是能够拆分,将器拆分红一个CPU密集性任务和一个IO密集型任务,只要这两个任务执行时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。若是两个任务执行时间相差很大就不必进行拆分了;

  4)、优先级不一样的任务可使用优先级队列PriorityBlockingQueue来处理,它可让优先级高的任务先执行;

  5)、执行时间不一样的任务能够交给不一样规模的线程池来处理,或者能够适用优先级队列,让执行时间段的任务先执行;

  6)、是否依赖其余系统资源,如依赖数据库链接池的任务,由于线程提交SQL后须要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置的越大,这样才能更好的适用CPU;

  7)、尽可能使用有界队列,由于有界队列能够增长系统的稳定性和预警能力(无界队列可能会由于任务太多积压在队列里而撑满内存,致使系统瘫痪),能够根据须要将队列设大一点,好比几千。

六、线程池的关闭     

  1)、shutdown() 将线程池的状态置为SHUTDOWN,线程池会将空闲的线程调用它的interrupt()进行中断,还在排队的任务取消,而后等待正在执行任务的线程执行完成后销毁全部线程;

  2)、shutdownNow() 将线程池的状态置为STOP, 而后遍历线程池中全部的线程,并逐个调用它们的interrupt()方法进行中断正在执行任务或者暂停的线程,并返回还在排队的任务列表。

 

参考资料:一、《Java并发编程的艺术》

        二、https://www.cnblogs.com/dolphin0520/p/3932921.html

     三、https://juejin.im/post/5b3cf259e51d45194e0b7204