一、线程池

线程池(ThreadPool)是一种线程复用的机制。它维护着若干个线程,任务来了就复用这些线程去执行,任务做完线程不会销毁,而是回到池中等待下一个任务。

为什么要用线程池?

降低资源消耗:避免频繁创建和销毁线程带来的系统开销。

提高响应速度:任务到来时可以直接复用已有线程,无需等待新线程创建。

便于统一管理:可以统一分配、调优和监控线程,控制最大并发数,防止系统资源耗尽。

支持任务排队和拒绝策略:可以灵活处理任务高峰和异常情况。

二、线程池的核心组成及工作流程

Java线程池的核心实现类是 ThreadPoolExecutor,其主要组成如下:

  • 核心线程数(corePoolSize):池中始终存活的线程数,即使它们处于空闲状态也不会被销毁。
  • 最大线程数(maximumPoolSize):池中允许的最大线程数。
  • 线程空闲时间(keepAliveTime):非核心线程空闲多久会被销毁。
  • 时间单位(unit):keepAliveTime的时间单位。
  • 任务队列(workQueue):用于保存等待执行任务的队列。
  • 线程工厂(threadFactory):用于创建新线程的工厂。
  • 拒绝策略(handler):当线程池和队列都满了时,如何处理新任务。

线程池的工作流程

提交任务:调用execute()或submit()方法提交任务。

判断核心线程数:如果当前线程数小于corePoolSize,创建新线程执行任务。

任务入队:如果核心线程已满,尝试将任务放入队列。

创建非核心线程:如果队列也满了,且线程数小于maximumPoolSize,创建非核心线程执行任务。

拒绝策略:如果线程数已达最大且队列也满,执行拒绝策略(如抛异常、丢弃任务等)。

线程复用:线程执行完任务后不会销毁,而是回到池中等待下一个任务。

三、创建线程池的两种方式:

方式一:通过 Executors 工具类(不推荐在生产环境使用)

1. newFixedThreadPool(int nThreads)
  • 特点:创建一个固定大小的线程池。
  • 核心线程数 = 最大线程数。
  • 使用 LinkedBlockingQueue(无界队列),可能会导致任务堆积,有OOM(内存溢出)风险。
  • 适用场景:需要控制并发线程数量的场景。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
2. newSingleThreadExecutor()
  • 特点:创建一个只有一个线程的线程池。
  • 核心线程数 = 最大线程数 = 1。
  • 使用 LinkedBlockingQueue(无界队列),同样有OOM风险。
  • 适用场景:需要保证所有任务按顺序执行的场景。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
3 newCachedThreadPool()
  • 特点:创建一个可缓存的线程池,线程数会根据任务量动态调整。
  • 核心线程数为0,最大线程数为 Integer.MAX_VALUE。
  • 使用 SynchronousQueue,任务来了如果没有空闲线程,就直接创建新线程。
  • 风险:如果任务量巨大,会无限制地创建线程,可能导致OOM或系统资源耗尽。
  • 适用场景:执行大量短期、异步任务的场景。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
4  newScheduledThreadPool(int corePoolSize)
  • 特点:创建一个支持定时及周期性任务执行的线程池。
  • 适用场景:需要执行定时任务或周期性任务的场景。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

方式二:通过 ThreadPoolExecutor 构造函数(推荐)

ThreadPoolExecutor 是 Java 并发包(java.util.concurrent)中线程池的核心实现类。它功能强大、高度可配置,是理解和使用 Java 线程池的基础。这是最原始、最灵活,也是生产环境中推荐使用的方式。你可以完全控制线程池的所有参数。

下面是一段ThreadPoolExecutor实现的线程池构建:

