线程池的使用简介

线程池的使用:

 

public class ThreadTack extends Thread {

    public static void main(String[] args) {
        threadPoolExecutor();
    }


    /**
     * 1、JDK 自带线程池
     * 线程池类为 java.util.concurrent.ThreadPoolExecutor
     * <p>
     * corePoolSize:线程池维护线程的最少数量
     * <p>
     * maximumPoolSize:线程池维护线程的最大数量
     * <p>
     * keepAliveTime:线程池维护线程所允许的空闲时间
     * <p>
     * unit:线程池维护线程所允许的空闲时间的单位
     * <p>
     * workQueue:线程池所使用的缓冲队列
     * <p>
     * handler:线程池对拒绝任务的处理策略
     * <p>
     * <p>
     * 2、当一个任务通过execute(Runnable)方法欲添加到线程池时:
     * <p>
     * l  如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
     * <p>
     * l  如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
     * <p>
     * l  如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
     * <p>
     * l  如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
     * <p>
     * 也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
     * 一个线程池最多能同时处理的任务数量等于maximumPoolSize + workQueue之和
     * 一个线程池中最多能同时存在的线程数量等于maximumPoolSize
     * <p>
     * l  当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
     */
    public static void threadPoolExecutor() {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 24, 200, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(6)
                , new ThreadFactoryBuilder().setNameFormat("threadPoolExecutor-example-tools-%d").build());
        for (int i = 0; i < 24; i++) {
            final int num = i;
            pool.execute(() -> {
                System.out.println("正在执行任务:" + num);
                try {
                    Thread.sleep(300000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务" + num + "执行完毕");
            });
            System.out.println("线程池中线程数目:" + pool.getPoolSize() + ",队列中等待执行的任务数目:" +
                    pool.getQueue().size() + ",已执行完别的任务数目:" + pool.getCompletedTaskCount());
        }
    }

    /**
     * Spring自带线程池
     * 线程池类为 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
     * <p>
     * 一、ThreadPoolExecutor的重要参数
     * 1、corePoolSize:核心线程数
     * * 核心线程会一直存活,及时没有任务需要执行
     * * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
     * * 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
     * <p>
     * 2、queueCapacity:任务队列容量(阻塞队列)
     * * 当核心线程数达到最大时,新任务会放在队列中排队等待执行
     * <p>
     * 3、maxPoolSize:最大线程数
     * * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
     * * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
     * <p>
     * 4、 keepAliveTime:线程空闲时间
     * * 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
     * * 如果allowCoreThreadTimeout=true,则会直到线程数量=0
     * <p>
     * 5、allowCoreThreadTimeout:允许核心线程超时
     * 6、rejectedExecutionHandler:任务拒绝处理器
     * * 两种情况会拒绝处理任务:
     * - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
     * - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
     * * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
     * * ThreadPoolExecutor类有几个内部实现类来处理这类情况:
     * - AbortPolicy 丢弃任务,抛运行时异常
     * - CallerRunsPolicy 执行任务
     * - DiscardPolicy 忽视,什么都不会发生
     * - DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
     * * 实现RejectedExecutionHandler接口,可自定义处理器
     * <p>
     * 二、ThreadPoolExecutor执行顺序
     *  
     * <p>
     *   如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
     * <p>
     *   如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
     * <p>
     *   如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
     * <p>
     *   如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
     * <p>
     * 也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
     * <p>
     *   当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
     * <p>
     * 三、如何设置参数
     * 1、默认值
     * * corePoolSize=1
     * * queueCapacity=Integer.MAX_VALUE
     * * maxPoolSize=Integer.MAX_VALUE
     * * keepAliveTime=60s
     * * allowCoreThreadTimeout=false
     * * rejectedExecutionHandler=AbortPolicy()
     * <p>
     * 2、如何来设置
     * * 需要根据几个值来决定
     * - tasks :每秒的任务数,假设为500~1000
     * - taskcost:每个任务花费时间,假设为0.1s
     * - responsetime:系统允许容忍的最大响应时间,假设为1s
     * * 做几个计算
     * - corePoolSize = 每秒需要多少个线程处理?
     * * threadcount = tasks/(1/taskcost) =tasks*taskcout =  (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
     * * 根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
     * - queueCapacity = (coreSizePool/taskcost)*responsetime
     * * 计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
     * * 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
     * - maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
     * * 计算可得 maxPoolSize = (1000-80)/10 = 92
     * * (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
     * - rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
     * - keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
     * <p>
     */
    public static void threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
        //线程池所使用的缓冲队列
        poolTaskExecutor.setQueueCapacity(200);
        //线程池维护线程的最少数量
        poolTaskExecutor.setCorePoolSize(5);
        //线程池维护线程的最大数量
        poolTaskExecutor.setMaxPoolSize(1000);
        //线程池维护线程所允许的空闲时间
        poolTaskExecutor.setKeepAliveSeconds(30000);
        poolTaskExecutor.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("threadPoolTaskExecutor-example-tools-%d").build());
        poolTaskExecutor.initialize();

