5种线程池的简单介绍以及为什么要使用自定义线程池

我们知道创建线程对象,就会在内存中开辟空间,而线程中的任务执行完毕之后,就会销毁。

单个线程的话还好,如果线程的并发数量上来之后,就会频繁的创建和销毁对象。这样,势必会消耗大量的系统资源,进而影响执行效率。

所以,线程池就应运而生。

生产环境使用的场景:有个功能,运算数据量大(最大可一起运算5w条数据,由于业务原因,每条数据需要与数据库交互好几次,还涉及不同的库),属于后台管理类的功能,我们最理想的方式就是使用异步处理的方式,即请求发送到后台后,开启一个线程去处理,不用立即给前端相应处理结果,提示正在处理即可,如果操作量大的话,这时我们就可以使用线程池了

实际开发中,我们都是使用自定义线程池的(阿里巴巴开发手册也是这么建议的),至于为什么,后面揭晓 

线程池的参数介绍

参数 说明
corePoolSize 核心线程
maximumPoolSize 最大线程数
keepAliveTime 存活时间
timeUnit 时间单位
workQueue 任务队列
threadFactory 线程工厂
rejectedExecutionHandler 拒绝策略

corePoolSize:的作用是代表要开启核心线程数量,核心线程会一直保留。

maximumPoolSize:的作用是最大可以创建的线程数量,当你的所有核心线程都在工作状态时,此时如果有新的任务需要执行,系统就会创建新的线程来执行任务(在队列已满的前提条件下)。

keepAliveTime:代表新开启的线程如果执行完毕后可以存活多长时间,如果在设置的时间内没有任务使用该线程,则线程资源就会归还操作系统。

timeUnit: 代表线程存活的时间单位。

workQueue: 任务队列,如果正在执行的任务超过了核心线程数,可以存放在队列中,当线程池中有空闲资源就可以从队列中取出任务继续执行。

任务队列类型(阻塞队例)有如下几种LinkedBlockingQueue 、DelayedWorkQueue、ArrayBlockingQueue、 SynchronousQueue、 TransferQueue,使用不同的队列就会产生不同类型的线程池。(任务队列的知识,我会单独的去写一篇文章

threadFactory: 线程工厂,他的作用是用来产生线程的,可以自定义线程的类型,比如我们可以定义线程组名称,在jstack问题排查时,非常有帮助。

rejectedExecutionHandler: 拒绝策略, 当所有线程都在忙,并且任务队列处于满任务的状态,则会执行拒绝策略。

拒绝策略可以自定义,JDK默认给我们提供了4种,分别是:

5种线程池的简单介绍以及为什么要使用自定义线程池_第1张图片

AbortPolicy:直接拒绝,并抛出异常,这也是默认的策略。
CallerRunsPolicy:直接让调用execute方法的线程去执行此任务。
DiscardOldestPolicy:丢弃最老的未处理的任务,然后重新尝试执行当前的新任务。
DiscardPolicy:直接丢弃当前任务,但是不抛异常。

总结一下线程池的执行过程。

  1. 当线程数量未达到corePoolSize的时候,就会创建新的线程来执行任务。
  2. 当核心线程数已满,就会把任务放到阻塞队列。
  3. 当队列已满,并且未达到最大线程数,就会新建非核心线程来执行任务(重要)
  4. 当队列已满,并且达到了最大线程数,则选择一种拒绝策略来执行。

我们再来看下jdk 给我们提供了哪些线程池:

1.Executors.newSingleThreadExecutor

 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
 单个线程线程池,只有一个线程的线程池,阻塞队列使用的是LinkedBlockingQueue,若有多余的任务提交到线程池中,则会被暂存到阻塞队列,待空闲时再去执行。按照先入先出的顺序执行任务

5种线程池的简单介绍以及为什么要使用自定义线程池_第2张图片

5种线程池的简单介绍以及为什么要使用自定义线程池_第3张图片

可以看到阻塞队例 使用的是LinkedBolckingQueue,且默认大小为Integer.MAX_VALUE,这样的话,如果有大量请求到来,会放入到这个任务队列里,可能会导致OOM;

private static void newSingleThreadExecutor() {
        //创建一个单线程化的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                public void run() {
                    try {
                        //结果依次输出,相当于顺序执行各个任务
                        System.out.println(Thread.currentThread().getName()+"正在被执行,打印的值是:"+index);
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

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

总结:单线程化的线程池在并发量高的情况下会因为阻塞队列为LinkedBlockingQueue 的原因,可能会导致OOM.

2.Executors.newFixedThreadPool

定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
固定大小的线程池,可以指定线程池的大小,该线程池corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue,大小为整数最大值。
该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。
同时使用无界的LinkedBlockingQueue来存放执行的任务。当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题
而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown

5种线程池的简单介绍以及为什么要使用自定义线程池_第4张图片

 private static void newFixedThreadPoolTest() {
        System.out.println(Runtime.getRuntime().availableProcessors());
        // 创建一个可重用固定个数的线程池
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            newFixedThreadPool.execute(new Runnable() {
                public void run() {
                    // 打印正在执行的缓存线程信息
                    System.out.println(Thread.currentThread().getName() + "正在被执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

6
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-1正在被执行
pool-1-thread-3正在被执行
pool-1-thread-3正在被执行

总结:核心线程数和最大线程数一样,但是因为阻塞队列使用的LinkedBlockingQueue,在并发高的场景下,一样可能导致OOM,或者资源耗尽。

3.Executors.newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行
定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。
  scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。
  schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

private static void newScheduledThreadPoolTest() {
        //创建一个定长线程池,支持定时及周期性任务执行——延迟执行
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        //创建一个单列线程池,支持定时及周期性任务执行——延迟执行  dubbo延迟暴露服务的原理
        ScheduledExecutorService scheduledThreadPool2 = Executors.newSingleThreadScheduledExecutor(
            new NamedThreadFactory("dubbo", true));
        //延迟1秒执行
        scheduledThreadPool.schedule(new Runnable() {
            public void run() {
                System.out.println("延迟1秒执行");
            }
        }, 1, TimeUnit.SECONDS);

        //延迟1秒后每3秒执行一次
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName() + "延迟1秒后每3秒执行一次");
            }
        }, 1, 3, TimeUnit.SECONDS);

        scheduledThreadPool2.schedule(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName() +"延迟1秒执行");
            }
        }, 1, TimeUnit.SECONDS);

    }

延迟1秒执行
pool-1-thread-2延迟1秒后每3秒执行一次
dubbo-thread-1延迟1秒执行
pool-1-thread-1延迟1秒后每3秒执行一次
pool-1-thread-2延迟1秒后每3秒执行一次
pool-1-thread-3延迟1秒后每3秒执行一次
pool-1-thread-3延迟1秒后每3秒执行一次

定时线程池核心线程数可指定,但是最大线程数为Integer.MAX_VALUE,显而易见也会导致OOM(资源耗尽)

4.Executors.newCachedThreadPool()

可缓存线程池,先查看线程池中有没有以前建立的线程,如果有就直接使用,如果没有新建一个线程加入线程池中,可缓存线程池
通常用于执行一些生存期很短的异步型任务;线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程

缓存的线程默认存活60秒。线程的核心池corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,阻塞队列使用的是SynchronousQueue。
是一个直接提交的阻塞队列,他总会迫使线程池增加新的线程去执行新的任务。
在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,当提交新任务时,
如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。
如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源

5种线程池的简单介绍以及为什么要使用自定义线程池_第5张图片

 private static void NewCachedThreadPoolDemo() {
        // 创建一个可缓存线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            try {
                // sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    // 打印正在执行的缓存线程信息
                    System.out.println(Thread.currentThread().getName()
                        + "正在被执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行

缓存线程池没有核心线程,新的请求来时先查看线程池中有没有以前建立的线程,如果有就直接使用,如果没有新建一个线程加入线程池中,最大线程数为Integer.MAX_VALUE,显而易见也会导致OOM(资源耗尽)。


可以看出四种线程池都是通过调用如下构造函数来返回一个线程池,而且因为最大线程数以及阻塞队里的原因,在并发搞得场景下都可能会导致OOM

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

阿里巴巴开发手册也是建议不要使用jdk提供的四中线程池,而是使用自定义的

5.Executors.newWorkStealingPool()

jdk 1.8 引入的

可以传入线程的数量,不传入,则默认使用当前计算机中可用的cpu数量

能够合理的使用CPU进行对任务操作(并行操作)

适合使用在很耗时的任务中

底层用的ForkJoinPool 来实现的。 ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”分发到不同的cpu核心上执行,执行完后再把结果收集到一起返回。

public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),//获取当前电脑核数
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
private static void newWorkStealingPool() throws InterruptedException {
        //格式化
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //AtomicInteger用来计数
         AtomicInteger number = new AtomicInteger();

        ExecutorService executorService = Executors.newWorkStealingPool();

        for (int i = 0; i < 12; i++) {
            executorService.execute(() -> {
                System.out.println("第" + number.incrementAndGet() + "周期线程运行当前时间【" + sdf.format(new Date()) + "】");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        System.out.println("主线程运行当前时间【" + sdf.format(new Date()) + "】");
        TimeUnit.SECONDS.sleep(3);
    }

这里没有设置线程数,所以会根据你设备的cpu核心数来创建线程,我的是6核cpu,所以会每隔1秒输出6个数字。 

主线程运行当前时间【2021-03-30 10:17:24】
第5周期线程运行当前时间【2021-03-30 10:17:24】
第3周期线程运行当前时间【2021-03-30 10:17:24】
第2周期线程运行当前时间【2021-03-30 10:17:24】
第1周期线程运行当前时间【2021-03-30 10:17:24】
第4周期线程运行当前时间【2021-03-30 10:17:24】
第6周期线程运行当前时间【2021-03-30 10:17:24】
第7周期线程运行当前时间【2021-03-30 10:17:25】
第8周期线程运行当前时间【2021-03-30 10:17:25】
第9周期线程运行当前时间【2021-03-30 10:17:25】
第10周期线程运行当前时间【2021-03-30 10:17:25】
第11周期线程运行当前时间【2021-03-30 10:17:25】
第12周期线程运行当前时间【2021-03-30 10:17:25】

6.自定义线程池

这个ThreadFactory是干嘛的呢:我们一般用来设定自定义线程池中线程的名字(前缀)

如果我们不指定,线程池会使用默认的,但是这样不便于日志分析,所以生产环境中我们都会设定

5种线程池的简单介绍以及为什么要使用自定义线程池_第6张图片

5种线程池的简单介绍以及为什么要使用自定义线程池_第7张图片

 private static void personalExecutor(){
        // 创建数组型缓冲等待队列
        BlockingQueue bq = new ArrayBlockingQueue(10);
        // ThreadPoolExecutor:创建自定义线程池,池中保存的线程数为3,允许最大的线程数为6
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(3, 6, 50, TimeUnit.MILLISECONDS, bq);

        // 创建6个任务
        Runnable t1 = new TempThread();
        Runnable t2 = new TempThread();
        Runnable t3 = new TempThread();
        Runnable t4 = new TempThread();
        Runnable t5 = new TempThread();
        Runnable t6 = new TempThread();

        // 6个任务在分别在3个线程上执行
        tpe.execute(t1);
        tpe.execute(t2);
        tpe.execute(t3);
        tpe.execute(t4);
        tpe.execute(t5);
        tpe.execute(t6);

        // 关闭自定义线程池
        tpe.shutdown();
    }

public static  class TempThread implements Runnable {

        @Override
        public void run() {
            // 打印正在执行的缓存线程信息
            System.out.println(Thread.currentThread().getName() + "正在被执行");
            try {
                // sleep一秒保证3个任务在分别在3个线程上执行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-2正在被执行
pool-1-thread-1正在被执行
pool-1-thread-3正在被执行

看一个实际生产环境的自定义线程池(细节代码经过处理):

//最大线程数 可配置,如没有配置使用默认值16
int maxThread = XXX;
//核心线程数,可根据服务器计算出最优的
int corePoolSize = 5;
if (corePoolSize > maxThread) {
      corePoolSize = maxThread;
 }
//阻塞队里大小,可配置,如没有配置使用默认值32
int maxBlockingSize = XXX;
ArrayBlockingQueue queue = new ArrayBlockingQueue(maxBlockingSize);
// 创建自定义线程池
ThreadPoolExecutor  executor = new ThreadPoolExecutor(corePoolSize, maxThread, 10, TimeUnit.MINUTES,
            queue, new NamedThreadFactory("XXXThread"));

我们改下我们上面的代码,加入new NamedThreadFactory("CJThread")看看效果;

NamedThreadFactory 是dubbo工程里的一个实现了jdk ThreadFactory接口的类,(当然ThreadFactory也有其他第三方的各种实现)

5种线程池的简单介绍以及为什么要使用自定义线程池_第8张图片

CJTest-thread-1正在被执行
CJTest-thread-3正在被执行
CJTest-thread-2正在被执行
CJTest-thread-1正在被执行
CJTest-thread-3正在被执行
CJTest-thread-2正在被执行

至于线程池的原理以及阻塞队列后面单独出文章分析

线程池原理分析——线程是如何做到复用的

浅析ArrayBlockingQueue 和 LinkedBlockingQueue的阻塞原理

你可能感兴趣的:(并发&JUC,java,多线程)