import java.util.concurrent.*;public class CreateThreadPoolDemo {public static void main(String[] args) {// 定义线程池的核心参数int corePoolSize = 2;int maximumPoolSize = 5;long keepAliveTime = 60L;TimeUnit unit = TimeUnit.SECONDS;BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10); // 使用有界队列ThreadFactory threadFactory = Executors.defaultThreadFactory();RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略// 创建线程池ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);// 使用线程池for (int i = 0; i < 15; i++) {threadPoolExecutor.execute(() -> {System.out.println(Thread.currentThread().getName() + " is running...");});}// 关闭线程池threadPoolExecutor.shutdown();}
}

而且我们查看方式一的四种类型的线程池创建:

public static ExecutorService newFixedThreadPool(int nThreads) {// LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}public static ExecutorService newSingleThreadExecutor() {// LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));}// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}// DelayedWorkQueue(延迟阻塞队列)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}

可以看到前三种类型都是对ThreadPoolExecutor的包装,可以大胆猜测一下,第四种应该也是对ThreadPoolExecutor的包装;但是在他的实现中没有看到实例化的ThreadPoolExecutor,那就有些疑惑了。那它是如何实现的封装ThreadPoolExecutor呢?看下面的类的关系:

可以看到ScheduledThreadPoolExecutor类中继承了ThreadPoolExecutor,所以它通过 super 关键字调用了父类的构造函数。这说明定时任务线程池本质上也是一个 ThreadPoolExecutor,只是配置了特殊的参数。由此可见ThreadPoolExecutor类十分的重要,它是Executors工具类的基础组成,而且阿里巴巴的《Java开发手册》中强制要求不要使用 Executors 的这几种方法来创建线程池,因为它们都存在资源耗尽的风险;因此ThreadPoolExecutor类进一步解析十分重要;

四、ThreadPoolExecutor源码解析

1 构造方法:
 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;}

这是其中的全参构造构造方法,其他的构造方法都是通过this对这个构造方法的调用,类似之前Executors工具类是对ThreadPoolExecutor的封装。它首先对非合理参数进行了判断,如果参数不合理直接抛出对应错误,否则初始化参数构建ThreadPoolExecutor实例;

2 execute方法:

execute方法是Java线程池(如ThreadPoolExecutor)中最核心的任务提交方法,其作用可以总结为:向线程池提交一个需要执行的任务(Runnable),由线程池中的线程来执行该任务。

具体流程如下:

当你调用executor.execute(task)时,线程池会按照如下流程处理:

  1. 判断当前线程数是否小于核心线程数(corePoolSize)
  2. 如果是,直接创建新线程来执行任务。
  3. 如果核心线程已满,尝试将任务放入任务队列(workQueue)
  4. 如果队列未满,任务会被缓存,等待线程池中的线程来取出并执行。
  5. 如果队列也满了,且线程数未达到最大线程数(maximumPoolSize)
  6. 会创建新的非核心线程来执行任务。
  7. 如果线程池已满且队列也满
  8. 执行拒绝策略(如抛出异常、丢弃任务等)。

那我们看看上述流程在execute方法中是如何实现的:

public void execute(Runnable command) {// 1. 判空,防止提交null任务if (command == null)throw new NullPointerException();// 2. 获取线程池当前状态和线程数int c = ctl.get();// 3. 如果当前线程数小于核心线程数,优先创建核心线程执行任务if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true)) // 尝试创建核心线程return; // 成功则直接返回c = ctl.get(); // 失败则重新获取ctl,继续后续流程}// 4. 如果线程池处于RUNNING状态且队列未满,任务入队if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get(); // 入队后再次获取ctl,防止状态变化// 4.1 如果线程池已关闭且任务还在队列,移除任务并执行拒绝策略if (!isRunning(recheck) && remove(command))reject(command);// 4.2 如果线程池里没有线程了,创建一个非核心线程来保证队列任务能被执行else if (workerCountOf(recheck) == 0)addWorker(null, false);}// 5. 如果队列也满了,尝试创建非核心线程执行任务else if (!addWorker(command, false))// 6. 如果创建失败(线程池已满或已关闭),执行拒绝策略reject(command);
}
3 状态控制

如上,你可能会疑惑ctl是啥 : 它是一种原子整型成员变量,主要用来状态控制:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

先看其参数RUNNING,它是-1向左位移29位

在 Java 中,int 类型是 32 位,-1 的补码是:

1111 1111 1111 1111 1111 1111 1111 1111

 左移 29 位后,低 29 位变成 0,高 3 位还是 1:

1110 0000 0000 0000 0000 0000 0000 0000

在看看ctlOf方法:

private static int ctlOf(int rs, int wc) { return rs | wc; 
}

它是实现了一个按位或运算,为啥要这样设计呢?这是因为可以用一个int变量(ctl)同时存储线程池的状态和线程数,高三位存储状态,低29位存储线程数,这样可以通过一个int变量同时管理线程池状态和线程数;

那如何获取状态信息和线程数呢?

private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }

如上两种方法,runStateOf是获取状态信息,wokerCountOf是获取线程数信息:

CAPACITY是1左移29位-1得到的,其二进制信息为:0001 1111 1111 1111 1111 1111 1111 1111

其中高三位全为0,低29为全为1;可以对CAPACITY适时取反后进行按位与操作获取高三位或者低29位信息,这样就可以单独获取到该线程池的状态和线程数量信息;

更多详细的信息如下表:

表达式二进制运算示例作用
c1110 0000 ... 0000 0101存储复合信息(状态+线程数)
CAPACITY0001 1111 ... 1111 1111线程数掩码(高3位为0,低29位为1)
c & CAPACITY0000 0000 ... 0000 0101提取线程数(workerCountOf(c)的实现)
~CAPACITY1110 0000 ... 0000 0000运行状态掩码(高3位为1,低29位为0)
c & ~CAPACITY1110 0000 ... 0000 0000提取运行状态(runStateOf(c)的实现)
4 addWorker方法

好了,了解了状态信息和线程信息获取过程,还需要看一下addWorker方法,这个是用来创建线程的方法。它的两个参数,一个是Runnable变量,一个是布尔类型变量;

/*** 尝试创建一个新的Worker线程,并执行其第一个任务。* @param firstTask Worker线程的第一个任务,可以为null。* @param core 如果为true,则使用corePoolSize作为线程数上限;否则使用maximumPoolSize。* @return 如果成功创建并启动了Worker,则返回true;否则返回false。*/
private boolean addWorker(Runnable firstTask, boolean core) {// 外层循环,用于在CAS失败或线程池状态改变时重试。retry:for (;;) {int c = ctl.get();int rs = runStateOf(c);// --- 状态检查 ---// 检查是否可以添加新的Worker线程。// 如果线程池已关闭 (>= SHUTDOWN),则通常不允许添加新线程。// 但有一个例外:如果状态是SHUTDOWN,且任务队列不为空,允许添加一个没有初始任务的Worker来处理队列中的任务。if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;// 内层循环,用于通过CAS原子地增加工作线程数。for (;;) {int wc = workerCountOf(c);// --- 容量检查 ---// 检查工作线程数是否已达到上限 (CAPACITY或core/maximumPoolSize)。if (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))return false;// --- CAS操作 ---// 尝试原子地将工作线程数+1。if (compareAndIncrementWorkerCount(c))break retry; // CAS成功,跳出所有循环,继续执行后续的创建逻辑。// --- CAS失败处理 ---c = ctl.get();  // CAS失败,重新读取ctl的值。if (runStateOf(c) != rs)continue retry; // 如果线程池状态已改变,回到外层循环重试。// 如果状态未变,说明是其他线程也增加了线程数导致的CAS失败,仅重试内层循环。}}// --- 创建并启动Worker线程 ---boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {// 创建一个新的Worker对象,它包装了任务和要执行的线程。w = new Worker(firstTask);final Thread t = w.thread;if (t != null) {final ReentrantLock mainLock = this.mainLock;mainLock.lock(); // 获取全局锁,保证线程安全地添加Worker和启动线程。try {// 在持有锁的情况下,再次检查线程池状态,防止在获取锁的过程中线程池被关闭。int rs = runStateOf(ctl.get());// 如果线程池正在运行,或者处于SHUTDOWN状态且允许添加空任务的Worker。if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // 预检,防止启动一个已经存活的线程。throw new IllegalThreadStateException();// 将新的Worker添加到workers集合中。workers.add(w);int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {mainLock.unlock(); // 确保锁被释放。}if (workerAdded) {t.start(); // 启动线程。workerStarted = true;}}} finally {// --- 失败回滚 ---// 如果线程启动失败(如ThreadFactory创建失败或启动过程中出错)。if (!workerStarted) {addWorkerFailed(w); // 调用失败处理方法,将之前增加的线程数-1,并从集合中移除Worker。}}return workerStarted;
}

先看for循环中的第一个判断

   if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;

rs先判断线程池是否是正常状态并且任务和队列不能为null,为啥判断队列不为null呢,因为,当核心线程数没到最大值时,再来的任务会创建核心线程,当核心线程达到最大值时会向队列中暂存任务,因此队列不为null时,核心线程数达到了最大值,不会创建核心线程数;

在状态检查之后,进行线程容量检查,如果小于核心线程数则进行增加线程数,在增加过程中使用的是cas操作,如果不成功则重新获取rs判断状态;如果线程池状态已改变,回到外层循环重试。如果状态未变,说明是其他线程也增加了线程数导致的CAS失败,仅重试内层循环。

