【多线程】线程池

🏀🏀🏀来都来了,不妨点个关注!
🎧🎧🎧博客主页:欢迎各位大佬!
在这里插入图片描述

1. 什么是线程池

说起池,我们就会联想到之前学过的字符串常量池,数据库连接池等,关于“池”还是很常见的,池的目的就是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池就是提前将线程准备好,创建线程不再是从系统中申请,而是直接从线程池中取,线程不用了也是还给线程池。

1.1 为什么要使用线程池

上面我们说了使用池主要是为了减少每次获取资源的消耗,提高对资源的利用率。我们就需要从使用线程池的好处出发解释:

  1. 降低资源消耗:频繁的创建和销毁线程对资源消耗较大,使用线程池可以复用已经创建的线程。
  2. 提高响应速度:当任务到达时,任务可以不需要等待线程的创建就能立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程进行统一的分配、调优和监控。

1.2 如何创建线程池

1.2.1 通过ThreadPoolExecutor构造方法创建线程池

ThreadPoolExecutor是创建线程池最原始也是最推荐的方式,它提供了7个参数来灵活配置线程池。

ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    int corePoolSize,  
    int maximumPoolSize,  
    long keepAliveTime,  
    TimeUnit unit,  
    BlockingQueue<Runnable> workQueue,  
    ThreadFactory threadFactory,  
    RejectedExecutionHandler handler);

1.2.2 通过Executors工厂类创建线程池

Executors工厂类提供了多种便捷的线程池创建方法,这些方法内部都是通过ThreadPoolExecutor类实现的,但简化了配置过程。

  • FixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
   ExecutorService pool = Executors.newFixedThreadPool(int nThreads);

其中的nThreads是线程池中线程的数量。

  • CachedThreadPoo:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
   ExecutorService Pool = Executors.newCachedThreadPool();

这种线程池适合执行大量短期异步任务。

  • SingleThreadExecutor:创建单个线程的线程池,它可以保证先进先出的执行顺序。
   ExecutorService pool = Executors.newSingleThreadExecutor();

这种线程池适合需要顺序执行的任务。

  • ScheduledThreadPool:创建一个可以执行延迟任务的线程池。
ExecutorService pool = Executors.newScheduledThreadPool(int corePoolSize);

其中的corePoolSize是核心线程数。

2. ThreadPoolExecutor 类

ThreadPoolExecutor 类,是 ExecutorService 接口的一个实现类,是原装的线程池类,上述的所有工厂方法都是对这个类进行进一步的封装。
这里我们可以打开官方文档,对ThreadPoolExecutor进行详细的了解。

2.1 构造方法

在这里插入图片描述
可以看到ThreadPoolExecutor 类的构造方法里的参数有很多,这里我们拿最后一个,最全的进行解释:

ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    int corePoolSize,  
    int maximumPoolSize,  
    long keepAliveTime,  
    TimeUnit unit,  
    BlockingQueue<Runnable> workQueue,  
    ThreadFactory threadFactory,  
    RejectedExecutionHandler handler);
  • corePoolSize: 核心线程数,即使此时线程池中没有任务也不会销毁
  • maximumPoolSize:最大线程数,
  • keepAliveTime:非核心线程存活时间
  • unit:keepAliveTime参数的时间单位,(比如毫秒,秒,分钟)
  • workQueue:任务队列,用于存放等待执行的任务的阻塞队列,可以选择不同类型的队列,如ArrayBlockingQueue、LinkedBlockingQueue等。
  • threadFactory:线程工厂,用于创建线程使用的工厂类
  • handler:线程池的拒绝策略,当线程池里的任务满了继续往里面添加任务时如何拒绝。

对于核心线程和最大线程数我们可以那公司的员工进行举例:
把公司比作一个线程池,里面有正式工和实习生,正式工就类似于核心线程数,他们负责处理任务,当任务特别多的时候,公司就会招实习生进来打杂,实习生就类似于非核心线程数正式工+实习生的数量则是最大线程数,而当公司任务变少了不忙了,公司为了节省成本和资源就会将实习生裁掉,而实习生在公司待的时间就是非核心线程存活时间。如下图:
在这里插入图片描述

2.2 四种拒绝策略

在这里插入图片描述
ThreadPoolExecutor.AbortPolicy(中止策略):这是默认的拒绝策略,如果任务满了,继续添加任务,直接抛出RejectedExecutionException异常。
ThreadPoolExecutor.CallerRunsPolicy(调用者策略):添加的线程自己执行这个任务。
ThreadPoolExecutor.DiscardPolicy(丢弃策略): 丢弃最新的任务,不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy(丢弃最老策略): 丢弃最老的任务,会丢弃任务队列中的头结点(通常是存活时间最长且未被处理的任务),然后尝试执行新的任务。

3.实现一个线程池

下面线程实现的是一个固定线程数的线程池,和我们上面的FixedThreadPool类似,如下:

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

class MyThreadPool {
    private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }


    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread( () -> {
                try {
                    while (true) {
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });
            t.start();
        }
    }
}

public class ThreadDemo26 {
    public static void main(String[] args) throws InterruptedException {
          MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int number = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello" + number);
                }
            });
        }
    }
}

