二、 注入 Hook 钩子线程
- 类似于在计算机中使用文件夹管理文件,也可以使用线程组来管理线程. 在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组
- Thread 类有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组. JVM 在创建 main 线程时会为它指定一个线程组,因此每个 Java 线程都有一个线程组与之关联, 可以调用线程的 getThreadGroup()方法返回
- 线程组.
- 线程组开始是出于安全的考虑设计用来 区分不同的 Applet,然而 ThreadGroup 并未实现这一目标,在新开发的系统中,已经不常用线程 组, 现在一般会将一组相关的线程存入一个数组或一个集合中,如果仅仅是用来区分线程时,可以使用线程名称来区分, 多数情况下,可以忽略线程组
三、线程池 1、 什么是线程池
- 现在很多软件包括 MySQL, Zookeeper, kafka 等都存在 Hook 线程的校验机制, 目的是校验进程是否已启动,防止重复启动程序.
- Hook 线程也称为钩子线程, 当 JVM 退出的时候会执行 Hook 线程.经常在程序启动时创建一个.lock 文件, 用.lock 文件校验程序是否启动
- 在程序退出(JVM 退出)时删除该.lock 文件, 在 Hook 线程中除了防止重新启动进程外,还可以做资源释放, 尽量避免在 Hook 线程中进行 复杂的操作.
2、 JDK对线程池的支持
- 可以以 new Thread( () -> { 线程执行的任务 }).start(); 这种形式开启一个线程. 当 run()方法运行结束,线程对象会被 GC 释放.
- 在真实的生产环境中,可能需要很多线程来支撑整个应用,当线程数量非常多时 ,反而会耗尽 CPU 资源. 如果不对线程进行控制与管理,反而会影响程序的性能. 线程开销主要包括: 创建与启动线程的开销;线程销毁开销; 线程调度的开销; 线程数量受限 CPU 处理器数量.
- 线程池就是有效使用线程的一种常用方式. 线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池, 线程池将这些任务缓存在工作队列中, 线程池中的工作线程不断地从队列中取出任务并执行
JDK 提供了一套 Executor 框架,可以帮助开发人员有效的使用线程池,但效率很低,不使用,通过 Executor 框架的工具类 Executors 来实现我们可以创建三种类型的 ThreadPoolExecutor
3、核心线程池的底层实现查 看 Executors 工 具 类 中3个构造方式实现线程池 newCachedThreadPool(),
newSingleThreadExcecutor(),
newFixedThreadPool()源码:
- FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
Excutors 工 具 类 中 返 回 线 程池方法底层 都使用了 ThreadPoolExecutor 线程池,这些方法都是 ThreadPoolExecutor 线程池的封装.
各个参数含义:
- corePoolSize, 指定线程池中核心线程的数量
- maxinumPoolSize,指定线程池中最大线程数量
- keepAliveTime,当线程池线程的数量超过 corePoolSize 时,多余的空闲线程的存活时长,即空闲线程在多长时长内销毁
- unit, 是 keepAliveTime 时长单位
- workQueue,任务队列,把任务提交到该任务队列中等待执行
- threadFactory,线程工厂,用于创建线程
- handler 拒绝策略,当任务太多来不及处理时,如何拒绝
说明:
5、ThreadFactory
ThreadPoolExecutor 构造方法的最后一个参数指定了拒绝策略.当提交给线程池的任务量超过实际承载能力时,如何处理? 即线程池中 的线程已经用完了,等待队列也满了,无法为新提交的任务服务,可以 通过拒绝策略来处理这个问题. JDK提供了四种拒绝策略:
- AbortPolicy 策略,会抛出异常
- DiscardPolicy 直接丢弃这个无法处理的任务
- DiscardOldestPolicy 将任务队列中最老的任务丢弃,尝试再次提交新任务
- CallerRunsPolicy 策略,只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务
Executors 工具类提供的静态方法返回的线程池默认的拒绝策略是AbortPolicy 抛出异常,如果内置的拒绝策略无法满足实际需求,可以扩展RejectedExecutionHandler 接口
线程池中的线程从哪儿来的? 答案就是 ThreadFactory.
6、优化线程池大小ThreadFactory 是一个接口,只有一个用来创建线程的方法:Thread newThread(Runnable r); 当线程池中需要创建线程时就会调用该方法
7、线程池死锁线程池大小对系统性能是有一定影响的,过大或者过小都会无法 发挥最优的系统性能, 线程池大小不需要非常精确,只要避免极大或 者极小的情况即可, 一般来说,线程池大小需要考虑 CPU 数量,内存大 小等因素. 在书中给出一个估算线程池 大小的公式:
- 线程池大小 = CPU 的数量 * 目标 CPU 的使用率( 1 + 等待时间与计算时间的比)*
8、 线程池中的异常处理
- 如果在线程池中执行的 任务 A 在执行过程中又向线程池提交了 任务 B, 任务 B 添加到了线程池的等待队列中, 如果任务 A 的结束需 要等待任务 B 的执行结果. 就有可能会出现这种情况: 线程池中所有的工作线程都处于等待任务处理结果,而这些任务在阻塞队列中等待 执行, 线程池中没有可以对阻塞队列中的任务进行处理的线程,这种 等待会一直持续下去,从而造成死锁
- 适合给线程池提交相互独立的任务,而不是彼此依赖的任务. 对于 彼此依赖的任务,可以考虑分别提交给不同的线程池来执行.
在使用 ThreadPoolExecutor 进行 submit 提交任务时,有的任务抛出 了异常,但是线程池并没有进行提示,即线程池把任务中的异常给吃掉 了,可以把 submit 提交改为 execute 执行,也可以对 ThreadPoolExecutor 线程池进行扩展.对提交的任务进行包装:



