面试题 -- 如何设计一个线程池

以前,我总觉得的买一件东西,做一件事,或者从某一个时间节点开始,我的生命就会发生转折,一切就会无比顺利,立马变厉害。但是,事实上并不是如此。我不可能马上变厉害,也不可能一口吃成一个胖子。看一篇文章也不能让你从此走上人生巅峰,越来越相信,这是一个长期的过程,只有量变引起质变,纵使缓慢,驰而不息。

如何设计一个线程池?

三个步骤

这是一个常见的问题,如果在比较熟悉线程池运作原理的情况下,这个问题并不难。设计实现一个东西,三步走:是什么?为什么?怎么做?

线程池是什么?

线程池使用了池化技术,将线程存储起来放在一个 “池子”(容器)里面,来了任务可以用已有的空闲的线程进行处理, 处理完成之后,归还到容器,可以复用。如果线程不够,还可以根据规则动态增加,线程多余的时候,亦可以让多余的线程死亡。

为什么要用线程池?

实现线程池有什么好处呢?

  • 降低资源消耗:池化技术可以重复利用已经创建的线程,降低线程创建和销毁的损耗。
  • 提高响应速度:利用已经存在的线程进行处理,少去了创建线程的时间
  • 管理线程可控:线程是稀缺资源,不能无限创建,线程池可以做到统一分配和监控
  • 拓展其他功能:比如定时线程池,可以定时执行任务

需要考虑的点

那线程池设计需要考虑的点:

  • 线程池状态:

    • 有哪些状态?如何维护状态?
  • 线程

    • 线程怎么封装?线程放在哪个池子里?
    • 线程怎么取得任务?
    • 线程有哪些状态?
    • 线程的数量怎么限制?动态变化?自动伸缩?
    • 线程怎么消亡?如何重复利用?
  • 任务

    • 任务少可以直接处理,多的时候,放在哪里?
    • 任务队列满了,怎么办?
    • 用什么队列?

如果从任务的阶段来看,分为以下几个阶段:

  • 如何存任务?
  • 如何取任务?
  • 如何执行任务?
  • 如何拒绝任务?

线程池状态

状态有哪些?如何维护状态?

状态可以设置为以下几种:

  • RUNNING:运行状态,可以接受任务,也可以处理任务
  • SHUTDOWN:不可以接受任务,但是可以处理任务
  • STOP:不可以接受任务,也不可以处理任务,中断当前任务
  • TIDYING:所有线程停止
  • TERMINATED:线程池的最后状态

各种状态之间是不一样的,他们的状态之间变化如下:

而维护状态的话,可以用一个变量单独存储,并且需要保证修改时的原子性,在底层操作系统中,对int的修改是原子的,而在32位的操作系统里面,对double,long这种64位数值的操作不是原子的。除此之外,实际上JDK里面实现的状态和线程池的线程数是同一个变量,高3位表示线程池的状态,而低29位则表示线程的数量。

这样设计的好处是节省空间,并且同时更新的时候有优势。

线程相关

线程怎么封装?线程放在哪个池子里?

线程,即是实现了Runnable接口,执行的时候,调用的是start()方法,但是start()方法内部编译后调用的是 run() 方法,这个方法只能调用一次,调用多次会报错。因此线程池里面的线程跑起来之后,不可能终止再启动,只能一直运行着。既然不可以停止,那么执行完任务之后,没有任务过来,只能是轮询取出任务的过程

线程可以运行任务,因此封装线程的时候,假设封装成为 Worker, Worker里面必定是包含一个 Thread,表示当前线程,除了当前线程之外,封装的线程类还应该持有任务,初始化可能直接给予任务,当前的任务是null的时候才需要去获取任务。

可以考虑使用 HashSet 来存储线程,也就是充当线程池的角色,当然,HashSet 会有线程安全的问题需要考虑,那么我们可以考虑使用一个可重入锁比如 ReentrantLock,凡是增删线程池的线程,都需要锁住。

    private final ReentrantLock mainLock = new ReentrantLock();

线程怎么取得任务?

(1)初始化线程的时候可以直接指定任务,譬如Runnable firstTask,将任务封装到 worker 中,然后获取 worker 里面的 threadthread.run()的时候,其实就是 跑的是 worker 本身的 run() 方法,因为 worker 本身就是实现了 Runnable 接口,里面的线程其实就是其本身。因此也可以实现对 ThreadFactory 线程工厂的定制化。

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        final Thread thread;
        Runnable firstTask;

        ...

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            // 从线程池创建线程,传入的是其本身
            this.thread = getThreadFactory().newThread(this);
        }
    }

