ThreadPoolExecutor探究

引言

 

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

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。那么到底线程池创建为什么需要使用
ThreadPoolExecutor方式 Executors创建又是怎么回事呢。

 

1.不使用线程池-Thread

1.1 线程的使用

 

 new Thread(
                new Runnable() {
                    public void run() {
                        System.out.println("start");
                    }
                }
        ).start();

如上就是Thread的使用方式了在Runnable中编写线程的运行代码,调用start即可完成线程开发的代码.

注意start方法只是说明线程已经准备启动,实际的启动需要CPU分配运行时间.

Runnable是一个接口,实现接口如果不执行start永远不可能线程被执行.

使用 new Thread 方式创线程的缺点:

a. 每次new Thread新建对象性能差。

b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。

c. 缺乏更多功能,如定时执行、定期执行、线程中断。

 

1.2 为什么要用线程池


1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。 

3.提供定时执行、定期执行、单线程、并发数控制等功能。

 

 

2.常见四大线程池

2.1 cachedThreadPool(可缓存的线程池)

 

 /**
         * 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,
         * 当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,
         * 线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
         */
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            Thread.sleep(index * 1000);
            cachedThreadPool.execute(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("执行:" + index + ",线程名称:" + threadName);
            });
        }

image.png

2.2 fixedThreadPool(固定大小的线程池)

 

/**
         *创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
         * 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
         */
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            fixedThreadPool.execute(() -> {
                try {
                    String threadName = Thread.currentThread().getName();
                    System.out.println("执行:" + index + ",线程名称:" + threadName);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

image.png

 

2.3 scheduledThreadPool(定时任务线程池)

 

 /**
         * 创建一个定长线程池,支持定时及周期性任务执行。延迟执行
         */
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        scheduledThreadPool.schedule(() -> System.out.println("表示延迟3秒执行。"), 3, TimeUnit.SECONDS);
        scheduledThreadPool.scheduleAtFixedRate(() -> System.out.println("表示延迟1秒后每3秒执行一次。"), 1, 3, TimeUnit.SECONDS);

 

image.png

2.4 singleThreadExecutor(单线程化的线程池

 

/**
         * 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
         */
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 1; i <= 5; i++) {
            final int index = i;
            singleThreadExecutor.execute(() -> {
                try {
                    String threadName = Thread.currentThread().getName();
                    Thread.sleep(2000);
                    System.out.println("执行:" + index + ",线程结束名称:" + threadName);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

image.png

 

3.手工创建线程池-ThreadPoolExecutor

3.1 ThreadPoolExecutor参数

image.png

corePoolSize - 线程池核心池的大小。
maximumPoolSize - 线程池的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 的时间单位。
workQueue - 用来储存等待执行任务的队列。
threadFactory - 线程工厂。
handler - 拒绝策略。

 

corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,
默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了
prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程
的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池
中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就
会把到达的任务放到缓存队列当中;

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线
程;

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于
corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的
线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不
超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于
corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大
影响,一般来说,这里的阻塞队列有以下几种选择:

java.lang.IllegalStateException: Queue full
方法 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)


ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。

 

 

3.2 线程池参数拒绝策略

 

RejectedExecutionHandler提供了四种方式来处理任务拒绝策略
1、直接丢弃(DiscardPolicy)
2、丢弃队列中最老的任务(DiscardOldestPolicy)。
3、抛异常(AbortPolicy)
4、将任务分给调用线程来执行(CallerRunsPolicy)。

这四种策略是独立无关的,是对任务拒绝处理的四中表现形式。最简单的方式就是直接丢弃任务。
但是却有两种方式,到底是该丢弃哪一个任务,比如可以丢弃当前将要加入队列的任务本身(DiscardPolicy)
或者丢弃任务队列中最旧任务(DiscardOldestPolicy)。丢弃最旧任务也不是简单的丢弃最旧的任务,而是有一些
额外的处理。除了丢弃任务还可以直接抛出一个异常(RejectedExecutionException),这是比较简单的方式。
抛出异常的方式(AbortPolicy)尽管实现方式比较简单,但是由于抛出一个RuntimeException,因此会中断调用
者的处理过程。除了抛出异常以外还可以不进入线程池执行,在这种方式(CallerRunsPolicy)中任务将有调用者
线程去执行。

3.3 线程池参数之间的对比

image.png

可以看到上述线程池之间的参数对比,其中threadFactory 参数默认都可以修改其余可以修改的参数只有FixedThreadPool的核心连接数和最大连接数,ScheduledThreadPool的核心连接数。

3.4 默认四大线程池的弊端

 

1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。因为使用的是
LinkedBlockingQueue(Integer.MAX_VALUE)作为缓冲队列,可以缓存int的最大值的线程。

2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

 

3.5 ThreadPoolExecutor使用示例

 

3.6 线程池的线程大小设置问题

 

本节来讨论一个比较重要的话题:如何合理配置线程池大小,仅供参考。
一般需要根据任务的类型来配置线程池大小:

a.如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
b.如果是IO密集型任务,参考值可以设置为2*NCPU

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,
再观察任务运行情况和系统负载、资源利用率来进行适当调整。
posted @ 2020-01-20 10:08  reload  阅读(207)  评论(0编辑  收藏  举报