2024年10月

Title translation

给定一棵
\(n\)
个点的树,
初始全是白点

要做
\(n\)
步操作,每一次选定一个
与一个黑点相隔一条边的白点
,将它染成
黑点
,然后获得该白点
被染色前
所在的
白色联通块大小
的权值。
第一次操作可以任意选点
,求可获得的
最大权值

Solution

如何让这道题秒降绿题呢?

先简化一下题意:

给定一个
\(n\)
个点的树,请求出
一个结点
,使得
以这个结点为根
时,所有结点的
深度之和最大
,求这个
深度

这不和
P3478
一样吗!

为何是这样?

我们换个方向思考:

因为每一次是
把一个与一个黑点相隔一条边的白点染成黑点
,考虑一个节点
\(v\)
,当这棵树的
根节点

\(v\)

\(v\)
的父亲、
\(v\)
的父亲的父亲……的时候,
\(v\)
都会给它们
所在的联通块大小
贡献
\(1\)
。那它最多
贡献多少个
\(1\)
呢?其实就是它自己的
深度

那题目就简化成了刚刚的那样……

Code

	#include <bits/stdc++.h>
	#define int long long
	using namespace std;
	const int Maxn=1e6+5;
	int n;vector<int>e[Maxn];
	int Size[Maxn],Dep[Maxn];
	int f[Maxn];
	void dfs1(int u,int fa){
		Size[u]=1;Dep[u]=Dep[fa]+1;
		for(auto v:e[u]){
			if(v==fa)continue;
			dfs1(v,u);
			Size[u]+=Size[v];
		}
	}
	void dfs2(int u,int fa){
		for(auto v:e[u]){
			if(v==fa)continue;
			f[v]=f[u]+n-2*Size[v];
			dfs2(v,u);
		}
	}
	signed main(){
		ios::sync_with_stdio(0);
		cin.tie(0); cout.tie(0);
		cin>>n;
		for(int i=1;i<=n-1;i++){
			int u,v;cin>>u>>v;
			e[u].push_back(v);
			e[v].push_back(u);
		}
		dfs1(1,0);
		for(int i=1;i<=n;i++)f[1]+=Dep[i];
		dfs2(1,0);int maxn=INT_MIN,id;
		for(int i=1;i<=n;i++)
			if(f[i]>maxn)maxn=f[i],id=i;
		cout<<maxn;
		return 0;
	}


大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是
瑞萨RA系列FSP固件库里的外设驱动

上一篇文章
《瑞萨RA8系列高性能MCU开发初体验》
,痞子衡带大家快速体验了一下瑞萨 MCU 开发三大件(开发环境e² studio、软件包FSP、评估板EK),其中软件包 FSP 为何不叫更通用的 SDK,痞子衡特地留了伏笔,今天就让我们分析一下这个 FSP 到底是什么来头?(本篇主要分析其中外设驱动部分)

一、固件包架构对比

我们尝试对比意法半导体、恩智浦以及瑞萨三家的固件包来看看它们的架构差异。

1.1 ST STM32Cube MCU Packages

首先来看在固件包生态上建立得比较早的意法半导体,它家固件包全称 STM32Cube MCU Packages,从下往上一共四层(MCU硬件、BSP&HAL驱动、Middleware、App),另外 CMSIS 地位与 Milddeware 平齐,说明意法认为 CMSIS 是相对通用的中间层代码。

其中我们主要关注 BSP 和 HAL 驱动,BSP 即板级器件(比如 Codec、各种传感器等)相关的驱动,HAL 则是 MCU 片内外设驱动,
在意法架构里 BSP 和 HAL 是相同的层级
,但其实我们知道 BSP 功能也要基于 HAL 驱动来具体实现。

关于这里的 HAL 驱动,有必要多展开一些,最早期的时候意法半导体主推得是标准库(Standard Peripheral Libraries,简称 SPL),目前已经不再维护更新,现在主推 HAL 库(Hardware Abstraction Layer)和 LL 库(Low-Layer),所以架构图里 HAL 实际上是统指 HAL 库和 LL 库,三者关系简单理解就是 SPL = HAL + LL。

底层库文件:xxxMCU_ll_xxxPeripheral.c/h,提供的 API 主要是对于片内外设寄存器的单一设置操作,API 命名为 LL_PERIPHERAL_xxxAction()
           原型示例:ErrorStatus LL_USART_Init(USART_TypeDef *USARTx, LL_USART_InitTypeDef *USART_InitStruct)
抽象层文件:xxxMCU_hal_xxxPeripheral.c/h,提供的 API 主要是对于片内外设具体功能的综合操作,API 命名为 HAL_PERIPHERAL_xxxFunc()
           原型示例:HAL_StatusTypeDef HAL_USART_Init(USART_HandleTypeDef *husart)
