Java多线程基础-11:工厂模式及代码案例之线程池(二)

简介: 这篇内容介绍了Java多线程基础,特别是线程池中的定时器和拒绝策略。

Java多线程基础-10:代码案例之定时器(一) +https://developer.aliyun.com/article/1520548?spm=a2c6h.13148508.setting.14.75194f0ethWdBZ


4、标准库提供的4种拒绝策略⭐


下面是标准库提供的四种拒绝策略。



  • ThreadPoolExecutor.AbortPolicy 直接抛异常:如果线程池满了还在继续加任务,添加操作就直接抛出异常,新任务和老任务都执行不了了。(你一个礼拜周一到周日满课,天天早八到晚十,结果班长还要求你去打扫办公室。你一听这个消息,直接绷不住了,课也不上了,哇得一声嚎啕大哭。)


  • ThreadPoolExecutor.CallerRunsPolicy 添加任务的线程自己负责执行这个任务,即在哪个线程中写了submit()或execute()等向线程池中添加任务的方法,就由哪个线程来执行它要添加的任务。(你直接怼回去:我才不去呢,要去你自己去。)


  • ThreadPoolExecutor.DiscardOldestPolicy 丢弃最老的任务,即阻塞队列队首元素,不执行了,直接删除。(你看了一下课表,决定把最早要上的这一节课鸽了,去打扫。)


  • ThreadPoolExecutor.DiscardPolicy 丢弃最新的任务,只做原来的任务。(你还是继续上你的课,打扫办公室这个任务就直接丢弃了。)


注意,这里线程池并没有依赖于阻塞队列的阻塞行为,而是通过额外实现其它逻辑来更好地处理这个场景的操作。就好比班长告诉你你要去打扫卫生,然后他就阻塞住了,他也干不了别的你也干不了别的……最好的情况应当是你立即给出答复。在线程池中,并不希望依赖“满了阻塞”,而更主要是利用“空了阻塞”。


关于各个拒绝策略的具体场景,可以参考这篇文章:


🔗线程池的拒绝策略


三、代码实现线程池


下面代码实现了一个固定线程数的简单的线程池:


import java.util.*;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
 
class MyThreadPool{
    //阻塞队列用来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
 
    //提交任务
    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 ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int num = i;    //lambda表达式的变量捕获规则
            myThreadPool.submit(()->{
                System.out.println("hello " + num);
            });
        }
        Thread.sleep(3000);
    }
}


运行结果:




打印1000次


1、代码解析


核心的数据结构是BlockingQueue,它用于存放各个可执行的任务(runnable):


class MyThreadPool{
    //阻塞队列用来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
 
    //...
}


submit()就像生产者一样,给队列中添加任务:

1.
class MyThreadPool{
    //向线程池中提交任务
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
 
    //...
}


在线程池内部有一组总数为n的工作线程,它就像消费者一样,不停地从队列中取任务然后执行。


主线程中传入n为10,即线程数固定为10。在线程池构造方法中,通过for循环创建了10个工作线程。这10个工作线程是并发执行,无序调度的。每一个线程的任务都是不断从阻塞队列中获取任务并执行。因此这里为了保证工作线程的活跃,不会在执行完一个任务后立即终止线程,需要给取任务、执行任务的操作加上while(true)。如果没有while(true),线程执行完一个任务后就会终止,导致线程池中的线程数量不足,无法处理后续的任务(运行结果中只能打印10次)。


class MyThreadPool{
    //实现一个固定线程数的线程池
    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();
        }
    }
 
    //...
}


主线程中,创建出线程池,并通过循环向其中添加1000个任务。这1000个任务先后被添加到阻塞队列中,工作线程从阻塞队列中获取任务并执行。注意,这里num的创建是由于lambda表达式(或匿名内部类)的变量捕获规则,它要求lambda表达式中捕获到的变量必须是final或实际final的(即不能被更改),由于变量 i 被更改了,因此重新创建一个变量来保存 i,代替 i 来使用:


public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int num = i;
            myThreadPool.submit(()->{
                System.out.println("hello " + num);
            });
        }
        Thread.sleep(3000);
    }
}


2、实际开发中如何给线程池设置合适的线程数量


实际不同的程序中,线程需要干的活大不相同。一个线程池的线程数量设置成几是比较合适的?这需要结合具体的任务情况,测试而定。


CPU密集型任务,主要做一些计算工作,要在 CPU 上运行。假设一个极端情况,如果你的线程执行的全是使用CPU资源的任务,那么线程数就不应该超过CPU的核心数(指逻辑核心)。

IO密集型任务,主要是等待 IO 操作(等待读写硬盘,读写网卡等),不怎么消耗 CPU 资源。如果你的线程全是使用IO,线程数就可以设置很多,远远超出 CPU 核心数。

不过,实践中很少有这么极端的情况,具体要通过测试的方式来确定,选取一个性能上恰当且资源使用上也恰当的这样一个均衡的结果。


测试的大体思路是:运行程序,通过记录时间戳计算一下执行时间(平均值),同时监测资源使用状态。


四、总结:线程池的执行流程


1. 任务提交


当有任务需要执行时,将任务提交给线程池。


2. 队列处理


线程池会将提交的任务放入任务队列中。任务队列是一个缓冲区,用于存储等待执行的任务。不同类型的线程池可能使用不同的任务队列。


3. 线程调度


线程池中的线程会从任务队列中获取任务。线程池会根据配置的核心线程数、最大线程数、线程空闲时间等参数来决定是否创建新的线程,或者复用空闲的线程来执行任务。


4. 任务执行