(2)运行完任务的线程,应该继续取任务,取任务肯定需要从任务队列里面取,要是任务队列里面没有任务,由于是阻塞队列,那么可以等待,如果等待若干时间后,仍没有任务,倘若该线程池的线程数已经超过核心线程数,并且允许线程消亡的话,应该将该线程从线程池中移除,并结束掉该线程。

取任务和执行任务,对于线程池里面的线程而言,就是一个周而复始的工作,除非它会消亡。

线程有哪些状态?

现在我们所说的是Java中的线程Thread,一个线程在一个给定的时间点,只能处于一种状态,这些状态都是虚拟机的状态,不能反映任何操作系统的线程状态,一共有六种/七种状态:

  • NEW:创建了线程对象,但是还没有调用Start()方法,还没有启动的线程处于这种状态。

  • Running:运行状态,其实包含了两种状态,但是Java线程将就绪和运行中统称为可运行

    • Runnable:就绪状态:创建对象后,调用了start()方法,该状态的线程还位于可运行线程池中,等待调度,获取CPU的使用权
      • 只是有资格执行,不一定会执行
      • start()之后进入就绪状态,sleep()结束或者join()结束,线程获得对象锁等都会进入该状态。
      • CPU时间片结束或者主动调用yield()方法,也会进入该状态
    • Running :获取到CPU的使用权(获得CPU时间片),变成运行中
  • BLOCKED :阻塞,线程阻塞于锁,等待监视器锁,一般是Synchronize关键字修饰的方法或者代码块

  • WAITING :进入该状态,需要等待其他线程通知(notify)或者中断,一个线程无限期地等待另一个线程。

  • TIMED_WAITING :超时等待,在指定时间后自动唤醒,返回,不会一直等待

  • TERMINATED :线程执行完毕,已经退出。如果已终止再调用start(),将会抛出java.lang.IllegalThreadStateException异常。

image-20210509224848865

线程的数量怎么限制?动态变化?自动伸缩?

线程池本身,就是为了限制和充分使用线程资的,因此有了两个概念:核心线程数,最大线程数。

要想让线程数根据任务数量动态变化,那么我们可以考虑以下设计(假设不断有任务):

  • 来一个任务创建一个线程处理,直到线程数达到核心线程数。
  • 达到核心线程数之后且没有空闲线程,来了任务直接放到任务队列。
  • 任务队列如果是无界的,会被撑爆。
  • 任务队列如果是有界的,任务队列满了之后,还有任务过来,会继续创建线程处理,此时线程数大于核心线程数,直到线程数等于最大线程数。
  • 达到最大线程数之后,还有任务不断过来,会触发拒绝策略,根据不同策略进行处理。
  • 如果任务不断处理完成,任务队列空了,线程空闲没任务,会在一定时间内,销毁,让线程数保持在核心线程数即可。

由上面可以看出,主要控制伸缩的参数是核心线程数最大线程数,任务队列,拒绝策略

线程怎么消亡?如何重复利用?

线程不能被重新调用多次start(),因此只能调用一次,也就是线程不可能停下来,再启动。那么就说明线程复用只是在不断的循环罢了。

消亡只是结束了它的run()方法,当线程池数量需要自动缩容的,就会让一部分空闲的线程结束。

而重复利用,其实是执行完任务之后,再去去任务队列取任务,取不到任务会等待,任务队列是一个阻塞队列,这是一个不断循环的过程。

任务相关

任务少可以直接处理,多的时候,放在哪里?

任务少的时候,来了直接创建,赋予线程初始化任务,就可开始执行,任务多的时候,把它放进队列里面,先进先出。

任务队列满了,怎么办?

任务队列满了,会继续增加线程,直到达到最大的线程数。

用什么队列?

