ThreadPoolExecutor线程池详解
ThreadPoolExecutor线程池详解
1. 背景
项目最近的迭代中使用到了ThreadPoolExecutor线程池,之前都只是知道怎么用,没有了解过线程池的底层原理,项目刚上线,有时间整理一下线程池的用法,学习一下线程池的底层实现与工作原理。
2. ThreadPoolExecutor工作原理
2.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;}
2.2 线程池的使用
worker
/*** @author itender* @date 2023/8/7 14:41* @desc*/
public class Worker implements Runnable {private String command;public Worker(String s) {this.command = s;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + command + " startTie = " + DateUtil.now());processCommand();System.out.println(Thread.currentThread().getName() + command + " endTime = " + DateUtil.now());}private void processCommand() {try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + command +" 处理任务逻辑。。。。。。。。");} catch (InterruptedException e) {Thread.currentThread().interrupt();e.printStackTrace();}}@Overridepublic String toString() {return this.command;}
}
- 线程池
/*** @author itender* @date 2023/8/7 14:37* @desc*/
public class ThreadPoolExecutorDemo {private static final int CORE_POOL_SIZE = 5;private static final int MAX_POOL_SIZE = 10;private static final int QUEUE_CAPACITY = 100;private static final Long KEEP_ALIVE_TIME = 1L;public static void main(String[] args) {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(// 核心线程数 5CORE_POOL_SIZE,// 最大线程数 10MAX_POOL_SIZE,// 超过核心线程数,线程最大存活时间KEEP_ALIVE_TIME,// 时间单位TimeUnit.MINUTES,// 工作队列最大值new ArrayBlockingQueue<>(QUEUE_CAPACITY),// 线程工厂,创建线程的时候使用r -> {Thread thread = new Thread(r);thread.setName("pool-");return thread;},new ThreadPoolExecutor.CallerRunsPolicy());for (int i = 0; i < 10; i++) {// 创建任务Worker myRunnable = new Worker("" + i);// 执行任务threadPoolExecutor.execute(myRunnable);}// 种植线程池,不接受新任务,但是有工作线程处理队列中的任务threadPoolExecutor.shutdown();while (!threadPoolExecutor.isTerminated()) {}System.out.println("Finished All Threads!");}
}
2.3 核心参数
2.3.1 核心参数详解
- corePoolSize:核心线程数,任务队列没有达到队列最大容量时,最大可以同时运行的线程数。
- maximumPoolSize:最大线程数。当任务队列中存储的任务达到队列的容量时,当前可以同时运行的线程数量变为最大线程数。
- keepAliveTime:线程池中的线程数量超过
corePoolSize时,如果没有新任务提交,核心线程外的线程不会立即销毁,而是等待,直到等待的时间超过了keepAliveTime才会被销毁回收。 - unit:
keepAliveTime参数的时间单位。 - workQueue:工作队列。当有新的任务提交的时候,会先判断当前运行的线程数是否达到核心线程数,如果达到核心线程数,则会把新提交的任务放到工作队列中。
- threadFactory:线程工厂,创建新的线程时会使用。
- handler:拒绝策略。
2.3.2 拒绝策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:
- AbortPolicy:默认拒绝策略。抛出
RejectExecutionException来拒绝新任务的处理。 - CallerRunsPolicy:调用当前提交任务的线程来执行任务。一般不希望任务丢失会选用这种策略,但从实际角度来看,原来的异步调用意图会退化为同步调用。
- DiscardPolicy:不处理新任务,直接丢弃。
- DiscardOldestPolicy:丢弃最早的未处理的任务。
2.4 执行流程

2.5 线程池状态
2.5.1 线程池核心属性ctl
// ctl本质是 Integer 型变量,进行了原子性的封装// ctl表示两种状态:// 高3位:线程池当前的状态// 低29位:线程池当前工作线程的数量private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));// COUNT_BITS 的值为 29(整型Integer.SIZE = 32 位);private static final int COUNT_BITS = Integer.SIZE - 3;// CAPACITY = (1 << 29) - 1; 1左移29位,减去1;即1*2^29-1;// 0001 1111 1111 1111 1111 1111 1111 1111// 低29位用来表示线程池的最大线程容量private static final int CAPACITY = (1 << COUNT_BITS) - 1;// 高3位用来表示线程池5种状态// 111 运行状态private static final int RUNNING = -1 << COUNT_BITS;// 000 shutdown状态private static final int SHUTDOWN = 0 << COUNT_BITS;// 001 停止状态private static final int STOP = 1 << COUNT_BITS;// 010 过渡状态private static final int TIDYING = 2 << COUNT_BITS;// 011 中介状态private static final int TERMINATED = 3 << COUNT_BITS;// 根据ctl的值,计算当前线程池的状态// 计算方式:c 与 非capacityprivate static int runStateOf(int c) { return c & ~CAPACITY; }// 根据ctl的值,计算线程池当前运行的线程的容量private static int workerCountOf(int c) { return c & CAPACITY; }// 通过运行状态和工作线程数计算ctl的值,或运算private static int ctlOf(int rs, int wc) { return rs | wc; }private static boolean runStateLessThan(int c, int s) {return c < s;private static boolean runStateAtLeast(int c, int s) {return c >= s;}private static boolean isRunning(int c) {return c < SHUTDOWN;}/*** Attempts to CAS-increment the workerCount field of ctl.*/private boolean compareAndIncrementWorkerCount(int expect) {return ctl.compareAndSet(expect, expect + 1);}/*** Attempts to CAS-decrement the workerCount field of ctl.*/private boolean compareAndDecrementWorkerCount(int expect) {return ctl.compareAndSet(expect, expect - 1);}/*** Decrements the workerCount field of ctl. This is called only on* abrupt termination of a thread (see processWorkerExit). Other* decrements are performed within getTask.*/private void decrementWorkerCount() {do {} while (! compareAndDecrementWorkerCount(ctl.get()));}
2.5.2 状态切换

- RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
- SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用
shutdown()方法会使线程池进入到该状态。(finalize()方法在执行过程中也会调用shutdown()方法进入该状态)。 - STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用
shutdownNow()方法会使线程池进入到该状态。 - TIDYING:如果所有的任务都已终止了,
workerCount(有效线程数) 为0,线程池进入该状态后会调用terminated()方法进入 TERMINATED 状态。 - TERMINATED:在
terminated()方法执行完后进入该状态,默认terminated()方法中什么也没有做。
3. 源码分析
3.1 execute方法
- 源码
public void execute(Runnable command) {// 判断任务是否为空,如果任务为空,抛出空指针异常if (command == null)throw new NullPointerException();// 获取ctl属性int c = ctl.get();// 判断当前工作线程数量是否小于核心线程的数量if (workerCountOf(c) < corePoolSize) {// 工作线程数小于核心线程数,创建一个核心线程执行command任务if (addWorker(command, true))// 创建核心线程成功,直接返回return;// 并发情况下添加核心线程失败,需要重新获取ctl属性c = ctl.get();}// 创建核心线程失败,当前工作线程数量大于或等于核心线程数量corePoolSize// 判断线程池的状态是否为running,如果是添加任务到工作队列中(放入任务失败返回false)if (isRunning(c) && workQueue.offer(command)) {// 任务添加到队列成功,再次获取ctl属性int recheck = ctl.get();// 二次检查,判断线程池的状态是否为running,如果不是队列中移除刚刚添加的任务if (!isRunning(recheck) && remove(command))// 执行拒绝策略reject(command);// 1.任务添加到队列// 2.线程池可能是running状态// 3.传入的任务可能从任务队列中移除失败(移除失败的唯一可能就是任务已经被执行了)// 判断工作线程数量是否为0else if (workerCountOf(recheck) == 0)// 工作线程数量为0// 工作队列中有任务在排队,添加一个空任务,创建非核心线程执行队列中等待的任务addWorker(null, false);}// 创建核心线程失败,// 线程池状态不是running状态// 线程池可能是running状态,但是任务队列已经满了// 添加任务到工作队列失败,创建非核心线程执行任务else if (!addWorker(command, false))// 创建非核心线程失败,执行拒绝策略reject(command);}
第一点核心:通过execute方法源码可以看出线程池具体的执行流程,以及一些避免并发情况的判断。
第二点核心:线程池为什么会添加空任务非核心线程到线程池。
这里是一个疑惑点:为什么需要二次检查线程池的运行状态,当前工作线程数量为0,尝试创建一个非核心线程并且传入的任务对象为null?这个可以看API注释:
如果一个任务成功加入任务队列,我们依然需要二次检查是否需要添加一个工作线程(因为所有存活的工作线程有可能在最后一次检查之后已经终结)或者执行当前方法的时候线程池是否已经shutdown了。所以我们需要二次检查线程池的状态,必须时把任务从任务队列中移除或者在没有可用的工作线程的前提下新建一个工作线程。
3.2 addWorker方法
- 源码
private boolean addWorker(Runnable firstTask, boolean core) {// for循环标识// 对线程池当前状态和当前工作线程数量的判断retry:for (;;) {// 获取线程池的状态int c = ctl.get();int rs = runStateOf(c);// Check if queue empty only if necessary.if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;for (;;) {// 获取线程池工作线程的数量int wc = workerCountOf(c);// 1. 如果传入的core为true,表示将要创建核心线程,通过wc和corePoolSize判断,如果wc >= corePoolSize,则返回false表示创建核心线程失败// 2. 如果传入的core为false,表示将要创非建核心线程,通过wc和maximumPoolSize判断,如果wc >= maximumPoolSize,则返回false表示创建非核心线程失败// core参数为false说明工作队列已经满了,线程池大小变为maximumPoolSize最大线程数if (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))return false;// CAS更新工作线程数wc,原子操作将workCount的数量加1,更新成功则直接跳出最外层循环if (compareAndIncrementWorkerCount(c))break retry;// CAS更新工作线程数失败,判断线程池的状态是否从running编程shutdown,如果线程池的状态改变了在执行上面的操作c = ctl.get(); // Re-read ctl// 如果线程池状态已经变成shutdown,跳过最外层本次循环,执行下一次循环if (runStateOf(c) != rs)continue retry;// 如果线程池状态依然是RUNNING,CAS更新工作线程数wc失败说明有可能是并发更新导致的失败,则在内层循环重试即可 // else CAS failed due to workerCount change; retry inner loop}}// 工作线程是否启动成功boolean workerStarted = false;// 工作线程是否创建成功boolean workerAdded = false;Worker w = null;try {w = new Worker(firstTask);final Thread t = w.thread;if (t != null) {// 加锁,因为会改变一些指标值和非线程安全的集合final ReentrantLock mainLock = this.mainLock;// 加锁mainLock.lock();try {// 获取线程池状态int rs = runStateOf(ctl.get());//rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中//(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker// 对于2,换言之,如果线程池处于SHUTDOWN状态下,同时传入的任务实例firstTask不为null,则不会添加到工作线程集合和启动新的Worker// 这一步其实有可能创建了新的Worker实例但是并不启动(临时对象,没有任何强引用),这种Worker有可能成功下一轮GC被收集的垃圾对象// firstTask == null证明只新建线程而不执行任务if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();// 将新建的工作线程添加到工作线程的集合workers.add(w);// 更新当前工作线程的最大容量int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;// 工作线程是否添加成功workerAdded = true;}} finally {// 释放锁mainLock.unlock();}// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例if (workerAdded) {// 启动线程,标识线程启动成功t.start();workerStarted = true;}}} finally {// 线程启动失败,需要从工作线程中移除对应的Workerif (!workerStarted)addWorkerFailed(w);}return workerStarted;
}
4. 线程池常见问题
4.1 execute() 和submit()的区别
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法的话,如果在timeout时间内任务还没有执行完,就会抛出java.util.concurrent.TimeoutException。
4.3 阻塞队列的作用
-
一般的队列只能是有限长度的缓冲区,一旦超出缓冲长度,就无法保留了。阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
-
阻塞队列可以在队列中没有任务时,阻塞想要获取任务的线程,使其进入wait状态,释放cpu资源。
-
阻塞队列带有阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源。
4.2 为什么先添加队列而不是先创建最大线程
-
在创建新线程的时候,是要获取全局锁的,这时候其他线程会被阻塞,影响整体效率。
-
在核心线程已满时,如果任务继续增加那么放在队列中,等队列满了而任务还在增加那么就要创建临时线程了,这样代价低。
5. 参考文章
https://www.throwx.cn/2020/08/23/java-concurrency-thread-pool-executor/
https://javaguide.cn/java/concurrent/java-thread-pool-summary.html#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90
相关文章:
ThreadPoolExecutor线程池详解
ThreadPoolExecutor线程池详解 1. 背景 项目最近的迭代中使用到了ThreadPoolExecutor线程池,之前都只是知道怎么用,没有了解过线程池的底层原理,项目刚上线,有时间整理一下线程池的用法,学习一下线程池的底层实现与工…...
【VB6|第22期】用SQL的方式读取Excel数据
日期:2023年8月7日 作者:Commas 签名:(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释:如果您觉得有所帮助,帮忙点个赞,也可以关注我,我们一起成长;如果有不对的地方ÿ…...
融云:从「对话框」跳进魔法世界,AIGC 带给社交的新范式
8 月 17 日(周四),融云将带来直播课-《北极星如何协助开发者排查问题与预警风险?》欢迎点击上方报名~ AIGC 与社交结合的应用主要分两种,一是发乎于 AIGC,以大模型为基础提供虚拟伴侣等服务的 Appÿ…...
UWB伪应用场景 - 别再被商家忽悠
近几年UWB技术在网上宣传得如火如荼,与高精度定位几乎或等号,笔者认为这是营销界上的一大成功案例。 UWB超宽带技术凭借着低功耗、高精度,确实在物联网行业混得风生水起,但在无数实际应用案例中,根据客户的反馈情况&a…...
【快应用】list组件属性的运用指导
【关键词】 list、瀑布流、刷新、页面布局 【问题背景】 1、 页面部分内容需要瀑布流格式展示,在使用lsit列表组件设置columns进行多列渲染时,此时在里面加入刷新动画时,动画只占了list组件的一列,并没有完全占据一行宽度&…...
js 面试题总结
js 面试题总结 文章目录 js 面试题总结近百道面试题1、实现 子元素 在父元素中垂直居中的方式2、实现 子元素 在父元素中水平 垂直居中的方式3、描述 Keepealive 的作用,有哪些钩子函数,如何控制组件级存列表?4、请写出判断对象是数组的三个方法5、请说…...
HTML之表单标签
目录 表单标签 Form表单 定义: 基本语法结构: form属性: enctyoe属性 fieldeset标签 fieldeset属性 legend标签 label标签 优势 label属性 input标签 input属性 input标签中的type属性 text text输入框有以下配套属性 searc bu…...
Java经典面试题总结(一)
Java经典面试题总结(一) 题一:Java编译运行原理题二:JDK,JVM,JRE三者之间的关系题三:谈一下对冯诺依曼体系的了解题四:重载与重写的区别题五:拆箱装箱是指什么࿱…...
Android监听设备亮灭屏广播(动态广播代码)
MainActivity中 public class MainActivity extends Activity {private WakeAndLockReceiver wakeAndLockReceiver;Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//注册亮屏和息…...
【前端面试手撕题】简易深拷贝、深拷贝、寄生组合式继承、发布订阅模式、观察者模式
FED16 简易深拷贝 描述 请补全JavaScript代码,要求实现对象参数的深拷贝并返回拷贝之后的新对象。 注意: 参数对象和参数对象的每个数据项的数据类型范围仅在数组、普通对象({})、基本数据类型中]无需考虑循环引用问题 <!DO…...
【生物医学】应激(应激反应)全身适应综合征
最近在探索疲劳、负荷、应激方面的底层发生机制,遂整理了一些相关内容,以脑图方式呈现。本文以生物医学向为主。 OK,开始基础介绍:应激 (stress)是指在收到外部或内部、心理社会刺激下的非特异性适应反应。 本文主要收集整理了相…...
浅析基于安防监控EasyCVR视频汇聚融合技术的运输管理系统
一、项目背景 近年来,随着物流行业迅速发展,物流运输费用高、运输过程不透明、货损货差率高、供应链协同能力差等问题不断涌现,严重影响了物流作业效率,市场对于运输管理数字化需求愈发迫切。当前运输行业存在的难题如下…...
VBA技术资料MF41:VBA_将常规数字转换为文本数字
【分享成果,随喜正能量】时有落花至,远随流水香。人生漫长,不攀缘,不强求,按照自己喜欢的方式生活,不必太过在意,顺其自然就好。路再长也有终点,夜再黑也有尽头。 我给VBA的定义&am…...
Wavefront .OBJ文件格式解读【3D】
OBJ(或 .OBJ)是一种几何定义文件格式,最初由 Wavefront Technologies 为其高级可视化器动画包开发。 该文件格式是开放的,已被其他 3D 图形应用程序供应商采用。 OBJ 文件格式是一种简单的数据格式,仅表示 3D 几何体&…...
JavaScript:ES6中类与继承
在JavaScript编程中,ES6引入了一种更现代、更清晰的方式来定义对象和实现继承,那就是通过类和继承机制。本文将以通俗易懂的方式解释ES6中类与继承的概念,帮助你更好地理解和应用这些特性。 1. 类的创建与使用 类是一种模板,用于…...
通用指令(汇编)
一、数据处理指令1)数学运算数据运算指令的格式数据搬移指令立即数伪指令加法指令带进位的加法指令减法指令带借位的减法指令逆向减法指令乘法指令数据运算指令的扩展 2)逻辑运算按位与指令按位或指令按位异或指令左移指令右移指令位清零指令 3ÿ…...
苏宁数据治理实战方法论和三字经
随着移动互联网和大数据的蓬勃发展,“数据即资产”的理念深入人心。大数据已发展成为具有战略意义的生产资料,在各行各业发挥着极其重要的作用,而大数据也给很多企业带来了前所未有的自豪感和自信感。 但是,大数据真的是越“大”越…...
创建型设计模式:3、单例模式(C++实现实例 线程安全)
目录 1、单例模式(Singleton Pattern)的含义 2、单例模式的优缺点 (1)优点: (2)缺点: 3、C实现单例模式的示例(简单) 4、C实现单例模式的示例ÿ…...
JavaWeb学习笔记
Maven:自动导入配置jar包。 Maven项目架构管理工具:核心思想:约定大于配置 Maven:环境优化 1.修改web.xml为最新的 <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmlns.jcp.org/xml/ns/javaee&…...
ad+硬件每日学习十个知识点(24)23.8.4(时序约束,SignalTap Ⅱ)
文章目录 1.建立时间和保持时间2.为什么要建立时序约束?3.SignalTap Ⅱ4.SignalTap Ⅱ使用方法5.HDL的仿真软件(modelsim)6.阻抗匹配 1.建立时间和保持时间 答: 2.为什么要建立时序约束? 答: 3.Sign…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
ESP32读取DHT11温湿度数据
芯片:ESP32 环境:Arduino 一、安装DHT11传感器库 红框的库,别安装错了 二、代码 注意,DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
