创建线程那么容易,为什么非要让我使用线程池?(深深深入剖析)(上)

简介: 创建线程那么容易,为什么非要让我使用线程池?(深深深入剖析)(上)

一、概述


1、问题


先看我们遇到的问题:我们创建线程的方式很简单,new Thread(() -> {...}),就是因为这么简单粗暴的方式,才带来了致命的问题。首先线程的创建和销毁都是很耗时很浪费性能的操作,你用线程为了什么?为了就是异步,为了就是提升性能。简单的new三五个Thread还好,我需要一千个线程呢?你也for循环new1000个Thread吗?用完在销毁掉。那这一千个线程的创建和销毁的性能是很糟糕的!


2、解决


为了解决上述问题,线程池诞生了,线程池的核心思想就是:线程复用。也就是说线程用完后不销毁,放到池子里等着新任务的到来,反复利用N个线程来执行所有新老任务。这带来的开销只会是那N个线程的创建,而不是每来一个请求都带来一个线程的从生到死的过程。


二、线程池


1、概念


还说个鸡儿,上面的问题解决方案已经很通俗易懂了。针对特级小白我在举个生活的案例:


比如找工作面试,涉及到两个角色:面试官、求职者。求职者成千上万,每来一个求职者都要为其单独新找一个面试官来面试吗?显然不是,公司都有面试官池子,比如:A、B、C你们三就是这公司的面试官了,有人来面试你们三轮流面就行了。可能不是很恰当,含义就是说我并不需要为每个请求(求职者)都单独分配一个新的线程(面试官) ,而是我固定好几个线程,由他们几个来处理所有请求。不会反复创建销毁。


2、参数


2.1、源码


public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler) {}


2.2、解释


  • corePoolSize:核心线程数

线程池在完成初始化之后,默认情况下,线程池中不会有任何线程,线程池会等有任务来的时候再去创建线程。核心线程创建出来后即使超出了线程保持的存活时间配置也不会销毁,核心线程只要创建就永驻了,就等着新任务进来进行处理。


  • maximumPoolSize:最大线程数

核心线程忙不过来且任务存储队列满了的情况下,还有新任务进来的话就会继续开辟线程,但是也不是任意的开辟线程数量,线程数(包含核心线程)达到maximumPoolSize后就不会产生新线程了,就会执行拒绝策略。


  • keepAliveTime:线程保持的存活时间

如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,那么这些多余的线程(超出核心线程数的那些线程)就会被回收。


  • unit:线程保持的存活时间单位


比如:TimeUnit.MILLISECONDSTimeUnit.SECONDS


  • workQueue:任务存储队列


核心线程数满了后还有任务继续提交到线程池的话,就先进入workQueue

workQueue通常情况下有如下选择:



LinkedBlockingQueue:无界队列,意味着无限制,其实是有限制,大小是int的最大值。也可以自定义大小。


ArrayBlockingQueue:有界队列,可以自定义大小,到了阈值就开启新线程(不会超过maximumPoolSize)。


SynchronousQueueExecutors.newCachedThreadPool();默认使用的队列。也不算是个队列,他不没有存储元素的能力。


一般都采取LinkedBlockingQueue,因为他也可以设置大小,可以取代ArrayBlockingQueue有界队列。


  • threadFactory:当线程池需要新的线程时,会用threadFactory来生成新的线程


默认采用的是DefaultThreadFactory,主要负责创建线程。newThread()方法。创建出来的线程都在同一个线程组且优先级也是一样的。


  • handler:拒绝策略,任务量超出线程池的配置限制或执行shutdown还在继续提交任务的话,会执行handler的逻辑。


默认采用的是AbortPolicy,遇到上面的情况,线程池将直接采取直接拒绝策略,也就是直接抛出异常。RejectedExecutionException


3、原理


3.1、原理


  • 线程池刚启动的时候核心线程数为0
  • 丢任务给线程池的时候,线程池会新开启线程来执行这个任务
  • 如果线程数小于corePoolSize,即使工作线程处于空闲状态,也会创建一个新线程来执行新任务
  • 如果线程数大于或等于corePoolSize,则会将任务放到workQueue,也就是任务队列
  • 如果任务队列满了,且线程数小于maximumPoolSize,则会创建一个新线程来运行任务
  • 如果任务队列满了,且线程数大于或等于maximumPoolSize,则直接采取拒绝策略