标准库文件:xxxMCU_xxxPeripheral.c/h,提供的 API 同时包含上述 LL 和 HAL 功能(但是实现丰富度稍低),API 命名为 PERIPHERAL_xxxFunc/Action()
           原型示例:void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct)

1.2 NXP MCUXpresso-SDK

再来看痞子衡东家恩智浦半导体,固件包全称 MCUXpresso-SDK,从下往上一共五层(MCU硬件、CMSIS、HAL驱动、Middleware&BSP、App),这样的分层方式其实是 ARM 公司比较推荐的,与意法见解不同的是,这里 CMSIS 紧靠 MCU 硬件层,显然恩智浦认为 CMSIS 也是底层基础代码。

恩智浦架构里 BSP 和 HAL 不在同一层,清晰地表明了 BSP 是在 HAL 基础之上的代码
。恩智浦的 HAL 驱动比较像意法半导体的早期标准库 SPL,但是 API 功能丰富度远超 SPL。

抽象层文件:fsl_xxxPeripheral.c/h,提供的 API 同时包含片内外设寄存器的单一设置操作以及外设具体功能的综合操作,API 命名为 PERIPHERAL_xxxFunc/Action()
           原型示例:status_t LPUART_Init(LPUART_Type *base, const lpuart_config_t *config, uint32_t srcClock_Hz);

1.3 Renesas RA FSP

最后来看瑞萨家的 FSP,没有表现出明显的层次结构,但是能看出
瑞萨架构里 BSP 和 HAL 不在同一层,且 BSP 在 HAL 之下
。这里的 BSP 也包含了 CMSIS,显然瑞萨认为 BSP 既包含了 MCU 内核相关基础硬件也包含板级器件硬件驱动。

瑞萨 HAL 驱动设计得比较有意思,不同于意法以及恩智浦,它对于外设功能抽象更为看重(也可以理解为更面向对象),为此额外创建了一个 r_xxxModule_api.h 文件,里面定义了 API 原型,原型重点强调外设的通用功能行为,而忽略具体外设的操作细节和差异,这个我们下一节会细聊。

抽象层文件:r_xxxModule_api.h,定义统一的外设模块驱动 API 原型结构体,适用于同类功能的不同外设情况(比如 UART 功能既可能是 SCI_USART 也可能是 SCI_UART 或者其它)
           r_xxxPeripheral.c/h,提供的 API 主要包含片内外设具体功能的综合操作,API 命名为 R_PERIPHERAL_xxxFunc()
           原型示例:fsp_err_t R_SCI_B_UART_Open (uart_ctrl_t * const p_api_ctrl, uart_cfg_t const * const p_cfg)

二、FSP里的外设驱动结构

在上篇文章示例工程 lpm_ek_ra8m1_ep 里,我们发现有如下 ra 文件夹,这就是 FSP 包相关的源文件,我们结合具体源文件来分析:

2.1 头文件与启动文件

首先在系统文件(头文件与启动文件)命名上,三家小有差异,不过差异最大的是型号头文件里的外设寄存器定义,这和后面的 HAL 驱动里代码实现息息相关。

文件类型 意法半导体 恩智浦半导体 瑞萨电子
系列头文件 stm32xxxx.h fsl_device_registers.h renesas.h
型号头文件 xxxMcu.h xxxMCU.h xxxMCU.h
启动文件 startup_xxxMcu.s startup_xxxMcu.s startup.c
初始化文件 system_xxxMcu.c/h system_xxxMcu.c/h system.c/h

在头文件里的外设寄存器原型定义上,意法和恩智浦是一致的,每个寄存器均用一个 uint32_t 类型存储,而瑞萨则用联合体(union)来存储每个寄存器,这样不仅能整体访问该寄存器,还能按 bit field 访问寄存器中的具体功能位。

除此以外,三家均为外设寄存器的单/多 bit 功能位做了 mask 和 pos 定义便于代码做相关位操作。而为了便于对多 bit 功能位区域的赋值,恩智浦和意法还有额外定义(以达到瑞萨用 union 定义外设寄存器原型的效果)。

xxxPERIPHERAL_xxxREGISTER_xxxFunc_Msk/MASK
xxxPERIPHERAL_xxxREGISTER_xxxFunc_Pos/SHIFT
// 恩智浦额外定义了如下宏用于赋值多 bit 功能位区域
xxxPERIPHERAL_xxxREGISTER_xxxFunc()
// 意法则直接用多个宏来辅助置位多 bit 功能位区域的每一位
xxxPERIPHERAL_xxxREGISTER_xxxFunc
xxxPERIPHERAL_xxxREGISTER_xxxFunc_0
xxxPERIPHERAL_xxxREGISTER_xxxFunc_1
...

2.2 HAL驱动文件

关于 HAL 驱动本身代码结构部分,我们主要分析三家 API 第一个形参定义即可知主要差别,其中恩智浦和意法 LL 库均是指向外设原型结构体的指针,而意法 HAL 库和瑞萨则是指向自定义外设控制块的指针,前者偏底层,后者偏应用层。

