zoukankan      html  css  js  c++  java
  • Java多线程学习总结之---线程池

    前言:

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

    1、线程池的基本概念

      线程池可以理解为一种管理线程的容器,是由我们根据自己的需求创建出来的。使用线程池可以降低系统资源开销、提高响应速度并帮我们管理线程。

    2、线程池的主要参数

      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()方法。

    3、线程池的工作原理

      线程池通过调用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) { // 1、如果当前线程数小于核心池,则创建线程并执行当前任务
                if (addWorker(command, true))
                    return;
                c = ctl.get();
            }
            if (isRunning(c) && workQueue.offer(command)) { // 2、如果条件1不满足则将任务放进任务队列
                int recheck = ctl.get();
                if (! isRunning(recheck) && remove(command)) // 如果线程池不处于运行状态,则拒绝
                    reject(command);
                else if (workerCountOf(recheck) == 0) // 3、如果线程数没超过最大池数则创建线程并执行任务
                    addWorker(null, false);
            }
            else if (!addWorker(command, false)) // 4、如果任务无法放入队列,则拒绝
                reject(command); // 拒绝
        }

    线程池工作原理图:

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

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

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

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

    4、常用线程池

        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

    5、合理线程池的参数

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

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

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

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

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

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

      7)、尽量使用有界队列,因为有界队列可以增加系统的稳定性和预警能力(无界队列可能会因为任务太多积压在队列里而撑满内存,导致系统瘫痪),可以根据需要将队列设大一点,比如几千。

    6、线程池的关闭     

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

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

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

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

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

  • 相关阅读:
    python批量安装模块 批量导出模块
    java 基础学习1
    linux 命令小记
    nosql数据库-mongodb
    python 列表大小快速比较方法
    nvidia-smi 系列命令,查看gpu ,显存信息
    pipinstaller
    pyinstaller 模块-python文件生成exe可执行文件
    git命令提交到github代码
    subprocess.Popen()
  • 原文地址:https://www.cnblogs.com/-Marksman/p/9291811.html
Copyright © 2011-2022 走看看