zoukankan      html  css  js  c++  java
  • 多线程--线程池的正确打开方式

    概述

    线程可认为是操作系统可调度的最小的程序执行序列,一般作为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。JVM线程跟内核轻量级进程有一对一的映射关系,所以JVM中的线程是很宝贵的。

    一般在工程上多线程的实现是基于线程池的。因为相比自己创建线程,多线程具有以下优点

    • 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
    • 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

    Executors存在什么问题

    看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

    Executors为什么存在缺陷

    1. 线程池工作原理

    当一个任务通过execute(Runnable)方法欲添加到线程池时:

    • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
    • 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
    • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
    • 那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
    • 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

    2.newFixedThreadPool分析

    Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueueLinkedBlockingQueue

    ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。

    LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE

    这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE

    newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

    3. newCachedThreadPool分析

    结合上述流程图,核心线程数=0,最大线程无限大,由于SynchronousQueue是一个缓存值为1的阻塞队列。当有大量任务请求时,线程池会创建大量线程,造成OOM。

    线程池参数详解

    1. 构造方法

    /**
     * @param corePoolSize  核心线程数
     * @param maximumPoolSize 最大线程数
     * @param keepAliveTime 线程所允许的空闲时间
     * @param unit 线程所允许的空闲时间的单位
     * @param workQueue  线程池所使用的缓冲队列
     * @param handler 线程池对拒绝任务的处理策略
     */
    ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  RejectedExecutionHandler handler)
    
    

    2. 线程池拒绝策略

    RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。。以下是JDK1.5提供的四种策略。

    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

    线程池正确打开方式

    1. 创建线程池

    避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

            ThreadPoolExecutor executorService = new ThreadPoolExecutor(8,
                    16,
                    60,
                    TimeUnit.SECONDS,
                    new LinkedBlockingDeque<>(10));
    

    2. 向线程池提交任务

    我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。

            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));
    
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程池无返回结果");
                }
            });
    

    我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));
    
            Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    return "ok";
                }
            });
            System.out.println("线程池返回结果:" + future.get());
    

    3. 关闭线程池

    shutdown关闭线程池

    方法定义:public void shutdown()

    (1)线程池的状态变成SHUTDOWN状态,此时不能再往线程池中添加新的任务,否则会抛出RejectedExecutionException异常。

    (2)线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

    注意这个函数不会等待提交的任务执行完成,要想等待全部任务完成,可以调用:

    public boolean awaitTermination(longtimeout, TimeUnit unit)

    shutdownNow关闭线程池并中断任务

    方法定义:public List shutdownNow()

    (1)线程池的状态立刻变成STOP状态,此时不能再往线程池中添加新的任务。

    (2)终止等待执行的线程,并返回它们的列表;

    (3)试图停止所有正在执行的线程,试图终止的方法是调用Thread.interrupt(),但是大家知道,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

    4. 如何配置线程池大小

    CPU密集型任务

    该任务需要大量的运算,并且没有阻塞,CPU一直全速运行,CPU密集任务只有在真正的多核CPU上才可能通过多线程加速 CPU密集型任务配置尽可能少的线程数量:

    CPU核数+1个线程的线程池

    例如: CPU 16核,内存32G。线程数=16

    IO密集型任务

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

    某大厂设置策略:IO密集型时,大部分线程都阻塞,故需要多配置线程数:

    CPU核数/(1-阻塞系数)

    例如: CPU 16核, 阻塞系数 0.9 ------------->16/(1-0.9) = 160 个线程数。

    此时非阻塞线程=16

    写在最后

    这篇文章是对线程池使用的简单分析,为了更好的学习编程,后续会从源码的角度分析线程池的运行原理,上述文章如有错漏,还望大家不吝赐教。

    公众号 【当我遇上你】

  • 相关阅读:
    产生唯一的临时文件mkstemp()
    Linux文档时间戳查看和修改——stat
    Linux下快速查找文件
    Crypt加密函数简介(C语言)
    产生随机数 random
    见微知著——从《新闻联播》挖掘价值资讯擒拿年度政策受益牛股
    Linux中link,unlink,close,fclose详解
    不用输液
    javaScript document对象详解
    javascript初步了解
  • 原文地址:https://www.cnblogs.com/idea360/p/12365546.html
Copyright © 2011-2022 走看看