参数 意法半导体 恩智浦半导体 瑞萨电子
第一个 LL库:PERIPHERAL_TypeDef *
HAL库:PERIPHERAL_HandleTypeDef *
PERIPHERAL_Type * module_ctrl_t * const

前面痞子衡说了瑞萨多了一个 r_xxxModule_api.h 文件,我们就以 SCI 外设为例,其对应 r_uart_api.h 文件,该文件里定义了如下标准 API 动作集,这些动作不太像一般的外设驱动函数名(比如 init, deinit 等),更像是应用层动作。

/** Shared Interface definition for UART */
typedef struct st_uart_api
{
    fsp_err_t (* open)(uart_ctrl_t * const p_ctrl, uart_cfg_t const * const p_cfg);
    fsp_err_t (* read)(uart_ctrl_t * const p_ctrl, uint8_t * const p_dest, uint32_t const bytes);
    fsp_err_t (* write)(uart_ctrl_t * const p_ctrl, uint8_t const * const p_src, uint32_t const bytes);
    fsp_err_t (* baudSet)(uart_ctrl_t * const p_ctrl, void const * const p_baudrate_info);
    fsp_err_t (* infoGet)(uart_ctrl_t * const p_ctrl, uart_info_t * const p_info);
    fsp_err_t (* communicationAbort)(uart_ctrl_t * const p_ctrl, uart_dir_t communication_to_abort);
    fsp_err_t (* callbackSet)(uart_ctrl_t * const p_ctrl, void (* p_callback)(uart_callback_args_t *),
                              void const * const p_context, uart_callback_args_t * const p_callback_memory);
    fsp_err_t (* close)(uart_ctrl_t * const p_ctrl);
    fsp_err_t (* readStop)(uart_ctrl_t * const p_ctrl, uint32_t * remaining_bytes);
} uart_api_t;

而在 r_sci_b_uart.c 文件里,将基于 SCI 外设实现的 UART 驱动函数对 uart_api_t 做了实例化,这样上层应用可以仅调用 uart_api_t 里的接口实现具体功能,而不必在意这些接口具体由哪个类型的外设来实现的。这样设计的好处是便于代码跨外设(跨MCU),移植起来方便,缺点是限制了 API 丰富度,难以展现外设间的差异化特性。

/* UART on SCI HAL API mapping for UART interface */
const uart_api_t g_uart_on_sci_b =
{
    .open               = R_SCI_B_UART_Open,
    .close              = R_SCI_B_UART_Close,
    .write              = R_SCI_B_UART_Write,
    .read               = R_SCI_B_UART_Read,
    .infoGet            = R_SCI_B_UART_InfoGet,
    .baudSet            = R_SCI_B_UART_BaudSet,
    .communicationAbort = R_SCI_B_UART_Abort,
    .callbackSet        = R_SCI_B_UART_CallbackSet,
    .readStop           = R_SCI_B_UART_ReadStop,
};

至此,瑞萨RA系列FSP固件库里的外设驱动痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的
博客园主页

CSDN主页

知乎主页

微信公众号
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。

前后端分离应用
指的是将应用的
前端部分
(用户界面与交互逻辑)和
后端部分
(业务逻辑、数据处理、服务器响应)拆分成独立的模块,各自通过 API 进行通信。这种架构设计的目的是提高开发效率、增强可扩展性和灵活性,使前端和后端能够独立开发、部署和维护。

一、传统的前后端耦合应用 vs 前后端分离应用

  1. 传统的前后端耦合应用


    • 特点
      :前端与后端代码紧密耦合,通常是服务端渲染的页面应用。前端页面(HTML、CSS、JavaScript)与后端代码(如 Java、PHP、Python 等)混合在一起,服务器端负责处理请求、生成 HTML 并发送给客户端。
    • 缺点

      • 开发效率低
        :前端开发和后端开发常常互相依赖,无法完全并行工作。
      • 难以复用
        :页面渲染逻辑与后端逻辑耦合在一起,不容易将后端的逻辑复用到其他客户端(如移动端)。
      • 扩展性差
        :随着前后端逻辑的复杂度增加,代码维护和扩展变得更加困难。
  2. 前后端分离应用


    • 特点
      :前后端通过标准化的 API 进行通信,前端由单独的团队或人员开发,主要负责界面展示与用户交互,后端专注于业务逻辑、数据处理和提供 API 服务。前端和后端可以独立开发、测试和部署。
    • 优点

      • 独立开发
        :前后端开发可以并行工作,前端与后端通过 API 定义进行解耦,前端开发人员只需调用后端提供的 API,而不关心后端实现细节。
      • 灵活性和扩展性
        :后端服务可以为多个前端(如 Web、移动应用、小程序等)提供统一的 API 服务,且前后端可以各自独立扩展。
      • 更好的用户体验
        :前端可以构建更丰富的交互体验,例如单页应用(SPA)使用框架如 React、Vue、Angular 等,使得页面部分刷新,提供更流畅的用户交互。