一般的队列,只是一个有限长度的缓冲区,要是满了,就不能保存当前的任务,阻塞队列可以通过阻塞,保留出当前需要入队的任务,只是会阻塞等待。同样的,阻塞队列也可以保证任务队列没有任务的时候,阻塞当前获取任务的线程,让它进入wait状态,释放cpu的资源。因此在线程池的场景下,阻塞队列其实是比较有必要的。

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。如果有帮助,顺手点个赞,对我,是莫大的鼓励和认可。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
附有面试讲解视频,不是网盘,下载既有视频,屡试不爽的面试宝典。 Java面试题01.面试的整体流程 Java面试题02.java的垮平台原理 Java面试题03.搭建一个java的开发环境 Java面试题04.java中int占几个字节 Java面试题05.java面向对象的特征 Java面试题06.装箱和拆箱 Java面试题07.==和equals的区别 Java面试题08.String Java面试题09.讲一下java中的集合 Java面试题10.ArrayList 和LinkedList的区别 Java面试题11.HashMap和HashTable的区别 Java面试题12.实现一个拷贝文件的工具类要使用字节流还是字符串 Java面试题13.线程的的实现方式?怎么启动线程?怎么区分线程? Java面试题14.线程并发库和线程池的作用 Java面试题15.设计模式和常用的设计模式 Java面试题16.http get post请求的区别 Java面试题17.说说你对Servlet的理解 Java面试题18.Servlet的生命周期 Java面试题19.Servlet中forward和redirect的区别 Java面试题20.jsp和Servlet的相同点和不同点 Java面试题21.内置对象和四大作用域和页面传值 Java面试题22.Session和Cookie的区别和使用场景 Java面试题23.mvc模式和mvc各部分的实现 Java面试题24.数据库分类和常用数据库 Java面试题25.关系型数据库的三范式 Java面试题26.事务的四大特征 Java面试题27.mysql数据库最大连接数 Java面试题28.mysql和oracle的分页语句 Java面试题29.触发器的使用场景? Java面试题30.存储过程的优点 Java面试题31.jdbc调用存储过程 Java面试题32.简单说一下你对jdbc的理解 Java面试题33.写一个jdbc的访问oracle的列子 Java面试题34.jdbc中preparedStatement比Statement的好处 Java面试题35.数据库连接池的作用 Java面试题36.HTML Java面试题37.简单介绍了一下Ajax Java面试题38.js和JQuery的关系 Java面试题39.jQuery中的常用选择器 Java面试题40.jQuery中页面加载完毕事件 Java面试题41.jQuery中Ajax和原生js实现Ajax的关系 Java面试题42.简单说一下html5 Java面试题43.简单说一下css3 Java面试题44.bootstrap的是什么 Java面试题45.什么是框架 Java面试题46.简单介绍一下MVC模式 Java面试题47.简单说一下对mvc框架的理解 Java面试题48.struts2的执行流程或者struts2的原理 Java面试题49.Struts2的拦截器是什么 Java面试题50.Spring MVC的执行流程 Java面试题51.SpringMVC和Struts2的不同 Java面试题52.简单介绍一下Spring或者Spring的两大核心 Java面试题53.AOP是什么?都用它做什么? Java面试题54.Spring事务的传播特性和隔离级别 Java面试题55.ORM是什么?ORM框架是什么? Java面试题56.ibatis和hibernate有什么不同 Java面试题57.hibernate对象状态及其转换 Java面试题58:hibernate的缓存 Java面试题59.webservice的使用场景 Java面试题60.activiti的简单介绍 Java面试题61.linux的使用场景 Java面试题62.linux常用命令 Java面试题63:怎么操作linux服务器 Java面试题64:有没有使用过云主机 Java面试题65:数据库优化方面的事情 Java面试题66:如果查询和定位慢查询 Java面试题67:数据库优化之数据库表设计遵循范式 Java面试题68:选择合适的数据库引擎 Java面试题69:选择合适的索引 Java面试题70:使用索引的一些技巧 Java面试题71:数据库优化之分表 Java面试题72:数据库的读写分离 Java面试题73:数据库优化之缓存 Java面试题74:sql语句优化小技巧 Java面试题75:批量插入几百万条数据 Java面试题76:有没有使用过redis Java面试题77:redis的使用场景 Java面试题78:redis存储对象的方式 Java面试题79:redis数据淘汰机制 Java面试题80:java访问redis级redis集群 ......
### 回答1: 好的,以下是一些2023年Java面试题和学习计划建议: Java面试题: 1. 什么是Java虚拟机(JVM)?JVM的组成部分是什么? 2. 什么是Java中的反射?反射的优缺点是什么? 3. 什么是Java中的注解?它们有哪些用途? 4. 什么是Java中的Lambda表达式?如何使用它们? 5. 什么是Java中的异常处理?Java中有哪些类型的异常? 6. 什么是Java中的多线程?如何创建和控制线程? 7. 什么是Java中的集合框架?Java中有哪些集合类? 8. 什么是Java中的IO和NIO?它们有什么区别? 9. 什么是Java中的泛型?如何使用它们? 10. 什么是Java中的类加载器?它们有哪些类型? 学习计划建议: 1. 学习Java基础知识,包括语法、数据类型、运算符等。 2. 深入了解Java中的面向对象编程思想,掌握类、对象、继承、多态等概念。 3. 学习Java中的集合框架,包括List、Set、Map等集合类的使用。 4. 学习Java中的异常处理机制,掌握try-catch-finally结构以及自定义异常的使用。 5. 学习Java中的多线程编程,包括线程的创建、启动、等待和同步等概念。 6. 学习Java中的IO和NIO编程,包括文件读写、网络编程等。 7. 学习Java中的反射和注解机制,了解它们的作用和使用方法。 8. 学习Java中的Lambda表达式和函数式编程思想,掌握Lambda表达式的语法和使用方法。 9. 学习Java中的泛型编程,包括泛型类和泛型方法的使用。 10. 学习Java中的类加载机制,包括类加载器的种类、类加载的顺序等。 以上是一些建议,您可以根据自己的情况进行调整和修改。最重要的是不断练习,多做一些实际的项目或者练习题目,以提高自己的编程能力和实践经验。 ### 回答2: 2023年的Java面试题和学习计划如下: 1. Java核心知识 - Java语言基础:变量、数据类型、运算符、控制结构等 - 面向对象编程:封装、继承、多态、抽象类、接口等 - 异常处理机制:try-catch-finally、自定义异常等 - 集合框架:List、Set、Map等常用集合操作 - IO操作:文件读写、序列化等 2. Java高级特性 - 多线程和并发:线程生命周期、同步、锁机制、线程池等 - 反射和动态代理:Class类、反射调用、动态代理的概念和用法 - 泛型:泛型类型、泛型方法、通配符等 - 注解:定义和使用注解、元注解等 - Lambda表达式和函数式接口:Lambda表达式的语法和用法、函数式接口定义和使用 3. 数据库相关 - SQL语法和基本操作:增删改查、连接查询、聚合函数等 - JDBC编程:连接数据库、CRUD操作、事务管理等 - ORM框架:例如Hibernate、MyBatis的使用和原理 4. web开发相关 - Servlet和JSP:Servlet生命周期、请求响应过程、Session管理等 - Spring框架:IOC、AOP、MVC等基本概念和使用 - Spring Boot:自动配置、常用注解、项目搭建等 - RESTful API设计和使用:HTTP方法、资源定义、状态码等 学习计划如下: 1. 确定学习目标:了解面试要求和考察重点,制定合理的时间表 2. 学习基础知识:通过阅读相关教材、网上教程等掌握Java的基础语法和核心知识 3. 实践项目:通过完成一些小项目或者任务来巩固理论知识,提高编程能力 4. 阅读原理文档:深入理解Java相关的原理和机制,如JVM、多线程等 5. 练习面试题:刷题、模拟面试,提高解决问题的能力和应对压力的能力 6. 参考书籍和培训班:选择一些经典的Java开发书籍和参加培训班提高自己的学习效果 7. 学习交流:参加技术社区、技术论坛等进行学习交流,与其他Java开发者共同进步 以上是2023年Java面试题的整理和一个较为综合的学习计划,具体根据个人情况进行调整。成功的关键在于持续努力、实践和不断积累经验。祝你在2023年的Java面试中取得好成绩! ### 回答3: 2023年的Java面试题分为三个主要方面:基础知识、核心概念和高级特性。下面是一个学习计划的建议,以帮助你准备面试。 1. 基础知识: - 熟悉Java语言的基本语法、数据类型、运算符和控制流程语句。 - 理解面向对象编程的概念,包括类、对象、继承、多态和封装。 - 学习异常处理机制、输入输出流和Java集合框架。 2. 核心概念: - 理解Java内存管理机制,包括堆和栈的区别、垃圾回收和内存泄漏。 - 学习多线程编程,包括线程的创建、同步、通信和线程池的使用。 - 熟悉Java的反射机制和注解,了解它们在框架和库中的应用。 3. 高级特性: - 学习Java的网络编程,包括Socket编程、HTTP传输和RESTful服务。 - 掌握常用的设计模式,如单例模式、工厂模式和观察者模式。 - 了解Java的新特性,如Lambda表达式、Stream API和模块化系统。 学习计划建议如下: - 设定一个合理的学习时间表,每天定期进行学习,以确保学习的连续性。 - 阅读相关的Java教程和参考资料,例如官方文档和经典的Java编程书籍。 - 实践编写Java代码,通过解决实际问题来加深对知识的理解和记忆。 - 参加Java开发的线上课程、培训或是参与开源项目,积累实践经验。 - 利用在线资源,如编程网站和论坛,与其他学习者交流、分享和提问。 - 制定一个自我评估计划,定期检查自己的学习进度并找出需要加强的领域。 通过系统学习和实践,逐步提升自己的Java编程能力,并不断完善面试题的准备。记住,面试并不仅仅是回答问题,还要展示自己的编程思维和解决问题的能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值