        for (int i = 0; i < 14; i++) {
            final int num = i;
            poolTaskExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("正在执行任务:" + num);
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("任务" + num + "执行完毕");
                }
            });
            ThreadPoolExecutor threadPoolExecutor = poolTaskExecutor.getThreadPoolExecutor();
            System.out.println("线程池中线程数目:" + threadPoolExecutor.getPoolSize() + ",队列中等待执行的任务数目:" +
                    threadPoolExecutor.getQueue().size() + ",已执行完别的任务数目:" + threadPoolExecutor.getCompletedTaskCount());
        }
    }


    /**
     * 1、JDK 自带线程池
     * 线程池类为 java.util.concurrent.Executors
     * <p>
     * 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
     * <p>
     * 如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
     * 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
     * 缺点:大家一般不用是因为newCachedThreadPool 可以无线的新建线程,容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值,但是出于后期维护原因,一般来说用 newFixedThreadPool 也就足够了。
     */
    static void newCachedThreadPool() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("newCachedThreadPool-example-tools-%d").build());
//        new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
        for (int i = 0; i < 10; i++) {

            final int index = i;

            try {

                Thread.sleep(index * 1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {

                    System.out.println(index);
                }
            });
        }
    }

    /**
     * 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
     * <p>
     * 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
     * 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
     * <p>
     * 因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
     * 其实newFixedThreadPool()在严格上说并不会复用线程,每运行一个Runnable都会通过ThreadFactory创建一个线程。
     */
    static void newFixedThreadPool() {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3, new ThreadFactoryBuilder().setNameFormat("newFixedThreadPool-example-tools-%d").build());
        new ThreadPoolExecutor(2, 3, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
        for (int i = 0; i < 10; i++) {
            final int index = i;

            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(index);

                        Thread.sleep(2000);

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    /**
     * 创建一个定长线程池,支持定时及周期性任务执行
     * <p>
     * ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。
     * 需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
     */
    static void newScheduledThreadPool() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5, new ThreadFactoryBuilder().setNameFormat("newScheduledThreadPool-example-tools-%d").build());
        for (int i = 0; i < 10; i++) {
            final int index = i;

            scheduledThreadPool.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("delay 3 seconds" + index);

                }
            }, 3, TimeUnit.SECONDS);
        }
    }

    /**
     * 表示延迟1秒后每3秒执行一次。
     * 与Timer 对比:
     * <p>
     * Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,
     * 前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
     * <p>
     * ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。
     * 需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
     * <p>
     * 通过对比可以发现ScheduledExecutorService比Timer更安全,功能更强大,在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer
     */
    static void newAtFixedRate() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5, new ThreadFactoryBuilder().setNameFormat("newAtFixedRate-example-tools-%d").build());
//        new ThreadPoolExecutor(1, 1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("delay 1 seconds, and excute every 3 seconds");
            }

        }, 1, 3, TimeUnit.SECONDS);
    }

    /**
     * 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当於单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
     * 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
     */
    static void newSingleThreadExecutor() {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("newSingleThreadExecutor-example-tools-%d").build());

        for (int i = 0; i < 10; i++) {
            final int index = i;

            singleThreadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(index);

                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }


    //写一个类执行十次随机时间休眠
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                int time = (int) (Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("当前线程是  " + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * new Thread的弊端如下:
     * <p>
     * a. 每次new Thread新建对象性能差。
     * b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
     * c. 缺乏更多功能,如定时执行、定期执行、线程中断。
     * 相比new Thread,Java提供的四种线程池的好处在于:
     * a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
     * b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
     * c. 提供定时执行、定期执行、单线程、并发数控制等功能。
     */
    static void newThread() {
        try {
            ThreadTack thread = new ThreadTack();
            thread.setName("new Thread");
            thread.start();
            for (int i = 0; i < 10; i++) {
                final int index = i;
                int time = (int) (Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("当前线程是  " + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


}

       项目中涉及到了线程池的使用,最初使用的是Executors的工厂模式向我们提供了4种线程池实现方式,但是阿里的编码规约并不推荐使用,原因是使用Executors创建线程池不会传入这个参数而使用默认值所以我们常常忽略这一参数,而且默认使用的参数会导致资源浪费,不可取。

阿里的 Java开发手册,上面有线程池的一个建议:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

ThreadPoolExecutor

先看看如何使用ThreadPoolExecutor创建线程池:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

下面来解释下各个参数:

  • int corePoolSize:该线程池中核心线程数最大值

核心线程:线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。

  • int maximumPoolSize: 该线程池中线程总数最大值

线程总数 = 核心线程数 + 非核心线程数。

  • long keepAliveTime:该线程池中非核心线程闲置超时时长

一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉,如果设置allowCoreThreadTimeOut = true,则会作用于核心线程。

  • TimeUnit unit:keepAliveTime的单位

TimeUnit是一个枚举类型,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天

  • BlockingQueue workQueue:该线程池中的任务队列:维护着等待执行的Runnable对象

当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。
常用的workQueue类型:

  • SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大

  • LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

  • ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

  • DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

  • ThreadFactory threadFactory:创建线程的方式,这是一个接口,你new他的时候需要实现他的Thread newThread(Runnable r)方法,一般用不上。

  • RejectedExecutionHandler handler:这玩意儿就是抛出异常专用的,比如上面提到的两个错误发生了,就会由这个handler抛出异常,根本用不上。

二、向ThreadPoolExecutor添加任务

我们怎么知道new一个ThreadPoolExecutor,大概知道各个参数是干嘛的,可是我new完了,怎么向线程池提交一个要执行的任务啊?

ThreadPoolExecutor.execute(Runnable command)

通过ThreadPoolExecutor.execute(Runnable command)方法即可向线程池内添加一个任务。

三、 明确拒绝策略

ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

这里给总结一下,当一个任务被添加进线程池时,执行策略:

  • 1.线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  • 2.线程数量达到了corePools,则将任务移入队列等待
  • 3.队列已满,新建线程(非核心线程)执行任务
  • 4.队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章