深刻理解java线程池

线程池

在介绍线程池以前先看看一道面试题:为何要使用线程池?使用线程池的优点是什么?java

做用:面试

​ 线程池作的工做主要是控制运行的线程的数量,处理过程当中将任务放入队列,而后再线程建立后启动这些任务,若是线程数量超过了最大的数量的线程排队等候,等其余线程执行完毕,再从队列中取出任务来执行。主要特定:线程复用,控制最大并发数,管理线程。在阿里巴巴java开发手册中,使用线程池的好处是减小在建立和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。若是不使用线程池,有可能形成系统建立大量同类线程而致使消耗完内存或者 “过分切换”的问题。缓存

优点:数据结构

  1. 下降资源消耗,经过重复利用已经建立的线程下降线程建立和销毁形成的消耗
  2. 提升响应速度,当任务到达时,任务能够不须要等到线程建立就能当即执行。
  3. 提升线程的可管理性。线程时稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行同一的分配,调优和监控。

java线程池概述:Java中的线程池是经过Executor框架实现的,该框架用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类,以下图所示: 多线程

线程池经常使用建立方式

Executors.newFixedThreadPool(int)并发

​ 建立一个固定长度的线程池,不能够扩容。能够控制线程的最大并发数量,超出的线程会在队列中等待。这种方式建立的线程池corePoolSize和maxinumPoolSize的值是相等的,它使用的是LinkedBlockingQueue。(注意: LinkedBlockingQueue默认大小是Integer.MAX_VALUE)框架

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                       //LinkedBlockingQueue默认大小是Integer.MAX_VALUE
                                      new LinkedBlockingQueue<Runnable>());
    }

Executors.newSingleThreadExecutor();this

​ 建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,保证全部任务按照指定顺序执行。这种方式建立建立的线程池将corePoolSize和maxinumPoolSize都设置为1,它使用的是LinkedBlockingQueue。线程

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    //LinkedBlockingQueue默认大小是Integer.MAX_VALUE
                                    new LinkedBlockingQueue<Runnable>()));
    }

Executors.newCachedThreadPool();code

​ 建立一个可缓存线程池,若是线程池长度超过处理须要,能够灵活回收空闲线程,若无可回收,则建立新线程这种方式建立的线程池将corePoolSize设置为0,将maxinumPoolSize设置为Integer.MAX_VALUE,使用的是SynchronousQueue,也就是说来了任务就建立线程运行,当线程空闲超过60秒就销毁线程。它底层使用的是SynchronousQueue

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

总结:上面三种不管哪一种方式建立线程池底层都是用了ThreadPoolExecutor,底层的数据结构都是阻塞队列。

线程池的简单使用,模拟10我的到5个窗口办理业务。

public class MyThreadPool {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(5);
        try {
            for (int i = 0; i < 10; i++) {
                pool.submit(()->{
                    System.out.println(Thread.currentThread().getName()+"\t 处理业务");
                });

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }

    }
}

线程池七大参数

线程池的建立源码

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
  1. corePoolSize:线程池中的常驻核心线程数。在建立线程池以后,当有请求任务来了以后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列中。
  2. maxinumPoolSize:线程池中可以容纳同时执行的最大线程数,此值必须大于等于1
  3. keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时候,当空闲时间达到keepAliveTime值时候,多余空闲线程会被销毁直到剩下corePoolSize数量个线程位置。默认状况下,只有当线程池中对额线程数大于corePoolSize时keepAliveTime才会起做用,直到线程池中的线程数量不大于corePoolSize。
  4. unit:keepAliveTime的单位
  5. workQueue:任务队列,被提交可是还没有被执行的任务
  6. threadFactory:表示生成线程池中工做线程线程工厂,用于建立线程通常用默认便可
  7. handler:拒绝策略,表示当前队列满了而且线程大于或者等于线程池的最大线程数时如何来拒绝请求执行的runnable的策略

线程池原理

  1. 在建立了线程池后,等待提交过来的任务请求

  2. 当调用execute()方法添加一个请求任务时候,线程池会作以下判断

    2.1 若是正在运行的线程数量<corePoolSize,立刻建立线程处理这个任务

    2.2 若是正在运行的线程数量>=corePoolSize,就将这个任务加入队列

    2.3 若是这个时候队列满了并且正在运行线程数<maxinumPoolsize,就建立非核心线程马上处理这个任务

    2.4 若是队列满了,并且正在运行线程数量>=maxinumPoolsize,那么线程就会启动饱和拒绝策略来执行

  3. 当一个线程完成任务时候,它会从队列中取出下一个任务执行

  4. 当一个线程空闲超过keepAliveTime是,线程池会判断若是当前运行的线程数量大于corePoolSize,那么这个线程会被销毁,因此线程池的全部任务完成以后它最终会收缩到corePoolSize的大小。

线程池的拒绝策略

所谓的拒绝策略就是线程池须要分配的线程数量大于线程池最大容量,没法分配新的线程处理当前任务时候的策略。JDK默认提供了如下四种拒绝策略

AbortPolicy(默认):直接抛出RejectedExecutionExeception异常阻止系统运行

CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者

DiscardOldestPolicy:抛弃队列中等待最久的任务,而后把当前任务加入队列中尝试再次提交当前任务

DiscardPolicy:直接多余丢弃任务,不予任何处理也不会抛出异常。若是容许任务丢失,这是最好的解决方案。