二、前后端分离的核心要素

  1. 前端部分


    • 职责
      :前端主要负责用户界面(UI)展示和用户交互逻辑。它是运行在用户浏览器中的应用,利用 HTML、CSS、JavaScript 等技术创建页面并与用户交互。
    • 前端技术栈
      :主流的前端框架包括 React、Vue.js、Angular 等,通常使用 Webpack 等构建工具将代码打包成静态资源。
    • 典型实现
      :通过浏览器向后端发送 API 请求(如 RESTful API 或 GraphQL 请求),获取数据后更新页面,前端本身可以托管在 CDN 或静态资源服务器上。
  2. 后端部分


    • 职责
      :后端主要负责处理业务逻辑、数据存储、认证与授权、负载均衡等。它通常由 Java、Node.js、Python、Go 等后端语言编写,提供面向前端的 API 接口。
    • 后端技术栈
      :后端常用的技术包括 Spring Boot(Java)、Django(Python)、Express(Node.js)等,使用 HTTP 协议来为前端提供数据。
    • 典型实现
      :后端通过暴露 RESTful API、GraphQL 或 WebSocket 服务,处理来自前端的请求,并将处理后的数据返回给前端。
  3. API 接口


    • 职责
      :API 是前端和后端之间的通信桥梁。前端通过 HTTP 请求向后端获取或提交数据,后端通过返回 JSON、XML 或其他格式的数据响应。
    • API 类型

      • RESTful API
        :基于 HTTP 协议的资源导向架构风格,使用 HTTP 方法(GET、POST、PUT、DELETE 等)进行操作。
      • GraphQL
        :一种更灵活的查询语言,允许前端按需查询特定的数据字段。
      • WebSocket
        :用于实时双向通信的协议,适用于实时更新的应用场景,如在线聊天、实时推送等。


三、前后端分离的工作原理

  1. 用户请求


    • 用户在浏览器中访问前端页面(例如 React 应用),静态资源通过 CDN 或 Nginx 等静态资源服务器加载到用户浏览器中。
    • 前端应用会根据用户的操作(如点击按钮或提交表单)向后端发出 API 请求。请求的 URL 通常是后端的 RESTful API。
  2. 后端处理请求


    • 后端应用接收到 API 请求后,处理业务逻辑。例如,查询数据库、验证用户权限、执行计算等。
    • 处理完成后,后端将数据(通常是 JSON 格式)作为响应返回给前端。
  3. 前端接收数据并更新视图


    • 前端应用接收到后端返回的数据后,使用这些数据更新页面的内容。例如,显示用户信息、刷新商品列表等。
    • 由于前端采用单页应用(SPA),页面可以局部刷新,用户体验更好,页面的响应速度更快。


四、前后端分离的典型架构

  1. 三层架构
    :前后端分离应用通常采用以下三层架构:


    • 前端层
      :负责用户界面与交互,通常是静态页面、JavaScript、CSS 组成的 Web 应用,运行在客户端浏览器中。
    • 后端层
      :负责处理业务逻辑,管理数据流动,通常是 RESTful API、GraphQL、WebSocket 等服务的提供者。
    • 数据层
      :负责数据存储,通常是数据库(如 MySQL、PostgreSQL、MongoDB 等)或缓存系统(如 Redis 等)。
  2. 微服务架构(可选)


    • 在更复杂的应用中,后端可以使用微服务架构,将不同功能模块(如用户管理、订单管理、支付服务等)拆分成多个微服务。
    • 前端可以调用 API Gateway,后端的 API Gateway 会将请求分发给各个微服务,处理完成后再将结果返回给前端。


五、前后端分离的优势

  1. 开发效率提升


    • 前后端可以并行开发,后端专注于提供 API,前端负责界面开发,不再需要频繁等待对方完成某个功能才能继续工作。
    • 通过 API 文档(如 Swagger)前端可以基于接口协议开发和测试,即使后端服务尚未完成。
  2. 技术选型灵活


    • 前后端分离允许前端和后端使用不同的技术栈。比如,前端可以使用 React 或 Vue,而后端可以使用 Java、Node.js 或 Python。
  3. 独立部署和扩展


    • 前后端可以独立部署和扩展。前端可以通过 CDN 加速资源的加载,而后端可以通过负载均衡和微服务架构提升 API 性能。
    • 业务扩展时,前端不必担心后端的技术实现,后端也可以独立维护和优化。
  4. 提升用户体验


    • 前端可以基于单页应用(SPA)技术构建富交互应用,减少页面的整体刷新,提升用户体验。