3.2、图解


image.png


3.3、举例



线程池参数配置:核心线程5个,最大线程数10个,队列长度为100。


那么线程池启动的时候不会创建任何线程,假设请求进来6个,则会创建5个核心线程来处理五个请求,另一个没被处理到的进入到队列。这时候有进来99个请求,线程池发现核心线程满了,队列还在空着99个位置,所以会进入到队列里99个,加上刚才的1个正好100个。这时候再次进来5个请求,线程池会再次开辟五个非核心线程来处理这五个请求。目前的情况是线程池里线程数是10个RUNNING状态的,队列里100个也满了。如果这时候又进来1个请求,则直接走拒绝策略。


3.4、源码


public void execute(Runnable command) {
    int c = ctl.get();
    // workerCountOf(c):工作线程数
    // worker数量比核心线程数小,直接创建worker执行任务
    if (workerCountOf(c) < corePoolSize) {
        // addWorker里面负责创建线程且执行任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // worker数量超过核心线程数,任务直接进入队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 线程池状态不是RUNNING状态,说明执行过shutdown命令,需要对新加入的任务执行reject()操作。
        // 这儿为什么需要recheck,是因为任务入队列前后,线程池的状态可能会发生变化。
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 这儿为什么需要判断0值,主要是在线程池构造方法中,核心线程数允许为0
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。
    // 这儿有3点需要注意:
    // 1. 线程池不是运行状态时,addWorker内部会判断线程池状态
    // 2. addWorker第2个参数表示是否创建核心线程
    // 3. addWorker返回false,则说明任务执行失败,需要执行reject操作
    else if (!addWorker(command, false))
        reject(command);
}


4、Executors


4.1、概念


首先这不是一个线程池,这是线程池的工具类,他能方便的为我们创建线程。但是阿里巴巴开发手册上说明不推荐用Executors创建线程池,推荐自己定义线程池。这是因为Executors创建的任何一种线程池都可能引发血案,具体是什么问题下面会说。


4.2、固定线程数


4.2.1、描述


核心线程数和最大线程数是一样的,所以称之为固定线程数。


其他参数配置默认为:永不超时(0ms),无界队列(LinkedBlockingQueue)、默认线程工厂(DefaultThreadFactory)、直接拒绝策略(AbortPolicy)。


4.2.2、api


Executors.newFixedThreadPool(n);


4.2.3、demo


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * Description: 创建2个线程来执行10个任务。
 *
 * @author TongWei.Chen 2020-07-09 21:28:34
 */
public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10; i++) {
            // 从结果中可以发现线程name永远都是两个。不会有第三个。
            executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
        }
    }
}


4.2.4、问题


问题就在于它是无界队列,队列里能放int的最大值个任务,并发巨高的情况下极大可能直接OOM了然后任务还在堆积,毕竟直接用的是jvm内存。所以建议自定义线程池,自己按照需求指定合适的队列大小,自定义拒绝策略将超出队列大小的任务放到对外内存做补偿,比如Redis。别把业务系统压垮就行。


4.2.5、源码


public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
                // 核心线程数和最大线程数都是nThreads
                nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  // 无界队列!!!致命问题的关键所在。
                                  new LinkedBlockingQueue<Runnable>());
}


相关文章
|
4月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
123 1
|
4月前
|
存储 监控 安全
一天十道Java面试题----第三天(对线程安全的理解------>线程池中阻塞队列的作用)
这篇文章是Java面试第三天的笔记,讨论了线程安全、Thread与Runnable的区别、守护线程、ThreadLocal原理及内存泄漏问题、并发并行串行的概念、并发三大特性、线程池的使用原因和解释、线程池处理流程,以及线程池中阻塞队列的作用和设计考虑。
|
1月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
105 38
|
29天前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
58 2
|
1月前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
69 4
|
3月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
144 29
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
1月前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
118 2
|
2月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
116 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
3月前
|
Java
直接拿来用:进程&进程池&线程&线程池
直接拿来用:进程&进程池&线程&线程池
|
2月前
|
设计模式 Java 物联网
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
【多线程-从零开始-玖】内核态,用户态,线程池的参数、使用方法详解
59 0