Java多线程案例——线程池

简介: 本来多进程就是解决并发编程的方案,但是进程有点太重量了(创建和销毁开销比较大),因此引入了线程,线程比进程要轻量很多。即便如此,如果在某些场景中需要频繁的创建和销毁线程,线程的创建销毁开销也就无法忽视了。

1.线程池是什么


1.1 线程池


本来多进程就是解决并发编程的方案,但是进程有点太重量了(创建和销毁开销比较大),因此引入了线程,线程比进程要轻量很多。即便如此,如果在某些场景中需要频繁的创建和销毁线程,线程的创建销毁开销也就无法忽视了。


为了解决这样的问题,我们引入了线程池:


使用线程的时候,不是说用的时候才创建,而是提前创建好,放到一个“池子”里(类似于字符串常量池),当我们需要使用线程的时候,直接从池子里面取一个线程出来,当我们不需要这个线程的时候,就把这个线程还回池子中(此时我们的操作就会比创建销毁线程效率更高)。


为了更好的理解线程池的概念我们引入下边这个例子:


在我们找工作时,面试结束之后会面临两个结果,1是已经通过oc(offer call),2是没有通过,但是公司不会直接告诉你你没有通过,而是完全不通知(公司会把你放到“人才储备池”里),此时假设A公司要招100人而目前只招够了60个人,剩下的40个名额公司将会从“人才储备池”中直接挑选hc(head count),直接发offer,从而避免了需要再次笔试面试等一系列的流程来招纳新人。


如果是真的创建/销毁线程,就会涉及到用户态和内核态的切换。

如果不是真的创建销毁线程,而是放到池子里,就相当于全在用户态搞定了这个事情。

在用户态操作会比较高效,切换到内核态以后操作就可能很低效。

线程池最大的好处就是减少每次启动、销毁线程的损耗


下边便是用户态和内核态的具体介绍


1.2 用户态与内核态


用户态就是应用程序执行的代码,内核态就是操作系统内核执行的代码,一般认为,用户态和内核态之间的切换,是一个开销比较大的操作。


比如我们去银行办理业务时,需要去复印一份文件,如果我们自己去复印,就能够很快的完成并交给工作人员,但如果让工作人员去完成这个打印文件的工作就会比较耗费时间(工作人员需要完成的工作很多,不能及时调度去完成打印文件工作)。

此时的我们便是用户态,工作人员便是内核态。


2.标准库中的线程池


在Java的标准库中,有下边这两者方式来创建线程池


2.1 ThreadPoolExecutor


微信图片_20230110221650.png

ThreadPoolExecutor的使用是相对复杂的,因为里边有很多的参数,ThreadPoolExecutor的构造方法的参数是什么意思,便是一道经典的面试题:()中的含义在下边引用中有介绍


  • corePoolSize :核心线程数(正式员工的数量)
  • maximumPoolSize:最大线程数(正式工+临时工)
  • keepAliveTime:线程保持活动的时间(描述临时工摸鱼可以摸多久)
  • unit:keepAliveTime的时间单位(ms,s,minute)
  • workQueue:阻塞队列,组织了线程池要执行的任务
  • threadFactory:线程的创建方式,通过这个参数,来设定不同的线程的创建方式
  • RejectedExecutionHandler:拒绝策略,当任务队列满了的时候,又来了新的任务,要根据具体的业务场景来选取具体的拒绝策略


我们可以把线程池想象成一个“公司”,公司里面的每个员工,就相当于是一个线程。

员工分为两类:


1,正式工(corePoolSize):签了劳动合同的,不能随便辞退

2,临时工:没有签劳动合同,随时可以踢掉


正式员工允许摸鱼(这样的线程即使是空闲,也不会被销毁)

临时工不允许摸鱼(如果临时工线程摸鱼的时间到了一定程度,就会被销毁)

如果我们要解决的任务场景任务量比较稳定,就可以设置corePoolSize和maximumPoolSize尽量接近(临时工就可以少一些)

如果我们要解决的任务场景任务量波动较大,既可以设置corePoolSize和maximumPoolSize相差大一些(临时工就可以多一些)


我们需要主要了解的是拒绝策略,在实际开发中,要根据具体的业务场景来选取具体的拒绝策略:


微信图片_20230110221646.png

以老板给员工分配工作的任务为例,来引出这四种拒绝策略:(员工相当于线程池)

1.老板给员工分配了很多任务,员工即使加班也无法完成工作,导致情绪崩溃,所有的工作都不干了。

2.老板给员工分配了很多任务,员工完不成的老板接手,如果老板也无法完成,就丢弃任务。

3.老板给员工安排了工作1、2、3、4,此时员工搞不定了,就可以先丢弃工作1(最旧的),去干其他的工作。

4.老板给员工安排了工作1、2、3、4,此时员工搞不定了,就可以先丢弃工作4(最新的),去干其他的工作。


1.AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;

2.CallerRunsPolicy:把任务交给添加此任务的线程来执行;

3.DiscardOldestPolicy:忽略最先加入队列的任务(最老的任务);

4.DiscardPolicy:忽略此任务(最新加入的任务)。


面试题:

线程池的数目如何确定?或者说设置成几比较合适?


我们并不能给出确定的具体个数,原因有以下两点:

1.主机的CPU配置不确定

2.程序的执行特点不确定


所以只有针对程序进行性能测试,分别给线程池设置成不同的数目,分别记录每种情况,才能最终选一个合适的配置。


2.2 Executors


由于ThreadPoolExecutor使用起来比较复杂,标准库又提供了Executors类,相当于对ThreadPoolExecutor又进行了一层封装,这个类相当于一个“工厂类”,通过这个类提供的一组工厂方法,就可以创建出不同风格的线程池实例了。(工厂模式在2.3中讲解)


Executors 创建线程池的几种方式:


  • newFixedThreadPool: 创建固定线程数的线程池(完全没有临时工的版本)
  • newCachedThreadPool: 创建线程数目可变的线程池.(完全没有正式员工,全是临时工)
  • newSingleThreadExecutor: 创建只包含单个线程的线程池. (只在特定场景下使用)
  • newScheduledThreadPool: 能够设定延时时间的线程池(插入的任务能够过一会再执行),相当于进阶版的定时器。


代码示例:


public class Demo2 {
    public static void main(String[] args) {
        //用标准库中的线程池,创建出一个线程池中的实例
        ExecutorService pool= Executors.newCachedThreadPool();
        //给这个实例里面加入一些任务
        for (int i = 0; i < 5; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

微信图片_20230110221641.png

2.3 工厂模式


Executors类的几种创建线程池的方式就是工厂模式的体现,工厂模式也是一种设计模式,和单例模式是并列的关系,工厂模式存在的意义,就是弥补构造方法的一些缺陷。


工厂模式主要是为了创建实例,正儿八经的创建实例是通过构造方法,但是构造方法的限制比较多:

1.构造方法的名字,必须是固定的(类名)

2.如果需要多个版本的构造方式,就只能依赖构造方法的重载,但是重载方法又要求方法参数的个数和类型不一致。

比如下边这个例子:


假设我们有个类Point,我们希望通过笛卡尔坐标和极坐标两种方式来构造这个点


class Point {
  public Point(double x,double y){...}       -->笛卡尔坐标
  public Point(double r,double a){...}       -->极坐标
}


但是这俩方法的参数个数和类型都一样,这个时候编译不了,而且构造方法的方法名是固定的,光看方法名并不知道咱们是使用哪种方式构造


为了解决这个问题:

我们就不使用构造方法来构造实例了,而是使用其他的方法来进行构造实例,这样的用来构造实例的方法就被称为“工厂方法”。

工厂方法其实就是普通的方法,这个工厂方法里面会调用对应的构造方法,并进行一些初始化的操作,并返回这个对象的实例。


实现线程池


  • 核心操作为 submit, 将任务加入线程池阻塞队列中,并创建线程
  • 使用一个 BlockingQueue 组织所有的任务
  • 一个线程池可以同时提交N个任务,对应的线程池中有M个线程来负责完成这N个任务,利用生产者消费者模型,把N个任务分配给M个线程
import java.util.concurrent.BlockingQueue;
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 m) {
        //在构造方法中,创建出M个线程,负责完成工作
        for (int i = 0; i < m; i++) {
            Thread t=new Thread(()-> {
                while (true) {
                    try {
                        Runnable runnable=queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}


检测实现的线程池:


public class Demo {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool=new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int taskId=i;//注意匿名内部类的变量捕获
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行当前任务:"+taskId+"当前线程:"+Thread.currentThread().getName());
                }
            });
        }
    }
}

微信图片_20230110221635.png

线程池的执行流程:


  1. 1.当新加入一个任务时,先判断当前线程数是否大于核心线程数,如果结果为false,则新建线程并执行任务;
  2. 2.如果结果为true,则判断任务队列是否已满,如果结果为false,则把任务添加到任务队列中,等待线程执行
  3. 3.如果结果为true,则判断当前线程数量是否超过最大线程数;如果结果为false,则新建线程执行此任务
  4. 4.如果结果为true,执行拒绝策略。
相关文章
|
6月前
|
自然语言处理 前端开发 Java
JBoltAI 框架完整实操案例 在 Java 生态中快速构建大模型应用全流程实战指南
本案例基于JBoltAI框架,展示如何快速构建Java生态中的大模型应用——智能客服系统。系统面向电商平台,具备自动回答常见问题、意图识别、多轮对话理解及复杂问题转接人工等功能。采用Spring Boot+JBoltAI架构,集成向量数据库与大模型(如文心一言或通义千问)。内容涵盖需求分析、环境搭建、代码实现(知识库管理、核心服务、REST API)、前端界面开发及部署测试全流程,助你高效掌握大模型应用开发。
689 5
|
6月前
|
前端开发 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)的全流程开发。
300 9
|
2月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
205 2
|
5月前
|
安全 Java API
Java 集合高级应用与实战技巧之高效运用方法及实战案例解析
本课程深入讲解Java集合的高级应用与实战技巧,涵盖Stream API、并行处理、Optional类、现代化Map操作、不可变集合、异步处理及高级排序等核心内容,结合丰富示例,助你掌握Java集合的高效运用,提升代码质量与开发效率。
282 0
|
5月前
|
安全 JavaScript Java
java Web 项目完整案例实操指南包含从搭建到部署的详细步骤及热门长尾关键词解析的实操指南
本项目为一个完整的JavaWeb应用案例,采用Spring Boot 3、Vue 3、MySQL、Redis等最新技术栈,涵盖前后端分离架构设计、RESTful API开发、JWT安全认证、Docker容器化部署等内容,适合掌握企业级Web项目全流程开发与部署。
428 0
|
6月前
|
缓存 算法 NoSQL
校招 Java 面试高频常见知识点深度解析与实战案例详细分享
《2025校招Java面试核心指南》总结了Java技术栈的最新考点,涵盖基础语法、并发编程和云原生技术三大维度: 现代Java特性:重点解析Java 17密封类、Record类型及响应式Stream API,通过电商案例演示函数式数据处理 并发革命:对比传统线程池与Java 21虚拟线程,详解Reactor模式在秒杀系统中的应用及背压机制 云原生实践:提供Spring Boot容器化部署方案,分析Spring WebFlux响应式编程和Redis Cluster缓存策略。
183 0
|
6月前
|
人工智能 Java API
Java 生态大模型应用开发全流程实战案例与技术路径终极对决
在Java生态中开发大模型应用,Spring AI、LangChain4j和JBoltAI是三大主流框架。本文从架构设计、核心功能、开发体验、性能扩展性、生态社区等维度对比三者特点,并结合实例分析选型建议。Spring AI适合已有Spring技术栈团队,LangChain4j灵活性强适用于学术研究,JBoltAI提供开箱即用的企业级解决方案,助力传统系统快速AI化改造。开发者可根据业务场景和技术背景选择最适合的框架。
1245 2
|
6月前
|
存储 Java 数据安全/隐私保护
Java技术栈揭秘:Base64加密和解密文件的实战案例
以上就是我们今天关于Java实现Base64编码和解码的实战案例介绍。希望能对你有所帮助。还有更多知识等待你去探索和学习,让我们一同努力,继续前行!
501 5
|
6月前
|
缓存 NoSQL Java
校招 Java 面试常见知识点及实战案例全解析
本文全面解析了Java校招面试中的常见知识点,涵盖Java新特性(如Lambda表达式、、Optional类)、集合框架高级应用(线程安全集合、Map性能优化)、多线程与并发编程(线程池配置)、JVM性能调优(内存溢出排查、垃圾回收器选择)、Spring与微服务实战(Spring Boot自动配置)、数据库与ORM框架(MyBatis高级用法、索引优化)、分布式系统(分布式事务、缓存应用)、性能优化(接口优化、高并发限流)、单元测试与代码质量(JUnit 5、Mockito、JaCoCo)以及项目实战案例(电商秒杀系统、社交消息推送)。资源地址: [https://pan.quark.cn/s
213 4
|
6月前
|
缓存 Java API
Java 集合容器实操技巧与案例详解
本教程基于Java 8+新特性和现代开发实践,深入讲解Java集合容器的实操技巧。通过具体场景演示Stream API数据处理、ConcurrentHashMap并发控制、LinkedHashMap实现LRU缓存、TreeSet自定义排序等高级特性。同时涵盖computeIfAbsent优化操作、EnumMap专用集合使用、集合统计与运算(交集、并集、差集)等内容。代码示例丰富,助力掌握高效编程方法。[点击获取完整代码](https://pan.quark.cn/s/14fcf913bae6)。
95 0