搞懂线程池

搞懂线程池
一、为什么要使用线程池
1.1 活跃线程过多会导致OOM

​ 创建完线程后,线程是需要内存去放的,一个线程对应一个Thread对象。我们知道,对象是会占用JVM中的堆内存的空间。所创建的线程越多,线程的上下文切换不仅影响性能,它占用的内存也就越多,可能导致OOM,代码如下:

public class ThreadPoolDemo {
    public static void show(){
       for(int i=0;;i++){//死循环,一直创建线程并启动。
           new Thread(()->{
               try {
                   Thread.sleep(TimeUnit.SECONDS.toSeconds(Integer.MAX_VALUE));
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }).start();
           System.out.println("************* "+i);
       }
    }
}

执行结果:

在这里插入图片描述

1.2 线程不能复用

​ 普通的创建线程的方法执行任务不能连续执行,在完成任务之后,线程会被销毁。需要完成别的任务,又需要创建新的线程。而线程创建与线程销毁是耗费资源耗费时间的操作,线程不能复用是对系统资源的浪费。传统的创建线程的方法,完成不同的任务,如下所示:

public class ThreadPoolDemo {
    public static void show(){
        //创建任务 需要完成任务的顺序 task1->task2->task3
        Task task1 = new Task();
        Task task2 = new Task();
        Task task3 = new Task();
        //必须创建3个线程,才能完成任务要求
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        Thread thread3 = new Thread(task3);
        //启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
public class Task implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
1.3 线程池的引入

​ 线程池是一种线程使用模式,线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。总的来说,使用线程池有以下三个优点:

  1. 降低系统资源的消耗。线程池起到线程复用的作用,不用创建过多的线程,也就不会消耗太多系统资源。
  2. 提高响应速度。线程池中的线程,其中的核心线程,一直都存在,不用创建也不会销毁。当并发任务到来时,提高了响应速度。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、如何使用线程池
2.1 线程池的相关类与接口

​ Java线程池核心的实现类是ThreadPoolExecutor,我们想要创建自己的线程池时,可以使用这个类。ThreadPoolExecutor实现的顶层接口是Executor。顶层接口Executor提供了一种思想:任务提交和任务执行进行解耦。我们无需关心如何创建线程如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行。ExecutorService接口增加提供了监控线程池的方法和扩充任务执行能力。它们的UML图如下所示:

在这里插入图片描述

​ Executors是快速得到线程池的工具类,创建线程池的工厂类。能够创建各种功能的线程池,比如newFixedThreadPool(int nThreads),是创建一个固定大小、任务队列无界的线程池。newSingleThreadExecutor(),创建的是只有一个线程来执行无界任务的单一线程池。虽然Executors使用起来很方便,不过在阿里编程规范里是强调了慎用Executors创建线程池。【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

2.2 ThreadPoolExecutor创建线程池并使用

​ 首先创建线程池对象ThreadPoolExecutor,然后向线程池提交任务调用execute方法,最后关闭线程池。线程池的使用代码如下所示:

public class ThreadPoolDemo {
    public static void show(){
        //1. 创建线程池
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 5, 60L,
                TimeUnit.SECONDS, new ArrayBlockingQueue(5));
        //2.提交线程池任务
        poolExecutor.execute(
                () -> {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(String.format("thread Name:%s",Thread.currentThread().getName()));
                });
        //3.关闭线程池,防止内存泄漏
        poolExecutor.shutdown();//设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
        //poolExecutor.shutdownNow();//设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
    }
}

执行结果:

在这里插入图片描述

三、线程池的七大参数
3.1 线程池七大参数

​ 我们在创建ThreadPoolExecutor对象时,发现有四个构造器方法,其中的参数是线程池的七大参数,有5个参数在创建对象时必须给出。它们分别是corePoolSizemaximumPoolSizekeepAliveTimeunitworkQueuethreadFactoryhandler。它们的详细介绍,如下表所示:

参数描述
corePoolSize核心线程数。即使这些线程处于空闲状态,他们也不会被销毁。默认情况下,核心线程会一直存活。
maximumPoolSize线程池所能容纳的最大线程数。当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到任务队列中。当活跃线程数达到该数值后,后续的新任务将会阻塞。
keepAliveTime线程闲置超时时长。一个线程如果处于空闲状态,如果超过该时长,非核心线程就会被回收。
unit指定 keepAliveTime 参数的时间单位。
workQueue任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
threadFactory(非必需)线程工厂。用于指定为线程池创建新线程的方式。
handler(非必需)拒绝策略。当达到最大线程数时需要执行的饱和策略。
3.2 线程池拒绝策略

​ 当任务队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的。JDK提供了四种拒绝策略:

  • DiscardPolicy(直接丢弃)。在该策略下,线程池直接丢弃任务,什么都不做。
  • AbortPolicy(丢弃抛异常)。这是默认策略,该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
  • DiscardOldestPolicy(丢弃任务队列中最早任务,换新任务)。该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
  • CallerRunsPolicy(调用线程直接运行)。在该策略下,对新任务优先执行,直接调用其run()方法。
四、线程池的原理

​ 线程池的工作原理简单点来概括就是:工作任务可以一直放,直到线程池满了线程池才会拒绝。除了核心线程,其它线程空闲的情况下会被合理回收。正常情况下,线程池中线程的数量会处在corePoolSize与maximumPoolSize的闭区间。下图是线程池工作原理的示意图:

在这里插入图片描述

​ 详细的工作流程可以用下面的流程图表示:

在这里插入图片描述

五、线程池的分类与选择

​ 上述的ThreadPoolExecutor创建线程池的过程需要我们给定较多的参数,比较复杂。线程池工具类Executors为我们封装了4种常见的功能性线程池:

  1. 定长线程池(FixedThreadPool):只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。适用于控制线程最大并发数。
  2. 定时线程池(ScheduledThreadPool ):核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。适用于执行定时或周期性的任务。
  3. 可缓存线程池(CachedThreadPool):无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。适用于执行大量、耗时少的任务。
  4. 单线程化线程池(SingleThreadExecutor):只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。不适合并发以及可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。

总结:虽然Executors使用起来很方便,不过在阿里编程规范里是强调了慎用Executors创建线程池,而是通过ThreadPoolExecutor的方式。

弊端:

  • FixedThreadPool和SingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
    xecutors使用起来很方便,不过在阿里编程规范里是强调了慎用Executors创建线程池,而是通过ThreadPoolExecutor的方式。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值