Java线程池

本文探讨了Java线程池的重要性,解释了为何频繁创建线程会导致性能开销,介绍了Executors类提供的线程池工厂方法,详细解析了ThreadPoolExecutor的构造参数和执行策略,并指出了使用Executors创建线程池的潜在风险。
摘要由CSDN通过智能技术生成

线程池

为什么需要线程池?

当我们想要程序并发执行的时候,通常会去手动创建一个新的线程,比如

new Thread(() -> {
    并行任务代码...
}).start();

单个线程这么做确实没什么问题

但是,如果需要创建1000个甚至更多的线程呢?难道每次都需要new Thread吗?

创建线程需要消耗一些系统资源。首先,线程的创建需要开辟内存资源,包括本地方法栈、虚拟机栈和程序计数器等线程私有变量的内存。具体来说,每个线程都有其专属的内核数据,如OSID(线程的ID)和Context(存放CPU寄存器相关的资源)。这些上下文状态会被保存到Context中,以便下次使用。

其次,频繁的创建线程和销毁线程会带来一定的性能开销。但是,在同一进程中,只是创建第一个线程的时候需要申请资源,后续再创建新的线程都是共用同一份资源(节省了申请资源的开销);销毁线程的时候,只有销毁到最后一个线程的时候才真正释放资源,前面的线程销毁都不是真正的释放资源。

怎么解决呢?

那就是使用线程池,线程池通俗来讲就是一个"池子"里存放了固定存活的线程,只要你有任务了就可以去线程池中捞一个线程把任务交给它去处理,等它处理完任务了,再把线程放回池子里

在这里插入图片描述

此外,由于线程是反复利用的,所以降低了创建线程分配资源和销毁线程的开销,提高了响应速度。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题

实践

Executors类提供4个静态工厂方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor()和newScheduledThreadPool(int)。

public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            // 提交任务给线程池
            singleThreadExecutor.execute(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程执行");
            });
        }
        // 当线程池的任务都执行完了,关闭线程池
        singleThreadExecutor.shutdown();
    }

这里不一一写Executors提供的方法,其实就是改变了ExecutorService的实现方法

方法对比

工厂方法corePoolSizemaximumPoolSizekeepAliveTimeworkQueue
newCachedThreadPool0Integer.MAX_VALUE60sSynchronousQueue
newFixedThreadPooln(手动指定)n(手动指定)0LinkedBlockingQueue
newSingleThreadExecutor110LinkedBlockingQueue
newScheduledThreadPoolcorePoolSizeInteger.MAX_VALUE0DelayedWorkQueue

自定义线程池

在这里插入图片描述

在这里插入图片描述

点进newSingleThreadExecutor方法可以看见,其实创建线程池是需要new一个ThreadPoolExecutor()对象,这个ThreadPoolExecutor对象决定了线程池的运行策略

参数说明

  1. corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
  2. maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  3. keepAliveTime(线程存活保持时间):默认情况下,当线程池的线程个数多于corePoolSize时,如果线程在 keepAliveTime 的时间内 poll 不到任务,那我就认为这条线程没事做,可以干掉了。但只要keepAliveTime大于0,allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。另外,也可以使用setKeepAliveTime()动态地更改参数。
  4. unit(存活时间的单位):时间单位,分为7类,从细到粗顺序:NANOSECONDS(纳秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小时),DAYS(天);
  5. workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。可以使用此队列与线程池进行交互:
    如果运行的线程数少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
    如果运行的线程数等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
    如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
  6. threadFactory(线程工厂):用于创建新线程。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号);
  7. handler(线程饱和策略):当线程池和队列都满了,则表明该线程池已达饱和状态。
    ThreadPoolExecutor.AbortPolicy:处理程序遭到拒绝,则直接抛出运行时异常 RejectedExecutionException。(默认策略)
    ThreadPoolExecutor.CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
    ThreadPoolExecutor.DiscardPolicy:无法执行的任务将被删除。
    ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。

线程池的执行过程

在这里插入图片描述

案例:

假设我们自定义一个线程池执行器

public ThreadPoolExecutor threadPoolExecutor() {
    // 创建线程时会先执行threadFactory的newThread方法
    ThreadFactory threadFactory的 = new ThreadFactory() {
        private int count = 1;

        @Override
        public Thread newThread(@NotNull Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("线程" + count);
            count++;
            return thread;
        }
    };
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(4), threadFactory);
    return threadPoolExecutor;
}

参数:corePoolSize: 2, maximumPoolSize: 4, keepAliveTime: 100s, workQueue: ArrayBlockingQueue(容量为4)

提交2个任务到线程池

在这里插入图片描述

继续提交两个任务

在这里插入图片描述

此时线程池要处理的任务大于corePoolSize,就将未处理的任务放入任务队列中

继续往线程池提交4个任务

在这里插入图片描述

从上图可以看出两个特点:

  1. 线程池的线程增多了。这是因为当我们添加到第6个任务的时候,任务队列已经满了,放不下了;那这个时候maximumPoolSize参数就派上场了,maximumPoolSize决定了线程池最多能有多少个线程,一旦任务队列满了,再有新任务提交,就新增线程到线程池(找临时工干活);那临时工是怎么退掉的呢?临时线程如果在keepAliveTime时间内没事情干,就会被干掉(解雇)
  2. 新增的线程没有处理任务队列的第一个任务,而是去处理了队列满了之后的新增任务;所有我们看见了线程3去处理了任务7,线程4去处理了任务8

什么情况下会去执行handler?

当线程池和队列都满了,则表明该线程池已达饱和状态,这时候线程池就会去执行拒绝策略

有哪几种拒绝策略?

拒绝策略场景分析

(1)AbortPolicy

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.AbortPolicy());

这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样能在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

(2)DiscardPolicy

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.DiscardPolicy());

使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。

(3)DiscardOldestPolicy

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy());

此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。

(4)CallerRunsPolicy

ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

慎用!!!,假如提交任务的是主线程,那么可能会影响主线程任务的执行

为什么要自定义线程池

在这里插入图片描述

在这里插入图片描述

Fixed ThreadPool存在的问题就是它使用的是LinkedBlockingQueue,LinkedBlockingQueue的容量是没有上限的,一旦处理队列线程的速度跟不上线程入队的速度,很有可能产生OOM(内存溢出)

在这里插入图片描述

SingleThreadPool也有同样的问题

在这里插入图片描述

newCachedThreadPool存在的问题就是没有去限制最大的线程数

总结:

为什么不推荐使用Executors创建的线程池?很简单,虽然我们使用Executors创建了固定的线程数量,但是却没有指定任务队列的实现以及大小,一旦大量任务积压在任务队列中,可能会产生OOM(内存溢出)问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值