• 线程池


    参考链接:https://www.toutiao.com/i6621053271307977229/?group_id=6621053271307977229

    java中为了提高并发度,可以使用多线程共同执行,但是如果有大量线程短时间之内被创建和销毁,会占用大量的系统时间,影响系统效率。为了解决这个问题,java中引入了线程池,可以使创建好的线程在指定的时间内由系统统一管理,而不是在执行时创建,执行后就销毁,从而避免了频繁创建、销毁线程带来的系统开销。

    一、线程池的使用原理

    当系统接受一个提交的任务时,并不会着急去创建一个新的线程去执行这个任务,而是去线程池中查询是否有空闲的线程。

    • 若有:直接使用这个线程。
    • 若没有:根据配置的策略执行(有可能时创建一个新的线程,也有可能是阻塞该任务等待空闲线程)。
    • 待任务结束之后,也不会销毁线程,而是放入线程池的空闲队列,等待下次使用。

    就以ThreadPoolExecutor为例,当我们把一个Runnable交给线程池去执行的时候,这个线程池处理的流程是这样的:

    • 先判断线程池中的核心线程们是否空闲,如果空闲,就把这个新的任务指派给某一个空闲线程去执行。如果没有空闲,并且当前线程池中的核心线程数还小于 corePoolSize,那就再创建一个核心线程。
    • 如果线程池的线程数已经达到核心线程数,并且这些线程都繁忙,就把这个新来的任务放到等待队列中去。如果等待队列又满了,那么查看一下当前线程数是否到达maximumPoolSize,如果还未到达,就继续创建线程。
    • 如果已经到达了,就交给RejectedExecutionHandler(拒绝策略)来决定怎么处理这个任务。

    二、线程池的使用

     在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,java.uitl.concurrent.ThreadPoolExecutor是线程池中最核心的一个类,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器:

    public class ThreadPoolExecutor extends AbstractExecutorService {
        .....
    
        public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
    
                       BlockingQueue<Runnable> workQueue );
    
        public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
    
                       BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory );
    
        public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
    
                       BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler );
    
        public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
    
                       BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler );
    
        ...
    }

    ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

    1. 线程池各参数说明

    • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
    • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
    • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
      • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
      • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
      • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
      • PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
    • ThreadFactory:用于设置创建线程的工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常有帮助。
    • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略:
      • AbortPolicy:直接抛出异常。
      • CallerRunsPolicy:只用调用者所在线程来运行任务。
      • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
      • DiscardPolicy:不处理,丢弃掉。

      当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

    • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
    • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

    2. 线程池的注意事项

    虽然线程池能大大提高服务器的并发性能,但使用它也会存在一定风险。与所有多线程应用程序一样,用线程池构建的应用程序容易产生各种并发问题,如对共享资源的竞争和死锁。此外,如果线程池本身的实现不健壮,或者没有合理地使用线程池,还容易导致与线程池有关的死锁、系统资源不足和线程泄漏等问题。

    (1) 建议使用new ThreadPoolExecutor(...)的方式创建线程池

    线程池的创建不应使用 Executors 去创建,而应该通过 ThreadPoolExecutor 创建,这样可以让读者更加明确地知道线程池的参数设置、运行规则,规避资源耗尽的风险,这一点在也阿里巴巴JAVA开发手册中也有明确要求。

    (2) 合理设置线程数

    线程池的工作线程数设置应根据实际情况配置,CPU密集型业务(搜索、排序等)CPU空闲时间较少,线程数不能设置太多。

    如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1

    如果是IO密集型任务,参考值可以设置为2*NCPU

    (3) 设置能代表具体业务的线程名称

    这样方便通过日志的线程名称识别所属业务。具体实现可以通过指定ThreadPoolExecutor的ThreadFactory参数。如使Spring提供的CustomizableThreadFactory。

    3. keepAliveTime的含义

    引用一句话:当线程空闲时间达到keepAliveTime,该线程会退出。

    有两个疑问:(1) 线程为什么会空闲  (2) 线程为什么要退出

    举例:核心线程数10,最大线程数30,keepAliveTime是3秒

    随着任务数量不断上升,线程池会不断的创建线程,直到到达核心线程数10,就不创建线程了,这时多余的任务通过加入阻塞队列来运行,当超出阻塞队列长度+核心线程数时,这时不得不扩大线程个数来满足当前任务的运行,这时就需要创建新的线程了(最大线程数起作用),上限是最大线程数30那么超出核心线程数10并小于最大线程数30的可能新创建的这20个线程相当于是“借”的,如果这20个线程空闲时间超过keepAliveTime,就会被退出。

    既然上面这么说,那10个核心线程会不会退出呢?这就由下面的参数决定:

    allowCoreThreadTimeout:是否允许核心线程空闲退出,默认值为false

    当keepAliveTime设置为0时到底是空闲线程直接退出还是不退出?答案是直接不等待退出。所以设置为0 并不是个很好的做法(除非场景中任务数量极少能超出核心线程数),如果任务数频繁超出核心线程数,这个值需要评估设定为合理值尽量避免线程开启关闭的动作。

    4. 创建线程池示例

    不建议使用Executors去创建线程池,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
    (1) newFixedThreadPool和newSingleThreadExecutor :主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
    (2) newCachedThreadPool和newScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

    这里介绍三种创建线程池的方式:

    第一种:

    ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

    第二种:

    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
    
    //Common Thread Pool
    ExecutorService pool = new ThreadPoolExecutor(5, 200,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    pool.execute(()-> System.out.println(Thread.currentThread().getName()));
    pool.shutdown();//gracefully shutdown

    第三种:

    <bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
            <property name="corePoolSize" value="10" />
            <property name="maxPoolSize" value="100" />
            <property name="queueCapacity" value="2000" />
            <property name="threadFactory" value= threadFactory />
            <property name="rejectedExecutionHandler">
                <ref local="rejectedExecutionHandler" />
            </property>
        </bean>
     //in code
     userThreadPool.execute(thread);
    public class ThreadPoolHelper {
    
        private static final Logger logger = Logger.getLogger(ThreadPoolHelper.class);
    
        private static final int POOL_SIZE = 40;//线程池大小
    
        //订单任务线程池
    
        private static ThreadPoolExecutor comitTaskPool =(ThreadPoolExecutor) new ScheduledThreadPoolExecutor(POOL_SIZE,
                new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
    
    
        /**
         * 执行订单任务
         *
         * @param comitTask
         */
        public static void executeTask(Runnable comitTask) {
            comitTaskPool.execute(comitTask);
            logger.debug("【线程池任务】线程池中线程数:" + comitTaskPool.getPoolSize());
            logger.debug("【线程池任务】队列中等待执行的任务数:" + comitTaskPool.getQueue().size());
            logger.debug("【线程池任务】已执行完任务数:" + comitTaskPool.getCompletedTaskCount());
        }
    
    
        /**
         * 关闭线程池
         */
        public static void shutdown() {
            logger.debug("shutdown comitTaskPool...");
            comitTaskPool.shutdown();
            try {
                if (!comitTaskPool.isTerminated()) {
                    logger.debug("直接关闭失败[" + comitTaskPool.toString() + "]");
                    comitTaskPool.awaitTermination(3, TimeUnit.SECONDS);
                    if (comitTaskPool.isTerminated()) {
                        logger.debug("成功关闭[" + comitTaskPool.toString() + "]");
                    } else {
                        logger.debug("[" + comitTaskPool.toString() + "]关闭失败,执行shutdownNow...");
                        if (comitTaskPool.shutdownNow().size() > 0) {
                            logger.debug("[" + comitTaskPool.toString() + "]没有关闭成功");
                        } else {
                            logger.debug("shutdownNow执行完毕,成功关闭[" + comitTaskPool.toString() + "]");
                        }
                    }
                } else {
                    logger.debug("成功关闭[" + comitTaskPool.toString() + "]");
                }
            } catch (InterruptedException e) {
                logger.warn("接收到中断请" + comitTaskPool.toString() + "停止操作");
            }
        }
    }

    了解一下:Java通过Executors提供四种线程池,分别为:

    • newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,表示同一时刻只能有这么大的并发数
    • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • 相关阅读:
    【实战】PHP如何使用 ElasticSearch 做搜索
    基于PHP使用influxdb搭建监控服务系统
    influxdb 2.*版本与1.*版本区别
    rabbitmq的数据持久化
    基于纯真本地数据库的 IP 地址查询 PHP 源码
    【面试系列】主键索引和唯一索引谁更快?
    如何设计微博点赞功能数据库?
    降低composer版本(亲测可行)
    Compiler vs Interpreter
    Adobe AE问题排查 After Effects
  • 原文地址:https://www.cnblogs.com/myitnews/p/12038404.html
Copyright © 2020-2023  润新知