这四种拒绝策略均实现了RejectedExecutionHandler接口

代码验证四种拒绝策略

因为使用Executors建立的线程池workQueue队列默认的长度是Integer.MAX_VALUE,这个数字太大,即便使用默认的拒绝策略也通常不会抛出相应的异常,反而可能形成OOM,因此阿里巴巴java技术手册要求不能使用Executors的方式建立线程池,而是应该使用ThreadPoolExecutor的方式建立线程。

AbortPolicy策略

在自定义的线程池中maximumPoolSize=5,workQueue=3,因此该线程池可以接受的最大请求数为8,而请求数倒是10,使用AbortPolicy拒绝策略超过8个请求就会抛出异常

public class MyThreadPool {
    public static void main(String[] args) {

        ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,                                       
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
				//建立自定义线程池,使用AbortPolicy拒绝策略,请求线程超过8就会抛出异常                               new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 0; i < 10; i++) {
                int temp = i;
                pool.submit(()->{
                    System.out.println(Thread.currentThread().getName()+"\t 处理业务" + temp);
                });

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }

    }
}
                                                      
pool-1-thread-1	 处理业务0
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@b4c966a rejected from java.util.concurrent.ThreadPoolExecutor@2f4d3709[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0]
pool-1-thread-2	 处理业务1
pool-1-thread-1	 处理业务3
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
pool-1-thread-2	 处理业务4
pool-1-thread-2	 处理业务5
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
pool-1-thread-3	 处理业务2
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
pool-1-thread-4	 处理业务6
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
pool-1-thread-5	 处理业务7
	at com.ThreadPool.MyThreadPool.main(MyThreadPool.java:26)

CallerRunsPolicy策略

“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者。在本例中超出的两个请求会被交还给main线程处理

//将上述代码建立线程池的拒绝策略改变,其余不变 
ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());


pool-1-thread-1	 处理业务0
main	 处理业务8    //请求数为10,超过的两个请求交给调用者main线程处理
main	 处理业务9
pool-1-thread-2	 处理业务1
pool-1-thread-2	 处理业务3
pool-1-thread-2	 处理业务4
pool-1-thread-2	 处理业务5
pool-1-thread-4	 处理业务6
pool-1-thread-3	 处理业务2
pool-1-thread-5	 处理业务7

DiscardOldestPolicy

抛弃队列中等待最久的任务,而后把当前任务加入队列中尝试再次提交当前任务。

ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());


pool-1-thread-1	 处理业务0
pool-1-thread-1	 处理业务5
pool-1-thread-1	 处理业务8
pool-1-thread-1	 处理业务9
pool-1-thread-2	 处理业务1
pool-1-thread-3	 处理业务2
pool-1-thread-4	 处理业务6
pool-1-thread-5	 处理业务7

执行结果中3和4请求被丢弃,由此可知该队列中3和4请求时等待时间最久的

DiscardPolicy

直接多余丢弃任务,不予任何处理也不会抛出异常。

ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());

pool-1-thread-2	 处理业务1
pool-1-thread-2	 处理业务3
pool-1-thread-2	 处理业务4
pool-1-thread-2	 处理业务5
pool-1-thread-1	 处理业务0
pool-1-thread-3	 处理业务2
pool-1-thread-4	 处理业务6
pool-1-thread-5	 处理业务7

从执行结果来看请求8和请求9会被抛弃

合理配置线程池参数

如何合理配置线程池参数?

首先要获取硬件参数,经过Runtime.getRuntime().availableProcessors()获取CPU的核心数目

而后要区分所执行的任务是CPU密集型仍是IO密集型。
CPU密集型:该任务须要大量的运算,而没有阻塞,CPU一直全速运行。 CPU密集任务只有在真正的多核CPU上才可能获得加速(经过多线程)。而在单核CPU上不管开几个模拟的多线程,该任务都不可能获得加速,由于CPU总的运算能力就是那么多。CPU密集型任务配置尽量少的线程数量:通常公式:CPU核数+1线程的线程池

IO密集型:执行该任务须要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会致使浪费大量的CPU运算能力在等待上。因此在IO密集型任务中使用多线程能够大大的加速程序的运行,即便在单核CPU上,这种加速就是利用了被浪费掉的阻塞时间。IO密集型大部分线程被阻塞,故须要多配置线程数。通常有如下两种配置策略

  1. 因为IO密集型任务线程并非一直在执行任务,则应该配置尽量多的线程,如CPU核数*2

  2. 参考公式:CPU核数 / (1 - 阻塞系数)【阻塞系数在0.8~0.9之间】

    好比8核CPU:线程池应配置8/(1-0.9)= 80个线程数。

注意事项

阿里巴巴java开发手册v1.2中对线程池中有如下两点注意事项

【强制】线程资源必须经过线程池提供,不容许在应用中自行显式建立线程。

说明:使用线程池的好处是减小在建立和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。若是不使用线程池,有可能形成系统建立大量同类线程而致使消耗完内存或者 “过分切换”的问题。

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

说明:Executors 返回的线程池对象的弊端以下:

  1. FixedThreadPool 和 SingleThreadPool: 容许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而致使 OOM。

  2. CachedThreadPool 和 ScheduledThreadPool: 容许的建立线程数量为 Integer.MAX_VALUE,可能会建立大量的线程,从而致使 OOM。

相关文章
相关标签/搜索