六、前后端分离的挑战

  1. 跨域问题


    • 由于前端和后端可能部署在不同的域名下,前端发起的请求会遇到浏览器的跨域限制(CORS 问题)。需要在后端配置 CORS 策略以允许跨域访问。
  2. 数据同步与状态管理


    • 在单页应用中,前端的状态管理变得复杂,前端需要使用状态管理工具(如 Redux 或 Vuex)来管理全局状态和 API 数据。
  3. 性能优化


    • 需要通过合理的缓存、懒加载、代码分割等手段优化前端性能,避免过大的资源文件影响加载速度。
    • 后端的 API 也需要进行负载均衡、数据库索引等优化,保障响应速度。


前后端分离应用通过将前端和后端的开发、部署、运维解耦,极大提升了开发效率和灵活性。前端专注于用户界面和交互,后端专注于业务逻辑和数据处理,双方通过标准化的 API 进行通信。

在Java中,获取线程池中所有线程列表并不是一个直接支持的功能,因为线程池的设计通常是为了隐藏和管理底层的线程细节,从而提供更高层次的抽象和并发控制能力。然而,通过一些反射和技巧,我们仍然可以获取到线程池中的线程信息。

需要注意的是,直接操作线程池的内部状态并不是一种推荐的做法,因为它依赖于特定的实现细节,可能会在未来的Java版本中发生变化。因此,这种方法应该谨慎使用,并且主要用于调试或监控目的。

1.方法一:反射获取线程池中的线程列表

下面是一个详细的示例,展示了如何通过反射获取线程池中的线程列表,并打印出这些线程的信息。这个例子使用了
ThreadPoolExecutor
,这是Java中最常用的线程池实现。

import java.lang.reflect.Field;  
import java.util.List;  
import java.util.concurrent.*;  
  
public class ThreadPoolInfo {  
  
    public static void main(String[] args) throws InterruptedException {  
        // 创建一个固定大小的线程池  
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);  
  
        // 提交一些任务给线程池  
        for (int i = 0; i < 5; i++) {  
            executor.submit(() -> {  
                try {  
                    Thread.sleep(2000); // 模拟任务执行  
                    System.out.println(Thread.currentThread().getName() + " is executing a task.");  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
            });  
        }  
  
        // 等待一段时间以确保任务开始执行  
        Thread.sleep(1000);  
  
        // 获取线程池中的线程列表  
        List<Thread> threadList = getThreadPoolThreads(executor);  
  
        // 打印线程信息  
        for (Thread thread : threadList) {  
            System.out.println("Thread: " + thread.getName() + ", State: " + thread.getState());  
        }  
  
        // 关闭线程池  
        executor.shutdown();  
        executor.awaitTermination(1, TimeUnit.MINUTES);  
    }  
  
    /**  
     * 通过反射获取线程池中的线程列表  
     *  
     * @param executor 线程池执行器  
     * @return 线程列表  
     */  
    public static List<Thread> getThreadPoolThreads(ThreadPoolExecutor executor) {  
        List<Thread> threadList = null;  
        try {  
            // 获取workerQueue字段(这是一个阻塞队列,存储等待执行的任务)  
            Field workerQueueField = ThreadPoolExecutor.class.getDeclaredField("workerQueue");  
            workerQueueField.setAccessible(true);  
            BlockingQueue<?> workerQueue = (BlockingQueue<?>) workerQueueField.get(executor);  
  
            // 获取mainLock字段(这是一个ReentrantLock,用于同步对workerSet的访问)  
            Field mainLockField = ThreadPoolExecutor.class.getDeclaredField("mainLock");  
            mainLockField.setAccessible(true);  
            ReentrantLock mainLock = (ReentrantLock) mainLockField.get(executor);  
  
            // 获取workerSet字段(这是一个HashSet,存储所有的Worker对象)  
            Field workerSetField = ThreadPoolExecutor.class.getDeclaredField("workers");  
            workerSetField.setAccessible(true);  
            HashSet<?> workerSet = (HashSet<?>) workerSetField.get(executor);  
  
            // 锁定mainLock以确保对workerSet的访问是线程安全的  
            mainLock.lock();  
            try {  
                // 创建一个线程列表来存储所有的线程  
                threadList = new ArrayList<>();  
                // 遍历workerSet,获取每个Worker对象的线程  
                for (Object worker : workerSet) {  
                    Field threadField = worker.getClass().getDeclaredField("thread");  
                    threadField.setAccessible(true);  
                    Thread thread = (Thread) threadField.get(worker);  
                    threadList.add(thread);  
                }  
            } finally {  
                // 释放锁  
                mainLock.unlock();  
            }  
        } catch (NoSuchFieldException | IllegalAccessException e) {  
            e.printStackTrace();  
        }  
  
        // 如果workerQueue中有等待执行的任务,那么这些任务对应的线程可能还没有启动,因此这里不考虑它们  
        // 如果需要获取这些任务的信息,可以遍历workerQueue  
  
        return threadList;  
    }  
}

代码说明:

(1)
创建线程池
:使用
Executors.newFixedThreadPool(3)
创建一个固定大小的线程池,其中包含3个工作线程。

(2)
提交任务
:向线程池提交5个任务,每个任务会睡眠2秒钟并打印线程名称。

(3)
获取线程列表
:通过反射获取线程池中的线程列表。这个方法是
getThreadPoolThreads
,它使用反射访问
ThreadPoolExecutor
的内部字段来获取线程信息。

(4)
打印线程信息
:遍历线程列表并打印每个线程的名称和状态。

(5)
关闭线程池
:等待所有任务完成后关闭线程池。

注意事项:

(1)反射是一种强大的工具,但它破坏了Java的封装性。因此,使用反射时要特别小心,确保代码的稳定性和可维护性。

(2)这个示例代码依赖于
ThreadPoolExecutor
的内部实现细节,可能会在未来的Java版本中发生变化。因此,在生产环境中使用时,请务必进行充分的测试。

(3)这种方法主要用于调试或监控目的,不建议在生产环境中频繁使用。

在Java中,除了使用反射来获取线程池中的线程列表外,还有其他几种方法可以尝试,尽管它们可能不是直接获取线程列表的标准方式。以下是一些替代方法:

2.方法二:使用
Thread.getAllStackTraces()

Thread.getAllStackTraces()
方法返回当前Java虚拟机中所有活动线程的堆栈轨迹映射。虽然这不是直接针对线程池的,但我们可以通过遍历返回的映射来获取所有线程的引用,并根据线程的名称或其他属性来判断它们是否属于特定的线程池。

Set<Thread> allThreads = Thread.getAllStackTraces().keySet();  
// 遍历allThreads,检查每个线程是否属于你的线程池

然而,这种方法有一个显著的缺点:它返回的是当前JVM中所有活动线程的集合,因此我们需要额外的逻辑来过滤出属于特定线程池的线程。此外,这种方法也可能受到线程名称命名约定的影响,如果线程池中的线程没有使用统一的命名模式,那么过滤可能会变得困难。

代码示例:

import java.util.Map;  
import java.util.Set;  
  
public class ThreadPoolThreadChecker {  
    public static void main(String[] args) {  
        // 假设你有一个线程池在运行(这里不实际创建)  
        // ...  
  
        // 获取所有线程的堆栈轨迹映射  
        Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();  
        Set<Thread> allThreads = allStackTraces.keySet();  
  
        // 遍历所有线程,检查它们是否属于某个线程池  
        // 这里假设线程池中的线程名称包含特定的字符串,比如 "myThreadPool-"  
        for (Thread thread : allThreads) {  
            if (thread.getName().contains("myThreadPool-")) {  
                System.out.println("Found thread from thread pool: " + thread.getName());  
                // 你可以在这里添加更多逻辑来处理这些线程  
            }  
        }  
    }  
}

3.方法三:使用
ThreadPoolExecutor

getCompletedTaskCount()

getActiveCount()
等方法

虽然这些方法不能直接返回线程列表,但它们可以提供关于线程池状态的有用信息。例如,
getActiveCount()
方法返回当前正在执行任务的线程数,而
getCompletedTaskCount()
方法返回已完成的任务数。通过结合这些方法和线程池的配置信息(如核心线程数、最大线程数等),我们可以对线程池的活动状态有一个大致的了解。

代码示例:

import java.util.concurrent.*;  
  
public class ThreadPoolStatusChecker {  
    public static void main(String[] args) {  
        // 创建一个线程池  
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(4);  
  
        // 提交一些任务给线程池(这里只是示例)  
        for (int i = 0; i < 10; i++) {  
            executor.submit(() -> {  
                try {  
                    Thread.sleep(1000); // 模拟任务执行  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
            });  
        }  
  
        // 获取线程池的状态信息  
        System.out.println("Active threads: " + executor.getActiveCount());  
        System.out.println("Completed tasks: " + executor.getCompletedTaskCount());  
        System.out.println("Total tasks: " + (executor.getCompletedTaskCount() + executor.getTaskCount()));  
  
        // 关闭线程池(这里只是为了示例,实际使用中应该等待所有任务完成后再关闭)  
        executor.shutdownNow();  
    }  
}

4.方法四:自定义线程工厂

当我们创建线程池时,可以通过提供自定义的
ThreadFactory
来影响线程的创建过程。在自定义的
ThreadFactory
中,我们可以为创建的每个线程设置特定的属性(如名称、优先级等),并在工厂中维护一个对所有这些线程的引用。这样,虽然我们仍然不能直接从线程池获取线程列表,但我们可以通过访问工厂中的引用来获取线程信息。

需要注意的是,这种方法的一个潜在缺点是它增加了额外的内存开销,因为我们需要维护一个额外的线程引用集合。此外,如果线程池中的线程被回收(例如,在超过
keepAliveTime
后没有任务执行时),我们需要确保从集合中移除这些线程的引用,以避免内存泄漏。

代码示例:自定义线程工厂

import java.util.ArrayList;  
import java.util.List;  
import java.util.concurrent.*;  
  
public class CustomThreadFactory implements ThreadFactory {  
    private final String namePrefix;  
    private final List<Thread> createdThreads = new ArrayList<>();  
    private int threadNumber = 1;  
  