当线程核心数增加成功后,开始增加worker线程并且启动线程,在添加工作线程时使用ReentrantLock 对workers对象上锁;在此过程中会再次检查线程池状态,在添加成功后同时更新largestPoolSize参数,这个参数记录了线程池中最大的线程数量。别把核心线程数:corePoolSize和最大线程数:maximumPoolSize混淆了。

当添加线程成功则运行线程

如果线程启动失败则调用失败处理方法,将之前增加的线程数-1,并从集合中移除Worker。

先写这些吧......

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/89770.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/89770.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/89770.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux内核IP分片重组机制剖析:高效与安全的艺术

在IP网络通信中,当数据包超过MTU限制时,路由器会将其拆分为多个分片。这些分片到达目标主机后,内核必须高效、安全地重组原始数据包。Linux内核的net/ipv4/inet_fragment.c实现了一套精妙的分片管理框架,完美平衡了性能和安全性需求。本文将深入剖析其设计哲学与关键技术。…

相机模型和对极几何

一、相机模型 1.针孔相机模型-外参矩阵 1.世界坐标系到相机坐标系 世界坐标系&#xff1a;可以定义空间中任意一个位置&#xff0c;原点位置三个坐标轴方向坐标系姿态&#xff08;X,Y,Z&#xff09;相机坐标系&#xff1a;定义在相机上&#xff0c;原点是相机中心&#xff0c;z…

Git 常用命令与操作步骤

以下是 Git 常用命令与操作步骤 的整理&#xff0c;涵盖日常开发中最核心的场景&#xff0c;适合快速查阅和上手&#xff1a;1. 初始化与克隆仓库操作命令本地初始化仓库git init克隆远程仓库git clone <仓库URL> &#xff08;如 git clone https://gitlab.com/user/repo…

Leetcode-.283移动零

class Solution:def moveZeroes(self, nums: List[int]) -> None:"""Do not return anything, modify nums in-place instead."""pos0for i in range(len(nums)):if nums[i]!0:nums[pos],nums[i]nums[i],nums[pos]pos1本题运用双指针来写&…

在React中做过哪些性能优化?

1. 使用 React.memo 进行组件优化 问题:当父组件重新渲染时,子组件也会重新渲染,即使它的 props 没有变化。 解决方案:使用 React.memo 包裹子组件,让其只在 props 变化时才重新渲染。 const MyComponent = React.memo((props) => {// 子组件代码 }); 2. 使用 useCa…

安装docker可视化工具 Portainer中文版(ubuntu上演示,所有docker通用) 支持控制各种容器,容器操作简单化 降低容器门槛

以下有免费的4090云主机提供ubuntu22.04系统的其他入门实践操作 地址&#xff1a;星宇科技 | GPU服务器 高性能云主机 云服务器-登录 相关兑换码星宇社区---4090算力卡免费体验、共享开发社区-CSDN博客 兑换码要是过期了&#xff0c;可以私信我获取最新兑换码&#xff01;&a…

ansible批量部署zabbix客户端

✅ansible编写剧本步骤 1️⃣创建roles目录结构2️⃣在group_vars/all/main.yml中定义变量列表3️⃣在tasks目录下编写tasks任务4️⃣在files目录下准备部署文件5️⃣在templates目录下创建j2模板文件6️⃣在handlers目录下编写handlers7️⃣在roles目录下编写主playbook8️⃣运…

蚂蚁数科AI数据产业基地正式投产,携手苏州推进AI产业落地

近日&#xff0c;蚂蚁数科AI数据产业基地在太仓智汇谷科技创新园正式投产。该基地作为苏州市首个AI数据产业基地&#xff0c;旨在通过跨行业人才与前沿技术&#xff0c;为长三角制造业、金融、医疗等领域的大模型落地提供场景化、高质量的训练数据支撑。数据被视为AI学习的核心…

计算机的网络体系及协议模型介绍

目录 1、网络协议介绍 1.1、定义 1.2、基本作用 1.3、协议的主要内容 2、网络协议分层 2.1、协议分层原因 2.2、网络协议分层的缺点 2.3、OSI协议和TCP/IP协议的联系 3、TCP/IP 协议族 3.1、定义介绍 3.2、组成 1、应用层 2、运输层 3、网络层 3.3、底层流程 4、…

密码管理安全防御

密码管理是信息安全的核心环节,其目标是通过规范密码的生成、存储、传输、验证和生命周期管理,防止未授权访问,保护用户账号和系统资源的安全。以下从核心原则、技术实践、常见问题及解决方案等方面详细说明: 一、密码管理的核心原则 密码管理需遵循“安全性”与“可用性…

Java异步日志系统性能优化实践指南:基于Log4j2异步Appender与Disruptor

Java异步日志系统性能优化实践指南&#xff1a;基于Log4j2异步Appender与Disruptor 一、技术背景与应用场景 在高并发的后端应用中&#xff0c;日志记录往往成为性能瓶颈之一。同步写日志会阻塞业务线程&#xff0c;导致响应延迟&#xff1b;而简单的异步队列实现又可能出现积压…

Mybatis07-缓存

一、缓存机制的原理计算机每次从mysql中执行sql语句&#xff0c;都是内存与硬盘的通信&#xff0c;对计算机来说&#xff0c;影响效率。因此使用缓存机制。1-1、MyBatis 的缓存机制&#xff1a;执行 DQL&#xff08;select 语句&#xff09;的时候&#xff0c;将查询结果放到缓…

【机器学习深度学习】LoRA 与 QLoRA:大模型高效微调的进阶指南

目录 前言 一、LoRA&#xff1a;低秩微调的经典之作 二、QLoRA&#xff1a;效率与精度的升级版 三、LoRA vs QLoRA&#xff1a;如何选择&#xff1f; 3.1 性能维度对比 3.2 根据「显卡资源」选择 3.3 根据「任务类型与目标」选择 3.4 根据「模型规模」选择 3.5 根据…

教育行业网络升级最佳实践:SD-WAN、传统方案与混合方案对比分析

随着教育行业的数字化转型不断深入&#xff0c;网络的稳定性、灵活性和安全性成为各类教育应用&#xff08;如远程课堂、智慧校园和教育云平台&#xff09;的核心支撑。然而&#xff0c;传统的 MPLS 专线方案成本高、扩展性差&#xff0c;而纯 SD-WAN 的方案在极高可靠性要求的…

[黑马头条]-文章列表加载

目录 1.1)需求分析 1.2)表结构分析 ap_article 文章基本信息表 ap_article_config 文章配置表 ap_article_content 文章内容表 导入文章数据库 实现思路 接口定义 功能实现 定义接口 编写mapper文件 编写业务层代码 实现类&#xff1a; 定义常量类 编写控制器代码 …

使用TIANAI-CAPTCHA进行行为验证码的生成和缓存的二次校验

1.导入依赖&#xff1a;<dependency><groupId>cloud.tianai.captcha</groupId><artifactId>tianai-captcha-springboot-starter</artifactId><version>1.5.2</version> </dependency>2.在application.yml中配置验证码相关配置…

db.refresh()的重复使用和db.rollback()

db.refresh()在 SQLAlchemy 中&#xff0c;db.refresh() 用于从数据库中重新加载对象的状态&#xff0c;确保对象属性与数据库中的实际数据保持一致。下面详细介绍其使用场景和作用&#xff1a;1.获取数据库生成的值当数据库自动生成字段&#xff08;如自增 ID、默认值、触发器…

《Web安全之机器学习入门》读书笔记总结

目录 一、案例总结 1、基础知识 &#xff08;1&#xff09;第1章 通向智能安全的旅程 &#xff08;2&#xff09;第2章 打造机器学习工具箱 &#xff08;3&#xff09;第3章 机器学习概述 &#xff08;4&#xff09;第4章 Web安全基础 2、安全案例 &#xff08;1&#…

github 近期热门项目-2025.7.20

github 近期热门项目-2025.7.20 GitHub 上近期热门或趋势项目的信息可以从多个来源获取,包括 GitHub Trending 页面、技术社区推荐、以及各大技术媒体的报道。以下是一些近期在 GitHub 上备受关注的项目类别和示例: 1. AI 与机器学习项目 随着 AI 技术的快速发展,许多开源…

使用Python清理Excel中的空行和单元格内部空行:初学者指南

前言 作为数据处理人员或办公室工作者,你可能经常遇到Excel文件中存在多余空行或单元格内有多余空行的问题。这些不必要的空白会影响数据的美观性,更重要的是会给后续的数据分析、合并或处理带来麻烦。本文将介绍一个简单的Python脚本,帮助你高效地解决这些问题。 很多工具…