运行结果如下:
在这里插入图片描述
实现过程:

  1. 手动实现线程池,核心的数据结构是 BlockingQueue,用于存放各个可执行的任务
  2. submit()方法,是提交任务,将任务添加到队列中
  3. 实现一个固定线程数的线程池,MyThreadPool(int n),其中 n 为固定线程数量,在线程池构造方面里面,通过 for 循环,创建 n 个 工作线程,n 个线程并发执行,每个线程的任务是从队列中获取的,并进行执行,即在 while(true) 循环中,既有取队列操作,又有执行的操作;将 while(true) 中,循环条件设置为 true,是不让线程在执行完任务后终止,保持工作线程活跃的状态,如果去掉 true,可以看到,只会执行 n 次,因为在执行完后线程终止了不再执行,线程池数量不够任务数量,就无法处理后续任务
  4. 在主线程中,创建线程池,并通过 for 循环 与 submit() 向阻塞队列提交 1000 个任务,工作线程从队列中获取任务并进行执行
  5. 为什么要将 i 赋值 给 number,而不直接打印 i,这里涉及到 lambda 表达式的变量捕获规则,lambda 表达式捕获的变量必须是 final 修饰或者是实际 final,实际 final 是不能被修改的,在这里因为 i 变量被修改了,创建一个新的变量保存 i,即可解决

3.1 如何给线程池设置合理的线程数

在实际开发中怎么和线程池设置合理的线程数呢?
我们知道线程池中的线程数并不是越多越好,每一个线程都需要CPU的调度,都会对资源产生消耗,所以,一个线程池的线程数的数量如何设置,需要结合实际情况出发:

  1. CPU密集型任务:目标是尽量减少线程的上下文切换,以优化CPU的使用率,所以线程数一般设置为CPU的数量或者+1,
  2. IO密集型任务:对于线程经常处于等待状态(等待IO操作),可以设置更多的线程来提高并发性(比如两倍),从而增加CPU的利用率。

3.2 线程池处理任务的流程

线程池处理任务的流程图如下:
在这里插入图片描述

  1. 提交一个任务后,当此时的空闲线程数小于核心线程数(corePoolSize),就会创建一个核心线程来执行任务。
  2. 如果此时的空闲线程数大于核心线程数。就会将任务添加到工作队列。
  3. 如果工作队列满了,并且此时的线程数小于最大线程数,就会创建一个非核心线程来执行任务。
  4. 当此时的线程数等于最大线程数,再创建线程就会大于最大线程数了,此时会执行拒绝策略。

3.3 线程池提交任务

当有新任务需要执行的时候,这些任务会被提交到线程池,在我们上面自己实现的线程池中使用的是submit()方法,提交任务的方法有两种,如下:

  1. execute()方法:这种方式提交的任务是Runnable的,并且没有返回值,当任务执行出现异常,通常是直接抛出异常。
  2. submit()方法:这种方式既可以提交Runnable类型的任务,也可以提交Callable类型的任务。对于Callable类型的任务,submit()方法可以返回Future类型的结果,用于获取线程任务执行的结果。当任务产生异常时,异常通常会被捕获。

3.4 线程池的关闭

线程池的关闭可以通过shutdown()和shutdownNow()两个方法,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。区别如下:

  1. shutdown() :此方法会等待线程池中的所有任务执行完后关闭线程。
  2. shutdownNow() :此方法会尝试立即停止所有正在执行的任务,并返回那些未开始执行的任务列表。

本次分享就结束了,感谢支持!

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java多线程线程池是一种重用线程的机制,它可以管理和调度多个线程来执行任务。使用线程池可以提高程序的性能和效率,同时也可以避免线程的频繁创建和销毁。 在Java中,可以使用线程池类ExecutorService和Executors来创建和管理线程池。具体步骤如下: 1. 创建一个线程池对象:可以使用Executors提供的静态方法创建不同类型的线程池,比如newFixedThreadPool固定大小线程池、newCachedThreadPool缓存线程池等。 2. 向线程池提交任务:使用submit方法向线程池提交需要执行的任务,也可以使用execute方法提交任务。任务可以是实现了Runnable接口或Callable接口的类,也可以是Lambda表达式。 3. 线程池执行任务:线程池会根据线程池的规模和任务的数量来调度和执行任务,多个任务会并发执行。 4. 关闭线程池:当不再需要线程池时,可以调用线程池的shutdown方法来关闭线程池,确保所有的任务都被执行完毕。 使用线程池的好处有: 1. 提高性能:线程池可以重用线程,避免线程频繁创建和销毁的开销,提高程序的性能。 2. 提供线程管理和调度:线程池可以管理和调度线程,根据线程池的规模和任务的数量来调度和执行任务。 3. 控制资源的使用:线程池可以限制并发线程的数量,避免过度占用系统资源。 在Java开发中,使用线程池是一种推荐的多线程编程方式,也是阿里巴巴在其《Java开发手册》中强制规定的做法。 Java线程的创建是依赖于系统内核的,通过JVM调用系统库创建内核线程,内核线程与Java Thread是1:1的映射关系。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Java多线程(六):线程池详解](https://blog.csdn.net/m0_59140023/article/details/124436385)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [java多线程线程池](https://blog.csdn.net/qq_29996285/article/details/118955325)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值