    public CustomThreadFactory(String namePrefix) {  
        this.namePrefix = namePrefix;  
    }  
  
    @Override  
    public Thread newThread(Runnable r) {  
        Thread thread = new Thread(r, namePrefix + "-Thread-" + threadNumber);  
        createdThreads.add(thread);  
        threadNumber++;  
        return thread;  
    }  
  
    public List<Thread> getCreatedThreads() {  
        return createdThreads;  
    }  
  
    public static void main(String[] args) {  
        CustomThreadFactory factory = new CustomThreadFactory("MyThreadPool");  
        ThreadPoolExecutor executor = new ThreadPoolExecutor(  
                2, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), factory);  
  
        // 提交任务(这里只是示例)  
        for (int i = 0; i < 5; i++) {  
            executor.submit(() -> {  
                try {  
                    Thread.sleep(1000); // 模拟任务执行  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
            });  
        }  
  
        // 获取自定义工厂中创建的线程列表  
        List<Thread> threads = factory.getCreatedThreads();  
        for (Thread thread : threads) {  
            System.out.println("Created thread: " + thread.getName());  
        }  
  
        // 关闭线程池(这里只是为了示例,实际使用中应该等待所有任务完成后再关闭)  
        executor.shutdownNow();  
    }  
}

5.方法五:使用监控和诊断工具(JMX示例)

许多Java应用服务器和监控工具提供了对线程池的内置支持。例如,在Java EE环境中,我们可以使用JMX(Java Management Extensions)来监控线程池的状态。这些工具通常提供了更直观和全面的视图来查看线程池的活动线程、等待任务队列长度、任务执行时间等关键指标。

使用JMX来监控线程池通常涉及配置Java应用服务器或使用Java提供的JMX API来连接和查询MBeans。这里我将提供一个简单的JMX客户端示例,它连接到本地JVM并查询线程池MBeans。然而,请注意,这个示例假设我们已经有一个正在运行的线程池,并且它的MBeans已经注册到JMX中。

由于JMX的复杂性,这里只提供一个基本的框架,我们需要根据我们的具体环境和需求进行调整。

import javax.management.*;  
import java.lang.management.*;  
import java.util.Set;  
  
public class JmxThreadPoolMonitor {  
    public static void main(String[] args) throws Exception {  
        // 获取平台MBean服务器  
        MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();  
  
        // 查询线程池相关的MBean(这里需要知道具体的ObjectName)  
        // 例如,对于Java EE应用服务器,ObjectName可能会有所不同  
        // 这里只是一个假设的ObjectName,我们需要根据实际情况进行修改  
        ObjectName query = new ObjectName("java.util.concurrent:type=ThreadPool,name=*");  
  
        // 执行查询  
        Set<ObjectName> names = mbeanServer.queryNames(query, null);  
  
        // 遍历查询结果  
        for (ObjectName name : names) {  
            // 获取线程池的属性(这里只是示例,我们可以获取更多属性)  
            Integer activeCount = (Integer) mbeanServer.getAttribute(name, "ActiveCount");  
            Long completedTaskCount = (Long) mbeanServer.getAttribute(name, "CompletedTaskCount");  
  
            System.out.println("ThreadPool Name: " + name.getKeyProperty("name"));  
            System.out.println("Active Threads: " + activeCount);  
            System.out.println("Completed Tasks: " + completedTaskCount);  
        }  
    }  
}

请注意,上面的JMX示例中的
ObjectName
是一个假设的值,我们需要根据我们的具体环境和线程池的配置来确定正确的
ObjectName
。此外,不同的Java应用服务器和线程池实现可能会有不同的MBean名称和属性。因此,在实际使用中,我们可能需要查阅相关的文档或使用JMX客户端工具(如JConsole或VisualVM)来浏览和查询MBean。

6.总结

虽然Java标准库没有直接提供获取线程池中所有线程列表的方法,但我们可以通过上述替代方法来获取有关线程池状态的信息。每种方法都有其优缺点,我们需要根据具体的应用场景和需求来选择最适合的方法。在生产环境中使用时,请务必进行充分的测试以确保代码的可靠性和稳定性。

书节上回,我们接着聊二叉树,N叉树,以及树的存储。

01
、满二叉树

如果一个二叉树,除最后一层节点外,每一层的节点数都达到最大值,即每个节点都有两个子节点,同时所有叶子节点都在最后一层,则这个二叉树为满二叉树。

因此可以得到满二叉树有以下性质:

(1)树的最大层数为k(k>=0,即层数从0开始),则第i层的节点总数为2
i,树的叶子节点总数为2
k ,树的总节点数为2 ^(k+1) - 1;

(2)如果已知数的总节点数为n,则树的最大层数为k=log2(n+1) - 1(k>=0,即层数从0开始);

(3)从树的顶层开始从上到下,从左到右,对每个节点按顺序进行编号,根节点为1作为起始;

a.对于节点i,如果其存在父节点,则父节点编号为i/2向下取整;

b.对于节点i,如果其存在左节点则左节点编号为2i,如果其存在右节点则右节点编号为2i+1;

02
、完全二叉树

如果一棵树,叶子节点只能出现在最后两层,并且最后一层的节点都集中在左侧且从左到右是连续的。

由此我们可以得到完全二叉树有以下性质:

(1)叶子节点只能出现在倒数第一层和倒数第二层;

(2)倒数第一层的所有叶子节点都集中此层最左侧连续位置;

(3)倒数第二层如果存在叶子节点则层的所有叶子节点都集中在此层最右侧连续位置;

(4)所有节点中如果存在一个节点只有一个子节点,则此节点一定在倒数第二层,并且这个子节点一定是左节点;

(5)满二叉树一种特殊的完全二叉树;

(6)满二叉树性质(3)也适用于完全二叉树;

(7)如果完全二叉树不是满二叉树,则除最后一层外为满二叉树,适用满二叉树所有性质;

(8)如果节点总数为n,并且i<=n/2,则第i个节点为非叶子节点;

(9)如果节点总数为n,如果n为偶数则叶子节点数为n/2;如果n为奇数则叶子节点数为(n+1)/2;

(10)如果树的层数为k(k>=0),则树数的总节点数的访问为[2
k,2
(k+1) - 1)];

