java并发编程 线程池理解及使用(三)

8 篇文章 0 订阅

线程池定义

         上一章我们了解了线程的定义及使用,java使用的是内核级线程,线程的创建,销毁及上下文切换是非常消耗内核资源,如果是一个简单的dome,那么我们使用线程只需要使用 new Thread创建线程使用没有问题,但是如果是生产环境,要保证我们的程序能够长期稳定运行就需要线程池,线程池其实就可以理解为多个线程的组合管理,设计理念和我们数据库的连接池一样的

        我们通过一个场景来了解:一个web应用,服务器会接收请求并处理响应到客户端,要保证各个请求之间独立访问,我们就应该给每个请求开辟一个线程来进行处理。如果大量的请求进来,势必会有大量的线程创建,因为每一次响应时间都不会太长,当处理完成我们就应该销毁当前线程,这一次次创建与销毁,不光性能不好,而且很有可能造成内存溢出(OOM)等等异常。所以我们需要线程池来实现复用及管理

线程池优势

        刚才我们也了解了为什么要去创建线程池,那么他创建的原因其实也就是他的优势:

1. 降低系统资源的消耗,通过对线程池中线程的重复使用,降低创建与销毁线程的消耗

2. 提高效率,当任务到达,不用等创建好了线程才运行,可以立即执行

3. 提高线程的统一管理,线程是稀缺资源,不能无限制创建,否则影响性能及稳定性,线程池能方便线程统一管理及优化

什么时候使用线程池?

        1. 大量的任务处理

        2.处理任务时间较短  这两种情况下使用线程池是最合适的

线程池使用

        java中使用线程池是比较简单的,基本可以说是new就可以使用,但是因为线程池创建的便捷也隐藏了他的使用隐患,与一些复杂的原理

关于线程的使用,网上有说四种的,也有说五种的,众说纷纭,那么我们来看下,为啥有四种或者五种是怎么来的,首先看一张图

我们常用的创建线程池是 Executor 及 ThreadPoolExecutor两个类,当然除了这两个,也还有其他的线程池如:ScheduledThreadPoolExecutor定时任务线程池等等,自行了解,先搞清楚了前面两个,首先是ThreadPoolExecutor,因为Executor源码创建时也是在new ThreadPoolExecutor(除了定时线程池),所以我们先搞懂它

ThreadPoolExecutor

        使用只需要new ThreadPoolExecutor,1.8源码中有四个构造方法(其他版本没去翻源码,不过应该是一样的),我们之间了解最多的那个就好了

我们一个个的参数去了解:

        corePoolSize : 核心线程数, 线程池长期维持的线程数不会被回收,除非线程池被销毁,需要注意的是线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务

        maximumPoolSize: 最大线程数,核心线程数不够会创建非核心线程,但是是当workQueue队列填满时才会创建多于corePoolSize的线程,后面讲创建流程的时候会提到

        keepAliveTime: 保存生存时间,非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,如果核心线程数和最大核心线程数一致即 corePoolSize maxPoolSize 时无效,因为都是核心线程,不能回收

        unit: keepAliveTime的时间单位

        workQueue: 保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中

        threadFactory: 新线程的创建方式,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建

        handler: 拒绝策略,队列已满且线程数达到maximunPoolSize,策略有四种,后面再说

这个就是我们说所的线程池创建方法,只是这些参数怎么去填才是我们需要关注的点,大家也可以尝试用自己new ThreadPoolExecutor来使用调试,看看线程创建及任务处理完成后的线程情况,当然也可以尝试其他几个构造方法的使用。

Executor

        接下来看Executor的线程池创建 我们看打开其源码,我们主要先看这些个方法(其实应该是每个都有两个,都有不同的重载,代码没贴完,可以自行去查询源码,下面也会详细说明参数含义)

  public static ExecutorService newFixedThreadPool(int var0) {
        return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
  }
	
  public static ExecutorService newSingleThreadExecutor(ThreadFactory var0) {
        return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0));
    }
 
  public static ExecutorService newCachedThreadPool(ThreadFactory var0) {
          return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), var0);
  }
 
  public static ScheduledExecutorService newScheduledThreadPool(int var0) {
        return new ScheduledThreadPoolExecutor(var0);
    
  }

 public static ExecutorService newWorkStealingPool(int var0) {
        return new ForkJoinPool(var0, ForkJoinPool.defaultForkJoinWorkerThreadFactory, (UncaughtExceptionHandler)null, true);
    }

  public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory var0) {
        return new Executors.DelegatedScheduledExecutorService(new ScheduledThreadPoolExecutor(1, var0));
  }

我们一个个的去看他们的区别:

        首先,他们都是创建线程池的方法,也都有两种重载

        1.  newFixedThreadPool  创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,

                var0  代表核心线程长度,及最大线程数,

                还有一个重载是传入线程创建方式:ThreadFactory var1

        2.  newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

                一个无参,一个传入线程创建方式(对照一下源码及ThreadPoolExecutor参数)

        3. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

                无核心参数,最大线程数int的最大值。。。自行对照

        4. newScheduledThreadPool 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。               

             一个核心线程数,一个线程创建工厂,其他对应ThreadPoolExecutor

        5. newSingleThreadScheduledExecutor 我想应该不用介绍了吧,看源码传入了一个1,也就是单线程化的定时线程池

        6. newWorkStealingPool 创建一个抢占式执行的线程池(任务执行顺序不确定)1.8新加入而且源码中创建的方式也是使用的ForkJoinPool 和其他的几种区别参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面5种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。ForkJoinPool 注释还说明这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作。

 那现在回头看,我们已经了解了七种创建线程池的方式了,为啥有些说只有四种或者五种呢?我想可能是前面有人觉得Executor 常用的就那四种(前面四种不算newSingleThreadScheduledExecutor ),所以说线程池创建方式是四种,说五种的应该是加上了ThreadPoolExecutor。那么我们自己来分辨一下,如果面试到底应该是几种?

        只要了解了源码,说几种都是对的,看你怎么划分: 

首先核心的创建方式应该是只有两种

      ThreadPoolExecutor ,Executor

如果按所有创建方式算,应该是七种,Executor加 ThreadPoolExecutor

        注意1.8加入的newWorkStealingPool 

        所以我认为他们说四种,五种的都是错误的,要么两种,要么七种,要么就十三种,因为Executor每一个都有两个方法,参数不一样而已(是不是又可以装逼了,手动滑稽。。。)

那么这么多线程池创建方式,如何选择?

1. 执行很多短期异步的小程序或者负载较轻的服务器  newCachedThreadPool 

2. 执行长期的任务,性能稳定  newFixedThreadPool  

3. 一个任务一个任务执行 newSingleThreadScheduledExecutor ,newSingleThreadExecutor 

        说明一下,这两个单线程的为啥还要用线程池(有时候能遇到面试问)?一个个任务执行情况,不需要太多线程,浪费空间,因为你始终一个个执行的有顺序,另一个方面如果使用Thread还是要创建销毁,线程池虽然也只有一个线程,但是能复用,不用重新创建

4. 周期性执行任务的场景 newScheduledThreadPool 

5. 充分利用线程任务量不均衡 newWorkStealingPool 对于抢占式线程推荐:JDK实现的线程池之五: ForkJoinPool、newWorkStealingPool - duanxz - 博客园 ,这一篇文章很详细的说明,及与自定义创建的对比

ExecutorService常用方法说明

1,execute(Runnablecommand):履行Ruannable类型的任务

2,submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象     

3,shutdown():在完成已提交的任务后封闭办事,不再接管新任务

4,shutdownNow():停止所有正在履行的任务并封闭办事。

5,isTerminated():测试是否所有任务都履行完毕了。

6,isShutdown():测试是否该ExecutorService已被关闭。

线程池的执行流程

        只对ThreadPoolExecutor进行说明,下面我们看个图

 

        1. 线程池创建好了,里面并没有线程,第一个任务来,开始创建线程执行

        2. 线程数小于核心线程数,添加线程继续执行(核心线程未达到最大时,前面线程即使工作完成,也还是继续创建核心线程执行)

        3.核心线程数满了,放入队列

        4.队列满了,判断是否线程数是否大于最大线程数,如果不大于继续加非核心线程,如果大于触发拒绝策略

         线程池提供了4种策略:

        1、AbortPolicy:直接抛出异常,默认策略;

        2、CallerRunsPolicy:用调用者所在的线程来执行任务;

        3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

        4、DiscardPolicy:直接丢弃任务;

        上面的4种策略都是ThreadPoolExecutor的内部类。当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

源码查看

 

这个就是创建线程的流程,上面是描述及翻译,也对应我给出的执行流程

线程池如何关闭? 

shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true

 

关于线程池的相关知识就到此结束,基本能满足使用及面试等等要求,当然如果有兴趣,可以打开源码继续深入一些细节,本文就不再往下深究了,我们在工作中使用线程池要根据上面的创建方式进行选择,阿里规范给出的是不允许使用Executors,当然我个人感觉还是应该根据实际情况选择,使用 ThreadPoolExecutor确实是更好的选择,千万慎用无界线程池(int最大值) 

工作中使用线程池:

        最后附一个在spring boot 中简单使用线程池的源码:

/**
 * 线程池定义
 */
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("myThreadPoolExecutor")
    public Executor threadPoolExecutor(){
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(1);//核心线程数
        taskExecutor.setMaxPoolSize(10);//最大线程数
        taskExecutor.setQueueCapacity(100);//队列大小
        taskExecutor.setKeepAliveSeconds(60);//保持存活时长
        taskExecutor.setThreadNamePrefix("threadPoolExecutor-");//名称前缀
        //下面两个是线程关闭需要注意的问题
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);//线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,默认false
        taskExecutor.setAwaitTerminationSeconds(60);//设置线程池中 任务的等待时间,如果超过这个时间还没有销毁就 强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
        return taskExecutor;
    }
}

        

@Component //方便调用,实际使用之间使用下面注解就是一个任务
public class TestThreadPool{

    @Async("myThreadPoolExecutor")
    public void testTask(){
        System.out.println("第一个线程池任务");
    }
    @Async("myThreadPoolExecutor")
    public void testTask1(){
        System.out.println("第二个线程池任务");
    }
}

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值