线程池及使用

为什么要用线程池,及优势

线程池的主要工作时控制运行线程的数量,处理过程中将任务放入队列中,然后在线程创建后启动这些任务
主要的特点是:线程复用、控制最大并发数、管理线程

降低资源消耗,通过线程复用降低线程创建和销毁造成的消耗

提高响应速度,让任务到达时,任务可以不需要等待线程创建就立即执行

提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统的资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

线程池各个参数的含义

随机找了个现有的线程池newFixedThreadPool,通过它进行跟踪,发现线程创建的核心是ThreadPoolExecutor

public static ExecutorService newFixedThreadPool(int nThreads) {
   return new ThreadPoolExecutor(nThreads, nThreads,
                                 0L, TimeUnit.MILLISECONDS,
                                 new LinkedBlockingQueue<Runnable>());
}	

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
        Executors.defaultThreadFactory(), defaultHandler);
}
  1. corePoolSize
    线程池中的常驻核心线程数
  2. maximumPoolSize
    线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  3. keepAliveTime
    表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
    但是如果调用了**allowCoreThreadTimeOut(boolean)**方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
    示例代码:
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5), new ThreadPoolExecutor.CallerRunsPolicy());
        System.out.println("核心线程空闲超时是否关闭:" + executor.allowsCoreThreadTimeOut());//核心线程空闲超时是否关闭:false
        executor.allowCoreThreadTimeOut(true);
  1. unit
    keepAliveTime的单位
  2. workQueue
    阻塞式的任务队列,被提交但尚未被执行的任务
  3. threadFactory
    表示生成线程池中工作线程的线程工厂,用于创建线程 ,一般默认的即可
  4. defaultHandler
    拒绝策略,当任务队列满了并且线程数量大于等于线程池的最大线程数时,无法继续为新任务服务,需要拒绝策略机制合理处理这个问题,jdk内置了4种拒绝策略(往下看)。

线程池的工作原理(重要)

在这里插入图片描述
在这里插入图片描述

 1. 在创建了线程池后,等待提交过来的任务请求。
 2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
 	2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
 	2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
 	2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,
 	那么还是要【创建非核心线程并且立刻运行这个任务】,你没看错,
 	是立即运行,插队
 	2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,
 	那么线程池会【启动饱和拒绝策略来执行】。
 3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
 4.当一个线程无事可做超过一-定的时间(keepAliveTime) 时,线程池会判断:
 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。.

线程池的使用

理解了线程池的参数及原理,那么就要想想怎么使用它了

现有线程池

JDK内置了6种线程池:

  1. newFixedThreadPool
  2. newCachedThreadPool
  3. newSingleThreadExecutor
  4. newSingleThreadScheduledExecutor
  5. newScheduledThreadPool
  6. newWorkStealingPool(java8新出的线程池)

虽然JDK提供了现成的线程池,但是实际开发中基本上没人使用,下面介绍一下这几个线程池,你也就明白了:

newFixedThreadPool

Executors.newFixedThreadPool(10);

public static ExecutorService newFixedThreadPool(int nThreads) {
   return new ThreadPoolExecutor(nThreads, nThreads,
                                 0L, TimeUnit.MILLISECONDS,
                                 new LinkedBlockingQueue<Runnable>());
}

public LinkedBlockingQueue() {
   this(Integer.MAX_VALUE);
}

newFixedThreadPool,Fixed中文为固定的,也就是说newFixedThreadPool中文意思是固定线程的线程池,通过上述知道了ThreadPoolExecutor各个参数的含义,再结合这个源码,发现它的corePoolSize与maxPoolSize,是相同的,也就是说,线程空余时间是无效的,没起作用的,这个线程池的线程永远不会被销毁。
而且使用的阻塞队列是LinkedBlockingQueue(),并且没有传参数,通过源码看到它的默认上限是Integer.Max_Value,21E个数据,队列没满是不会触发拒绝策略的,所以,如果不断的添加,内存就被撑爆了。

newCachedThreadPool

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

缓存线程池,核心线程数为0,最大线程数Integer.MAX_VALUE,队列SynchronousQueue,首先因为最大线程数是Integer.MAX_VALUE,也就是说,可以无限制的创建线程,来一个任务就处理一个任务,所以基本上不可能使用到队列,这个队列完全可以忽略了。SynchronousQueue队列的特点是每个插入操作都必须有对应的取出操作,没取出时无法继续放入,但那又能怎样呢,最大的线程数是Integer.MAX_VALUE。

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
   return new FinalizableDelegatedExecutorService
       (new ThreadPoolExecutor(1, 1,
                               0L, TimeUnit.MILLISECONDS,
                               new LinkedBlockingQueue<Runnable>()));
}

单例线程,可以看到核心线程与最大线程数都是1,,也就是说始终只有1个线程在执行任务,队列为LinkedBlockingQueue,可以无限制的往队列中添加任务

newScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
   super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

定时执行的线程池,队列是延时队列,元素需要实现 Delayed 接口。线程存活时间为0,只要一空闲就被回收。

newSingleThreadScheduledExecutor

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
 super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

看名见义,源码也是,就是最大只有1个活动线程,并且是定时执行任务。

newWorkStealingPool

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

一个具有抢占式操作的线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和上面5种都不一样,用的是 ForkJoinPool 类,而不是ThreadPoolExecutor,所以在这里不会对他叙述

简单介绍了一下现成的线程池,通过各个参数也了解了它的工作原理,适不适用需要结合业务进行判断。

自定义线程池及拒绝策略

上面说了那么多,现成的线程池,但是在生产上,不会使用这些,因为他们提供的都是无限制大的,所以实际开发中,如果面试时你说使用的是内置的线程池,那基本上你就可以跟这个工作说拜拜了。
使用线程池时,一定要自定义!一定要自定义!一定要自定义!
以及自定义拒绝策略。

内置的拒绝策略:

1. AbortPolicy(默认):直接抛出RejectedExecutionHandler异常,并阻止系统正常运行
2. CallerRunsPolicy:“调用者运行机制”,一种调节机制,该策略不会拒绝任务,也不会抛异常,而是将某些任务回退到调用者,由调用者执行
3. DiscardOldestPolicy:抛弃队列中的等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
4. DiscardPolicy:直接丢弃任务,不予任务处理也不抛异常。如果允许任务丢失,这是最后的一种方案。 

很不友好,所以实际开发中,一定要结合具体的业务,定制合适的拒绝策略。

关闭线程池

shutdown() 方法用来关闭线程池,拒绝新任务。执行shutdown()方法后,线程池状态变为SHUTDOWN状态,此时,不能再往线程池中添加新任务,否则会抛出RejectedExecutionException异常。此时,线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出,即在终止前允许执行以前提交的任务

还有一个类似的方法shutdownNow(),执行shutdownNow()方法后,线程池状态会立刻变成STOP状态 ,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,会返回那些未执行的任务。ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

如何定义合理的线程数

这个需要从根据你的业务考虑

IO密集型

  1. 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,避免CPU闲置,如CPU核心数*2
  2. IO密集型,即该任务需要大量的IO,即大量的阻塞,在单线程上进行IO密集型的任务会导致大量的CPU运行能力浪费在等待,所以IO密集型任务中使用多线程可以大大的提高程序运行速度,即使在单核CPU上,这种加速主要是利用了被浪费掉的阻塞时间。
    参考公式:CPU核心数/1-阻塞系数,阻塞系数在0.8-0.9之间,比如8和CPU,8/1-0.9=80个线程数。

CPU密集型

CPU密集的意思是业务需要大量的运算,而没有阻塞,CPU一直在全速运行,CPU密集任务只有再真正的多核CPU上才可能得到加速(通过多线程),CPU密集型业务配置尽可能少的的线程数量,减少线程的上下文切换,一般公式:CPU核心数+1个线程的线程池

线程池中为什么要使用阻塞队列

阻塞队列的原理是,将进入队列中的线程阻塞,如果使用非阻塞队列,它不会对当前线程进行阻塞,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。我们反过来再从另一个角度想一下,如果线程没有阻塞而加入队列,那跟不加入队列有什么不同呢,所以,如果不阻塞而加入队列的话完全没有意义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值