线程池中的工作线程会执行从任务队列中获取的任务。每个线程执行一个任务后,会继续从任务队列中获取下一个任务,直到线程池关闭或者没有更多的任务可以执行。


5. 线程回收


如果线程池中的线程空闲时间超过设定的时间(线程空闲时间设置的过程中),则线程会被回收,以减少资源消耗。但是,核心线程不会被回收,线程池会保持至少核心线程数的线程处于运行状态。


6. 线程池关闭


当不再需要线程池时,应该显式地关闭线程池,释放相关资源。关闭线程池后,线程池将不再接受新的任务,并且会等待所有已提交的任务执行完毕。


总结来说,线程池的执行流程涉及任务提交、队列处理、线程调度、任务执行和线程回收等步骤。通过线程池的管理,我们可以更好地控制线程的创建和销毁,提高程序的性能和效率,并避免因为频繁创建和销毁线程而导致的资源浪费和性能下降。


相关文章
|
10月前
|
安全 Java API
Java 集合高级应用与实战技巧之高效运用方法及实战案例解析
本课程深入讲解Java集合的高级应用与实战技巧,涵盖Stream API、并行处理、Optional类、现代化Map操作、不可变集合、异步处理及高级排序等核心内容,结合丰富示例,助你掌握Java集合的高效运用,提升代码质量与开发效率。
379 0
|
10月前
|
安全 JavaScript Java
java Web 项目完整案例实操指南包含从搭建到部署的详细步骤及热门长尾关键词解析的实操指南
本项目为一个完整的JavaWeb应用案例,采用Spring Boot 3、Vue 3、MySQL、Redis等最新技术栈,涵盖前后端分离架构设计、RESTful API开发、JWT安全认证、Docker容器化部署等内容,适合掌握企业级Web项目全流程开发与部署。
816 0
|
11月前
|
缓存 算法 NoSQL
校招 Java 面试高频常见知识点深度解析与实战案例详细分享
《2025校招Java面试核心指南》总结了Java技术栈的最新考点,涵盖基础语法、并发编程和云原生技术三大维度: 现代Java特性:重点解析Java 17密封类、Record类型及响应式Stream API,通过电商案例演示函数式数据处理 并发革命:对比传统线程池与Java 21虚拟线程,详解Reactor模式在秒杀系统中的应用及背压机制 云原生实践:提供Spring Boot容器化部署方案,分析Spring WebFlux响应式编程和Redis Cluster缓存策略。
308 0
|
11月前
|
人工智能 Java API
Java 生态大模型应用开发全流程实战案例与技术路径终极对决
在Java生态中开发大模型应用,Spring AI、LangChain4j和JBoltAI是三大主流框架。本文从架构设计、核心功能、开发体验、性能扩展性、生态社区等维度对比三者特点,并结合实例分析选型建议。Spring AI适合已有Spring技术栈团队,LangChain4j灵活性强适用于学术研究,JBoltAI提供开箱即用的企业级解决方案,助力传统系统快速AI化改造。开发者可根据业务场景和技术背景选择最适合的框架。
2422 2
|
11月前
|
自然语言处理 前端开发 Java
JBoltAI 框架完整实操案例 在 Java 生态中快速构建大模型应用全流程实战指南
本案例基于JBoltAI框架,展示如何快速构建Java生态中的大模型应用——智能客服系统。系统面向电商平台,具备自动回答常见问题、意图识别、多轮对话理解及复杂问题转接人工等功能。采用Spring Boot+JBoltAI架构,集成向量数据库与大模型(如文心一言或通义千问)。内容涵盖需求分析、环境搭建、代码实现(知识库管理、核心服务、REST API)、前端界面开发及部署测试全流程,助你高效掌握大模型应用开发。
960 5
|
11月前
|
缓存 Java API
Java 集合容器实操技巧与案例详解
本教程基于Java 8+新特性和现代开发实践,深入讲解Java集合容器的实操技巧。通过具体场景演示Stream API数据处理、ConcurrentHashMap并发控制、LinkedHashMap实现LRU缓存、TreeSet自定义排序等高级特性。同时涵盖computeIfAbsent优化操作、EnumMap专用集合使用、集合统计与运算(交集、并集、差集)等内容。代码示例丰富,助力掌握高效编程方法。[点击获取完整代码](https://pan.quark.cn/s/14fcf913bae6)。
219 0
|
11月前
|
前端开发 JavaScript Java
Java 学习路线规划及项目案例中的技术栈应用解析
内容包括:**Java 17核心特性**(如sealed class、record)与模块化开发;Spring Boot 3 + Spring Cloud微服务架构,涉及响应式编程(WebFlux)、多数据库持久化(JPA、R2DBC、MongoDB);云原生技术**如Docker、Kubernetes及CI/CD流程;性能优化(GraalVM Native Image、JVM调优);以及前后端分离开发(Vue 3、Spring Boot集成)。通过全栈电商平台项目实战,掌握从后端服务(用户、商品、订单)到前端应用(Vue 3、React Native)的全流程开发。
478 9
|
设计模式 Java 程序员
【Java设计模式】用 披萨订购案例 详细讲解三种工厂模式(三)
【Java设计模式】用 披萨订购案例 详细讲解三种工厂模式(三)
【Java设计模式】用 披萨订购案例 详细讲解三种工厂模式(三)
|
设计模式 Java
【Java设计模式】用 披萨订购案例 详细讲解三种工厂模式(二)
【Java设计模式】用 披萨订购案例 详细讲解三种工厂模式(二)
【Java设计模式】用 披萨订购案例 详细讲解三种工厂模式(二)
|
7月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
347 1