(11)如果已知数的总节点数为n,则树的最大层数为k=log2(n+1) (k>=0,即层数从0开始),并且k向下取整;

03
、二叉搜索树

二叉搜索树是一种特殊的二叉树,又叫二叉查找树、二叉排序树,可以说这是一种为了查询而生的二叉树。

二叉搜索树有以下性质:

(1)每个节点值必须唯一,不能有重复值;

(2)每个节点最多有两个子节点;

(3)对于任意一个节点,如果其存在左子树则左子树上所有节点值小于该节点值,如果其存在右子树则右子树上所有节点值大于该节点值;

(3)左子树和右子树本身也各自是二叉搜索树;

04
、平衡二叉树

平衡二叉树顾名思义就是使二叉树平衡在某种状态下,某种状态具体指树中任意一个节点左右子树高度差绝对值小于等于1,并且其左右子树同样也是平衡二叉树。

平衡二叉树是通过控制树的深度来优化二叉搜索树平均操作时间。

AVL树是平衡二叉树的一种特例,严格执行平衡二叉树的定义,也是最早发明的自平衡二叉搜索树。

红黑树也是一种自平衡二叉搜索树,但其并没有严格执行平衡二叉树定义,平衡条件相对比较宽松,允许左右子树高度相差绝对值大于1。

这两种树我们后面会找机会单独详细讲解。

此外还有哈夫曼树、线段树、伸展树、替罪羊树等二叉树这里就不一一介绍了,后面我们单独拿出来讲解。

05
、N叉树

B树、B+树、2-3-4树、R树、Trie树等多叉树我们后面找机会单独拿出来详解。

06
、存储结构

1、顺序存储

顺序存储只用一组连续的地址空间存储整个树。

当然并不是所有树都适合顺序存储的,还记得上面说的满二叉树的性质(3)吗?如果把满二叉树从上到下、从左到右,则左右子节点编号可以通过父节点编号表示。如果父节点编号为i,则其左子节点编号为2i,其右子节点编号为2i+1。而根节点作为已知起始值1,就意味着其后代节点都可以通过根节点直接或间接表示。

而编号不经让我们想到数组下标,这就意味着我们可以把整个满二叉树装进数组里。因为完全二叉树也满足满二叉树的性质(3),所以完全二叉树也可以装进数组。如下图。

那如果非完全二叉树呢?能否放入数组中呢,答案是肯定可以的。我们只需要把非完全二叉树想象成满二叉树,把缺少的节点虚拟补全,然后对其编号,最后装入数组,入下图:

但是我们会发现编号4、5、7位都为空值,当然编号7是可以去掉的,即使如此,还是很浪费数组空间的,因此顺序存储是比较适合完全二叉树存储的,而其他类型二叉树并不是很适合。

2、链式存储

顺序存储有其局限性,因此大多数树都是使用链式存储。链式存储的核心思想就是每一个节点设计为两部分,其一为数据域存放元素值,其二为指针域存放指向子节点指针,节点有多少个子节点指针域就有多少个指针。

我们还是以二叉树为例,链式存储结构如下:


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner