• [专题汇总] 2025年开年的第一篇合集来了。速进
    大家好,2025年开年的第一篇合集,本次带来的是Python,Java,MySql,Golang,JSON,等等希望可以帮到大家。1.Python判断for循环最后一次的方法【转】https://bbs.huaweicloud.com/forum/thread-0248173698858425071-1-1.html2.使用Python实现高效的端口扫描器【转】https://bbs.huaweicloud.com/forum/thread-0248173699028101072-1-1.html3.使用Python实现操作mongodb详解【转】https://bbs.huaweicloud.com/forum/thread-02109173699263711070-1-1.html4.一文详解Python中数据清洗与处理的常用方法【转】https://bbs.huaweicloud.com/forum/thread-02109173699342905071-1-1.html5.Go中sync.Once源码的深度讲解【转】https://bbs.huaweicloud.com/forum/thread-0271173699402065058-1-1.html6.从源码解析golang Timer定时器体系【转】https://bbs.huaweicloud.com/forum/thread-0251173701525255062-1-1.html7.golang1.23版本之前 Timer Reset方法无法正确使用【转】https://bbs.huaweicloud.com/forum/thread-02127173701584637057-1-1.html8.Python文件读写实用方法小结【转】https://bbs.huaweicloud.com/forum/thread-02104173701685566070-1-1.html9.mysql外键创建不成功/失效如何处理【转】https://bbs.huaweicloud.com/forum/thread-02109173701958630072-1-1.html10.Redis的Zset类型及相关命令详细讲解【转】https://bbs.huaweicloud.com/forum/thread-02109173702031434073-1-1.html11.大数据小内存排序问题如何巧妙解决【转】https://bbs.huaweicloud.com/forum/thread-02127173702077058058-1-1.html12.Redis多种内存淘汰策略及配置技巧分享【转】https://bbs.huaweicloud.com/forum/thread-0272173702166312062-1-1.html13.MySQL通过binlog实现恢复数据【转】https://bbs.huaweicloud.com/forum/thread-02109173702268081074-1-1.html14.MySQL如何将一个表的字段更新到另一个表中【转】https://bbs.huaweicloud.com/forum/thread-0272173702328248063-1-1.html15.JSON字符串转成java的Map对象详细步骤【转】https://bbs.huaweicloud.com/forum/thread-02109173702572327075-1-1.html
  • [技术干货] 大数据小内存排序问题如何巧妙解决【转】
    大数据小内存排序问题,很经典,很常见,类似的还有比如 “如何对上百万考试的成绩进行排序” 等等。三种方法:数据库排序(对数据库设备要求较高)分治法(常见思路)位图法(Bitmap)方法概要数据库排序(对数据库设备要求较高)操作:将数据全部导入数据库,建立索引,数据库对数据进行排序,提取出数据。特点:操作简单, 运算速度较慢,对数据库设备要求较高。分治法(常见思路)操作:操作与归并排序的思想类似,都是分治。将数据进行分块,然后对每个数据块进行内部的排序(假如是对int形数据升序)。和归并排序类似,每个数据块取第一个数据(当前块的最小数据),然后比较取出的数据,取其最小加入结果集。重复2操作,直到取完所有数据,此时排序完毕。特点:位图法(Bitmap)操作:基本思想就是利用一位(bit)代表一个数字,例如第 3 位上为 1,则说明 3 这个数字出现过,若为0,则说明 3 这个数字没有出现过。很简单~​ java.util 封装了 BitSet 这样一个类,是位图法的典型实现。特点:可读性差(不是一般的差 🤔)位图存储的元素个数虽然比一般做法多,但是存储的元素大小受限于存储空间的大小。要想定义存储空间大小就需要实现知道存储的元素到底有多少对于有符号类型的数据,需要用 2 位来表示,比如 第 0 位和第 1 位表示 0 这个数据,第 2 位和第 3 位表示 1 这个数据......,这会让位图能存储的元素个数,元素值大小上限减半只知道元素是否出现,无法知道出现的具体次数
  • [技术干货] Volatile关键字在Java并发编程中的应用与解析
    个示例中,我们使用一个volatile修饰的布尔变量作为线程间的标志位,来控制一个线程的执行和停止。 public class VolatileExample { // 使用volatile修饰的布尔变量,作为线程间的标志位 private volatile boolean running = true; public static void main(String[] args) { VolatileExample example = new VolatileExample(); // 启动一个工作线程 Thread workerThread = new Thread(example::doWork); workerThread.start(); // 主线程休眠一段时间后,修改标志位以停止工作线程 try { Thread.sleep(5000); // 休眠5秒 } catch (InterruptedException e) { e.printStackTrace(); } example.stopWork(); // 确保工作线程已停止 try { workerThread.join(); // 等待工作线程结束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Worker thread has stopped."); } // 工作线程执行的方法 private void doWork() { while (running) { // 模拟工作负载 System.out.println("Worker is running..."); try { Thread.sleep(1000); // 每秒打印一次 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 保留中断状态 break; // 退出循环 } } System.out.println("Worker has stopped working."); } // 修改标志位以停止工作线程 public void stopWork() { running = false; // 修改volatile变量,确保对所有线程可见 } } 在这个示例中,running变量被声明为volatile,以确保当主线程修改其值时,工作线程能够立即感知到这个变化。工作线程在doWork方法中不断检查running变量的值,并根据其值决定是否继续执行模拟的工作负载。主线程在启动工作线程后,休眠5秒钟,然后调用stopWork方法来修改running变量的值,从而通知工作线程停止工作。六、结论volatile关键字在Java并发编程中扮演着重要角色,它确保了变量的可见性和禁止了指令的重排序。然而,它也存在一些局限性,如无法保证复合操作的原子性和可能增加内存开销等。因此,在使用volatile关键字时,需要谨慎考虑其适用场景和限制条件。在更复杂的并发场景中,可能需要使用synchronized关键字或其他并发工具类来确保线程安全。通过深入理解volatile关键字的特性和使用场景,并结合实际的代码示例进行实践,我们可以更好地利用Java并发编程的强大功能,构建出高效、稳定、可扩展的并发应用程序。
  • [问题求助] gateway接口方式平台反向检测业务系统请求断联
    【问题来源】【必填】南网电网【问题简要】【必填】如果业务系统犹豫网络或者环境问题导致签出时未能成功调用签出接口,但是将单轮循停止,平台是否会检测多长时间断联后将工号签出,如果有则检测时间是否可配置【问题类别】【必填】CC-Gateway【AICC解决方案版本】【必填】AICC 24.200.0【期望解决时间】【选填】尽快【问题现象描述】【必填】如果业务系统犹豫网络或者环境问题导致签出时未能成功调用签出接口,但是将单轮循停止,平台是否会检测多长时间断联后将工号签出,如果有则检测时间是否可配置【日志或错误截图】【可选】无【附件】【可选】无
  • [问题求助] 强制签入会中断通话
    【问题来源】【必填】南网电网【问题简要】【必填】通话过程中如果出现另一人进行强制签入操作,会将现有通话中断掉,有什么方法能够避免,或者在强制签入时检测到通话行为不进调用【问题类别】【必填】CC-Gateway【AICC解决方案版本】【必填】AICC 24.200.0【期望解决时间】【选填】尽快【问题现象描述】【必填】通话过程中如果出现另一人进行强制签入操作,会将现有通话中断掉,有什么方法能够避免,或者在强制签入时检测到通话行为不进调用【日志或错误截图】【可选】无【附件】【可选】无
  • [技术干货] Linux:线程控制
    一、线程库        在Linux中,内核中并没有很明确的线程概念,而是只有轻量级进程的概念!!因此OS并没有给我们提供线程的系统调用,只会给我们提供轻量级进程的系统调用——>可是我们的用户只认识线程而不认识什么轻量级进程啊!!而且使用起来的学习成本也很高啊! 因此就有大佬在应用层为轻量级进程接口进行封装,为用户提供直接的线程接口(pthread线程库)         pthread线程库又叫原生线程库,几乎所有的Linux平台都是默认自带这个库的,但是他对于g++来说属于第三方库,链接这些线程函数库时要使用编译器命令的“-lpthread”选项!二、线程创建pthread_create功能:创建一个新的线程int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);参数thread:返回线程ID(输出型参数)attr:设置线程的属性,attr为NULL表示使用默认属性(一般设为NULL)start_routine:是个函数地址,线程启动后要执行的函数(其实就是通过要执行的函数来给线程划分地址空间)arg:传给线程启动函数的参数(可以通过类传多个)返回值:成功返回0;失败返回错误码(pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通 过返回值返回)       pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码(局部存储)。对于pthreads函数的错误, 建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小 2.1 简单看看多线程 为什么-l就可以了呢??——>因为这个库已经默认安装在系统路径下了,编译器知道他在哪,只是不知道要链接哪个库而已!!   如果我们想查看所有的轻量级进程的话  可以用 ps -aL(a是all的意思,L是轻的意思)        我们会发现主线程的PID和LWP(cpu调度的基本单位)是一样的,这应该也是用来让CPU区分切换的是主线程还是次线程的一个标识!!监视线程的方法: 2.2 全局变量  所有的线程都可以看到全局变量       我们会发现以前的进程间通信,无论是管道、共享内存、消息队列……他们让两个进程看到同一份代码和资源的方法都比较麻烦,可以线程天生就具有看到同一份资源的能力,所以也给我们的通信提供了很好的应用场景和技术准备!! 问题:为什么我们不研究多进程并发,而是研究多线程并发呢??——>因为多进程的写时拷贝、通信……都很麻烦,而线程的共享性更容易实现,他是先进性的表现,但是方便的同时也伴随着线程之间的相互影响、健壮性差等问题,因此这些需要我们程序员在代码上去解决这类问题!!2.3 tid vs LWP  我们会发现tid和LWP差距非常大,因为lwp是操作系统的轻量级进程的概念,只需要OS知道就行,而tid是给用户使用的!(本质是一个地址)!  2.4 线程函数参数返回值为啥都是void*以往进程返回是通过返回错误码来告知我们错误信息,可以线程中的函数为什么会是void*呢??因为不止可以传整形、字符串……还可以传类对象!! (类里面可以放很多内置类型,其实就相当于可以传很多参数,以及返回很多返回值)即使你只想传一个整形或者字符串,你也可以封装在类里面传,能传类的话尽量传类,因为他具有可扩展性!未来想增加别的类型就很方便! 比方说我们要计算1-100相加,我们可以写个request的类传递给他1-100的区间,然后再写个Respond的类帮助我们把运行结果返回回来!! 要注意一定不要在主线程里面创建局部变量传递给次线程!!        如果我们主线程要传类对象给次线程,就必须在堆区开辟空间,这样虽然td指针被释放了,但是我们可以通过args把这个指针传递给线程,这样每个线程就可以去访问自己在堆中的对象了!     其实堆区的资源大家都看得到,比如我2号线程也可以去看1号线程堆区的数据,但是这样没有意义!!所以线程可以看到全部的堆空间,但是每个线程访问的是堆的不同位置!!问题:可是我们为什么不直接在线程里去写这个参数,而是要让主线程通过类传递过去呢??——> 因为主线程可能需要给不止一个次线程分配任务,比如说我想让1线程算1-100,让2线程算101-200…… 也就是可以让每个线程并行地去共同完成同一个任务,而我只需要讲需要处理的数据通过类告诉他们就行,最后我再对结果进行汇总(主线程重分配和管理,次线程重实践)——>甚至你还可以把方法都写进类里面!!这样你的线程就更简洁了!!——>你次线程需要什么类,需要什么方法,我可以通过类来告诉你!!你只管调用就行! 三、线程等待pthread_ join你主线程把我新线程创建出来了,你不得管我吗??万一我还没退你先退了怎么办??——>所以我们要尽量保证主线程最后退!怎么让主线程最后退呢??你可能会想到写个死循环,然后把工作都交给次线程去干,这是这样真的好吗??我只是想让你管理我,不是想让你当甩手掌柜然后自己啥代码也不执行,而且我要是自己退了,你就搁那傻傻循环啥也不管吗??你难道不关心我的运行结果吗??你难道不需要释放我的空间吗??——>所以你主线程必须要等待子线程!!(1、将已经退出的线程的空间释放掉 2、创建新的线程时不会复用刚在退出线程的地址空间)功能:等待线程结束int pthread_join(pthread_t thread, void **value_ptr);参数:thread:线程IDvalue_ptr:它指向一个指针,后者指向线程的返回值(得知新线程的运行情况)返回值:成功返回0;失败返回错误码       调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的 终止状态是不同的 总结如下:1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。 2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。 3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。 问题:为什么是void**呢?? ——> 因为OS作为管理者也需要知道执行结果,这个执行结果会先被携带结构体里,然后我们可以通过二级指针将我们自己的void*变量地址传递给他,然后把他拷贝过来!!四、线程分离pthread_detach和pthread_self           默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join(需要由主线程回收)操作,否则无法释放 资源,从而造成系统泄漏。       如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。 讲个小故事理解分离:比如说五六十年代那个时候,很多家庭里面会一次性有很多小孩由父母管理,然后这些小孩长大以后,有其中一个小孩跟父亲特别不对付(一种情况是小孩自己提出分家——子线程自己想分离  还有一种情况是父亲嫌弃你让你离开——父线程要求你分离),而这个小孩虽然可以共享家里的一部分资源,但是其实已经不是一家人了!!所以不管你以后怎么样了,父亲都不会管你了(就相当于线程分离之后,虽然他还可以用到进程的公共资源,并且他也有自己idea资源,但是父线程已经不关心他了  此时以后不管怎么都没人管他  只能自生自灭由OS去回收他)所以可以由线程组内的其他线程对目标线程进行分离,也可以是线程自己分离!! pthread_detach:int pthread_detach(pthread_t thread);  pthread_self pthread_t pthread_self(void); 可以获得线程自身的ID joinable和分离是冲突的,一个线程不能既是joinable又是分离的。——本质上就是将我们线程库中我们认为的tcp结构体里的一个关于线程是否分离的标记位给改了!!五、线程终止pthread_exit和pthread_cancel只终止某个线程而不终止整个进程,可以有三种方法:1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。2. 线程可以调用pthread_ exit终止自己。3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程pthread_exit函数功能:线程终止 void pthread_exit(void *value_ptr);参数value_ptr:value_ptr不要指向一个局部变量(独立栈空间会被释放)。返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(线程都终止了返回没有意义) pthread_cancel函数功能:取消一个执行中的线程 int pthread_cancel(pthread_t thread);参数thread:线程ID返回值:成功返回0;失败返回错误码六、c++的线程库 C++其实也有自己的线程库thread ! 可实际上他的底层也是封装的pthread的原生线程库!也需要指定链接!       而以往我们在windows系统下在vs中使用线程库,我们其实并不需要这样,这是因为windows下他有自己专门的线程库,因为windows实现的时候就是有专门的tcb结构体,所以我们包cpp头文件的时候,他的底层其实是windows类型的系统调用!!——>cpp具有跨平台性,根据不同的平台(Linux和windows),他用的是条件编译,外面虽然呈现出来的头文件和接口是一样的,但是不同的平台内部封装所使用的系统调用是不一样的!!——>所以在Linux下的cpp底层封装的是Linux的原生线程库(由于是用的进程模拟线程,所以并没有专门的tcb结构体,他的系统调用接口只有轻量级进程的概念,所以又封装了一个原生线程库给我们,而使用第三方库都需要链接,cpp底层也是这个原生线程库,所以也要链接)  而windows下的线程库就是原生windows下的系统调用,所以他并没有第三方库的概念!! ——>所以你平时写代码在不同的环境下没有感觉,是写库文件的设计者帮助你把这种差异给屏蔽掉了!! 所以我们平时刚推荐使用语言里的库方法而非系统调用接口,因为这样代码就不具备可移植性和跨平台性了!! 七、用户级线程vs内核级LWP用户级线程和内核级LWP是1:1的关系  线程共享进程数据,但也拥有自己的一部分数据:线程ID一组寄存器(保存上下文)独立栈 (完成调用链)errno (局部存储)信号屏蔽字调度优先级      进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id1、 线程的概念是库给我们维护的!!所以线程库注定要维护多个线程属性集合!——>先描述再组织2、不用维护线程的执行流,这是由OS的轻量级进程完成的(已经帮我们封装了) 3、原生线程库必然要被加载到内存中,因此我们的线程属性集合也应该在线程库中维护4、线程控制块就是库帮我们维护的一个用户级线程结构体tcb,而pthread_t类型的线程ID,本质 就是一个进程地址空间上的一个地址,就是指向他的。  7.1 独立线程栈       执行流的本质就是独立的调用链!! 所以每个线程都需要建立自己独立的调用链,所以就必须得有一个独立的栈结构——>支持我们在应用层来完成我们整个调用链对应的临时空间的开辟和释放——>这样对于局部变量来说,就可以保持线程的独立性        虽然是独立栈,但其实其他线程想要访问在技术角度也是可以做到的(定义一个全局的指针,然后在某一个线程中让他保存其中的一个局部变量的地址,然后主线程再当全部线程创建完成之后,再去查看这个全局的指针变量),因为线程与线程之间几乎没有秘密!! 7.2 局部存储 如果我们想要一个只属于线程的全局变量呢??——>通过局部存储(他会被存储在一个区域中  )!   问题:可是这看上去很鸡肋啊!!干嘛要定义这种私有的全局变量啊,我直接在自己的独立栈定义局部变量不就行了??——>可是如果你的线程内部将来也调用函数了呢??比如说你想让别的函数也能够知道你线程的id或者是其他属性,那你还得把这个局部变量通过参数传递给他!! 所以局部存储私有的全局变量最核心的意义就是可以让该线程独立栈内部调用链上所有的函数都可以看得到这些信息,而不需要传参或者是频繁地调用系统调用!————————————————                        原文链接:https://blog.csdn.net/weixin_51142926/article/details/142829794
  • [技术干货] Java 报错:找不到或无法加载主类
    引言在 Java 开发过程中,经常会遇到“找不到或无法加载主类”(Error: Could not find or load main class)的错误。这个错误通常表示 JVM 无法找到指定的主类,可能是由于类路径(Classpath)设置不正确、类文件缺失、编译错误等原因引起的。1. 错误描述当运行 Java 应用程序时,如果 JVM 无法找到指定的主类,会抛出以下错误:Error: Could not find or load main class <ClassName>其中 <ClassName> 是你尝试运行的主类名称。2. 常见原因以下是导致“找不到或无法加载主类”错误的一些常见原因:2.1 类路径设置错误类路径未包含主类:确保类路径(Classpath)中包含了主类所在的目录或 JAR 文件。类路径格式错误:确保类路径的格式正确,特别是多个路径之间的分隔符(Windows 使用 ;,Linux 使用 :)。2.2 类文件缺失编译错误:确保所有 Java 源文件已经成功编译,并且生成了相应的 .class 文件。文件路径错误:确保主类文件位于正确的目录中,且文件名和类名一致。2.3 主类声明错误缺少 public static void main(String[] args) 方法:确保主类中有一个 public static void main(String[] args) 方法,这是 JVM 入口点。类名拼写错误:确保命令行中指定的类名与实际类名完全一致,包括大小写。2.4 JAR 文件问题JAR 文件损坏:确保 JAR 文件没有损坏,并且包含所需的类文件。MANIFEST 文件错误:如果使用 JAR 文件,确保 MANIFEST 文件中的 Main-Class 属性正确指定了主类。3. 诊断方法以下是诊断“找不到或无法加载主类”错误的一些方法:3.1 检查类路径打印类路径:在命令行中使用 echo %CLASSPATH%(Windows)或 echo $CLASSPATH(Linux)命令,检查当前的类路径设置。手动验证:确保类路径中包含了主类所在的目录或 JAR 文件。3.2 检查类文件编译源文件:重新编译所有 Java 源文件,确保生成了 .class 文件。检查文件路径:确保主类文件位于正确的目录中,且文件名和类名一致。3.3 检查主类声明查看源代码:打开主类的源代码文件,确保有 public static void main(String[] args) 方法。检查类名:确保命令行中指定的类名与实际类名完全一致,包括大小写。3.4 检查 JAR 文件验证 JAR 文件:使用 jar tf <jar-file> 命令检查 JAR 文件中的内容,确保包含所需的类文件。检查 MANIFEST 文件:打开 JAR 文件中的 MANIFEST.MF 文件,确保 Main-Class 属性正确指定了主类。4. 解决方案根据诊断结果,采取相应的解决方案:4.1 修正类路径设置类路径:在命令行中使用 -cp 或 -classpath 参数指定类路径。例如:java -cp .;path/to/classes com.example.MainClass环境变量:确保 CLASSPATH 环境变量正确设置。例如,在 Windows 中:set CLASSPATH=.;path\to\classes4.2 重新编译类文件编译源文件:使用 javac 命令重新编译所有 Java 源文件。例如:javac -d . com/example/MainClass.java4.3 修正主类声明添加 main 方法:确保主类中有一个 public static void main(String[] args) 方法。例如:package com.example; public class MainClass {    public static void main(String[] args) {        System.out.println("Hello, World!");    }}检查类名:确保命令行中指定的类名与实际类名完全一致,包括大小写。例如:java com.example.MainClass4.4 修复 JAR 文件重新打包 JAR 文件:使用 jar 命令重新打包 JAR 文件。例如:jar cvf myapp.jar -C path/to/classes .更新 MANIFEST 文件:确保 MANIFEST.MF 文件中的 Main-Class 属性正确指定了主类。例如:Main-Class: com.example.MainClass5. 示例以下是一个完整的示例,展示了如何编译和运行一个简单的 Java 应用程序:5.1 创建源文件创建一个名为 MainClass.java 的文件,内容如下:package com.example; public class MainClass {    public static void main(String[] args) {        System.out.println("Hello, World!");    }}5.2 编译源文件在命令行中导航到源文件所在目录,编译源文件:mkdir -p com/examplemv MainClass.java com/example/javac -d . com/example/MainClass.java5.3 运行应用程序确保类路径设置正确,运行应用程序:java -cp . com.example.MainClass6. 总结“找不到或无法加载主类”错误通常是由于类路径设置错误、类文件缺失、主类声明错误或 JAR 文件问题引起的。通过仔细检查类路径、类文件、主类声明和 JAR 文件,可以快速定位和解决这个问题。————————————————                        原文链接:https://blog.csdn.net/2401_85648342/article/details/143706066
  • [技术干货] Java 中的 LocalDateTime、DateTime 和 Date 的区别解析
    在 Java 中,处理日期和时间是开发中常见的任务之一,特别是在涉及到多个时区、日期格式、时间计算等需求时。Java 提供了多种方式来处理日期和时间,其中 LocalDateTime、DateTime 和 Date 是三种常见的日期时间类。尽管它们看起来有些相似,但它们的设计理念和应用场景却各有不同。本文将深入分析这三者的区别,帮助大家更好地理解它们的使用场景。一、LocalDateTime:新的 Java 8 日期时间 API1.1 LocalDateTime 简介LocalDateTime 是 Java 8 中引入的 java.time 包的一部分,它代表了没有时区信息的日期和时间。它只包含 年、月、日、时、分、秒、纳秒 信息,不涉及与时区或具体的时间点相关的数据。1.2 设计理念LocalDateTime 设计的目标是解决传统 java.util.Date 类中存在的许多问题,提供一个清晰、直观的 API 来处理日期和时间。由于它没有时区信息,它非常适合表示 本地时间,例如在某个特定地点的时间,且不受时区转换的影响。1.3 适用场景本地日期时间处理:例如,我们只关心某个事件发生的日期和时间,但不关心该事件发生的时区。与数据库交互:当你存储和操作不涉及时区的日期时间(比如某些日历系统或事务记录)时,LocalDateTime 是一个理想的选择。1.4 示例代码import java.time.LocalDateTime;import java.time.format.DateTimeFormatter; public class LocalDateTimeExample {    public static void main(String[] args) {        LocalDateTime now = LocalDateTime.now();  // 获取当前的本地日期和时间        System.out.println("当前本地日期时间: " + now);         // 格式化输出        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");        System.out.println("格式化后的日期时间: " + now.format(formatter));    }}输出结果:当前本地日期时间: 2024-11-13T12:45:30.123456789格式化后的日期时间: 2024-11-13 12:45:30二、DateTime:没有明确标准的类2.1 DateTime 的模糊性在 Java 标准库中,并没有直接命名为 DateTime 的类。通常,在一些库或框架中,DateTime 用来指代 日期和时间的组合。例如,Java 8 的 ZonedDateTime 和 LocalDateTime 都可能被通称为 DateTime,尽管它们是不同的类。因此,DateTime 并没有明确的标准定义,通常只是作为一个通用术语,用来描述所有涉及日期和时间的类。2.2 适用场景在一些第三方库(如 Joda-Time)或框架中,DateTime 被广泛用于表示日期和时间。如果使用这些库,可能会遇到 DateTime 这个类。在 Java 中,如果遇到 DateTime,我们可能需要进一步查看它具体是哪个类,例如 ZonedDateTime 或 OffsetDateTime。三、Date:老旧的日期时间类3.1 Date 简介java.util.Date 是 Java 中最早的日期和时间类之一。它代表自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数,直到 Java 8 被新的日期时间 API 取代。Date 类最初设计上存在诸多缺陷,因此不再推荐用于新的项目开发。3.2 设计缺陷时区问题:Date 类本身并不包含时区信息,且会根据所在的系统时区进行转换,导致时区处理不够精确。不便的 API:许多方法(如 getYear())的返回值并不直观,而且对日期的操作不便,缺少流畅的日期计算方法。精度不足:Date 的精度仅限于毫秒,不能处理纳秒级别的时间。3.3 适用场景兼容旧代码:如果你需要与旧版的 API 进行交互,Date 可能仍然是不可避免的。时间戳:由于 Date 内部使用毫秒表示时间,因此它仍然适用于一些需要表示时间戳的场景。3.4 示例代码import java.util.Date; public class DateExample {    public static void main(String[] args) {        Date date = new Date();  // 获取当前日期和时间        System.out.println("当前日期时间: " + date);    }}输出结果:当前日期时间: Wed Nov 13 12:45:30 GMT 2024四、如何选择合适的日期时间类?特性    LocalDateTime    DateTime(通常指 ZonedDateTime、OffsetDateTime 等)    Date(旧的 java.util.Date)时区信息    无时区信息    ZonedDateTime 有时区信息,LocalDateTime 没有时区信息    Date 依赖于系统时区精确度    纳秒精度    视具体类型而定,如 ZonedDateTime 支持到纳秒精度    毫秒精度设计方式    Java 8 引入的新日期时间 API,推荐使用    DateTime 不是标准类,通常是指 LocalDateTime 或 ZonedDateTime 等    旧的 API,设计不够清晰,容易出错功能    只包含日期和时间,没有时区    包含日期和时间,可以有时区(如 ZonedDateTime)    仅表示一个时间点,设计较为原始使用推荐    推荐用于不需要时区的日期时间操作    ZonedDateTime 或 OffsetDateTime 用于处理带时区的日期时间    不推荐使用,除非需要兼容旧代码1. LocalDateTime:适用于本地日期和时间的场景,不需要时区处理。2. ZonedDateTime:适用于需要时区处理的场景,适合跨时区的日期时间计算。3. Date:仅在兼容旧代码或需要处理时间戳时使用,不推荐在新项目中使用。在 Java 8 之后,我更推荐使用新的日期时间 API(java.time 包中的类),这些类设计更加清晰,功能更强大,避免了 java.util.Date 中的很多问题。————————————————                        原文链接:https://blog.csdn.net/Y_1215/article/details/143755420
  • [技术干货] Java stream流中peek用法详解
    在Java中,Stream是一种用于处理集合数据的强大工具。它提供了一种函数式编程的方式来对数据进行操作和转换。Stream中的peek方法是一种非终端操作,它允许你在流的每个元素上执行一个操作,而不会改变流的内容。peek方法的语法如下:Stream<T> peek(Consumer<? super T> action)其中,action是一个接收一个元素并执行操作的函数。peek方法的主要作用是在流的每个元素上执行一个操作,比如打印元素的值、记录日志、调试等。它通常用于调试和观察流的中间状态,而不会对流的内容进行修改。下面是一个使用peek方法的简单示例:List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> doubledNumbers = numbers.stream() .peek(n -> System.out.println("Processing number: " + n)) .map(n -> n * 2) .collect(Collectors.toList());在上面的示例中,我们创建了一个整数列表numbers,然后通过流的方式对每个元素进行处理。在流的peek操作中,我们打印了每个数字的值。然后,我们使用map操作将每个数字乘以2,并将结果收集到一个新的列表中。当我们运行上面的代码时,会看到以下输出:Processing number: 1 Processing number: 2 Processing number: 3 Processing number: 4 Processing number: 5通过使用peek方法,我们可以观察到流中每个元素的处理过程。这对于调试和理解流的中间状态非常有用。需要注意的是,peek方法是一个中间操作,它不会触发流的终端操作。如果你希望对流的内容进行修改或者获取最终的结果,你需要在peek方法之后添加一个终端操作,比如collect、forEach等。总结起来,peek方法是一个在流的每个元素上执行操作的非终端操作。它通常用于调试和观察流的中间状态,而不会对流的内容进行修改。原文链接:https://gitcode.csdn.net/65e957c41a836825ed78f822.html
  • [技术干货] Java——Stream流的peek方法详解
    Java 8 中引入了Stream API,极大地简化了集合操作,使得开发者可以使用流的方式进行数据处理。Stream 提供了一系列非常强大的操作方法,其中之一就是 peek() 方法。peek() 是一个中间操作,它可以用来在操作流的过程中查看元素的处理状态。本文将详细介绍 peek() 方法的使用场景和原理,并配合代码示例帮助大家深入理解。一、peek() 方法简介peek() 方法的定义在 java.util.stream.Stream 接口中,其签名如下:Stream<T> peek(Consumer<? super T> action);作用:peek() 是一个中间操作,它允许我们在流的每个元素上执行一个操作,但并不会改变流中的元素或中断流的处理。常用作调试工具,用来在流的各个操作步骤中查看流中的数据。它接收一个 Consumer 函数作为参数,Consumer 函数可以对每个流中的元素执行某些动作。特点:peek() 不会消耗流,只是执行一个旁路行为。因为是中间操作,它不会触发终端操作,因此在调用完 peek() 后,还需要调用诸如 forEach()、collect() 这类终端操作来触发流的处理。示例代码:import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;public class PeekExample {    public static void main(String[] args) {        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);        // 使用peek方法调试流操作过程        List<Integer> result = numbers.stream()                .filter(n -> n % 2 == 0)  // 过滤出偶数                .peek(n -> System.out.println("Filtered: " + n))  // 查看过滤结果                .map(n -> n * n)  // 对偶数进行平方                .peek(n -> System.out.println("Mapped: " + n))  // 查看映射结果                .collect(Collectors.toList());  // 收集结果        System.out.println("最终结果: " + result);    }}输出结果:Filtered: 2Mapped: 4Filtered: 4Mapped: 16最终结果: [4, 16]在上面的示例中,peek() 用来查看流中元素的处理情况,展示了在经过 filter() 和 map() 操作后的数据变化。二、peek() 方法的常见使用场景2.1 调试流操作peek() 的主要用途之一是调试。当我们处理复杂的流操作链时,可能很难理解每个中间操作的效果。这时,可以通过 peek() 来查看流中的数据在每个操作后的变化,以便找到问题或验证逻辑是否正确。import java.util.Arrays;import java.util.List;public class DebugWithPeek {    public static void main(String[] args) {        List<String> words = Arrays.asList("apple", "banana", "cherry", "date");        words.stream()                .filter(w -> w.length() > 4)                .peek(w -> System.out.println("Filtered: " + w))                .map(String::toUpperCase)                .peek(w -> System.out.println("Mapped to upper case: " + w))                .forEach(System.out::println);    }}输出结果:Filtered: appleMapped to upper case: APPLEFiltered: bananaMapped to upper case: BANANAFiltered: cherryMapped to upper case: CHERRYAPPLEBANANACHERRY可以看到,peek() 方法被用于调试,以便我们看到 filter() 和 map() 操作后的字符串。2.2 记录日志在实际应用中,peek() 还可以用于记录流操作的执行过程,比如将流中每个元素的处理结果写入日志。这在数据处理链条较长时,尤为有用。import java.util.Arrays;import java.util.List;import java.util.logging.Logger;public class LogWithPeek {    private static final Logger logger = Logger.getLogger(LogWithPeek.class.getName());    public static void main(String[] args) {        List<Integer> numbers = Arrays.asList(10, 20, 30, 40, 50);        numbers.stream()                .filter(n -> n > 20)                .peek(n -> logger.info("After filter: " + n))                .map(n -> n / 2)                .peek(n -> logger.info("After map: " + n))                .forEach(System.out::println);    }}在这个例子中,peek() 被用于记录日志,通过 Logger 的 info() 方法记录流中每个元素的处理状态。2.3 数据检查与验证peek() 还可以用来对流中的数据进行检查与验证。当你想确认流中数据是否符合某种规则,但不希望中断流的处理时,peek() 是一个非常好的选择。import java.util.Arrays;import java.util.List;public class DataValidationWithPeek {    public static void main(String[] args) {        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");        names.stream()                .filter(name -> name.length() > 3)                .peek(name -> {                    if (name.startsWith("C")) {                        System.out.println("注意!名字以C开头: " + name);                    }                })                .forEach(System.out::println);    }}在这个示例中,peek() 方法用于检查名字是否以字母C开头,而不影响流的其他操作。三、与forEach()的区别peek() 和 forEach() 看似相似,都是用来对流中的元素进行操作,但它们有明显的区别:peek() 是中间操作,而 forEach() 是终端操作。peek() 通常用于调试或数据检查,因为它不会中断流的链式操作;而 forEach() 是用来最终消费流的元素。示例代码:import java.util.Arrays;import java.util.List;public class PeekVsForEach {    public static void main(String[] args) {        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);        // 使用peek()作为中间操作        numbers.stream()                .peek(n -> System.out.println("Peeked: " + n))                .map(n -> n * 2)                .forEach(System.out::println);        System.out.println("--------");        // 使用forEach()作为终端操作        numbers.stream()                .map(n -> n * 2)                .forEach(n -> System.out.println("ForEach: " + n));    }}输出结果:Peeked: 1Peeked: 2Peeked: 3Peeked: 4Peeked: 5--------ForEach: 2ForEach: 4ForEach: 6ForEach: 8ForEach: 10可以看到,peek() 用于在流操作中查看每个元素,而 forEach() 用于最终消费元素。四、注意事项惰性求值:peek() 是中间操作,具有惰性,只有在终端操作(如 forEach()、collect())调用时,流的处理才会被执行。不可用于修改流元素:peek() 不能修改流中的元素,它只用于执行副作用操作。如果需要修改元素的值,应使用 map() 方法。适用场景:peek() 最适合用于调试或监控流的中间状态,不应该滥用,否则可能会导致代码可读性降低。五、总结在Java的Stream API中,peek() 方法是一个强大的工具,它允许我们在流的处理中观察和调试数据,特别是在数据处理链比较长的情况下,它可以帮助我们跟踪流中元素的状态和变化。但需要注意的是,peek() 不能用于修改流的元素,更多地是用作调试、记录日志和数据检查的手段。通过丰富的代码示例,我们了解了peek() 的常见使用场景和注意事项。在实际开发中,合理使用peek() 可以极大地帮助我们调试和监控流操作,希望本文能帮助你深入理解并掌握peek()的使用。————————————————                        原文链接:https://blog.csdn.net/qq_42978535/article/details/142763452
  • [技术干货] Java中的数据库性能优化:索引、查询和连接池管理
    Java中的数据库性能优化:索引、查询和连接池管理大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!今天我们来探讨Java中的数据库性能优化,包括索引、查询优化和连接池管理。这些技术在提高应用程序的响应速度和系统的整体性能方面至关重要。一、索引索引是数据库性能优化的重要手段之一。它通过加快查询速度,提高数据检索效率。以下是关于索引的一些技术细节和Java中的应用示例。1. 创建索引在数据库中创建索引可以显著提高查询性能。以MySQL为例,可以使用以下SQL语句创建索引:CREATE INDEX idx_user_name ON users (name);1在Java中,使用JPA(Java Persistence API)可以在实体类上定义索引:package cn.juwatech.demo;import javax.persistence.*;@Entity@Table(name = "users", indexes = {@Index(name = "idx_user_name", columnList = "name")})public class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String name;// getters and setters}2. 使用合适的索引类型不同类型的索引适用于不同的场景。常见的索引类型有B树索引、哈希索引等。在选择索引类型时,应根据查询模式进行选择。例如,B树索引适用于范围查询,而哈希索引适用于等值查询。二、查询优化编写高效的SQL查询是数据库性能优化的关键。以下是一些常见的查询优化策略和Java中的应用示例。1. 避免全表扫描全表扫描会导致性能瓶颈,尤其是在数据量较大的情况下。使用索引可以避免全表扫描,提高查询效率。例如:SELECT * FROM users WHERE name = 'John';1如果在name列上有索引,上述查询会使用索引扫描而不是全表扫描。2. 使用批量操作批量操作可以减少数据库的往返次数,提高性能。在Java中,可以使用JDBC的批量操作功能:package cn.juwatech.demo;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;public class BatchUpdateExample {public static void main(String[] args) throws Exception {Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");String sql = "INSERT INTO users (name) VALUES (?)";PreparedStatement statement = connection.prepareStatement(sql);for (int i = 1; i <= 1000; i++) {statement.setString(1, "User" + i);statement.addBatch();}statement.executeBatch();statement.close();connection.close();}}3. 避免N+1查询问题在使用ORM(如Hibernate)时,N+1查询问题会导致大量的SQL查询,从而影响性能。使用fetch策略可以解决这个问题:package cn.juwatech.demo;import javax.persistence.*;import java.util.List;@Entitypublic class Department {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String name;@OneToMany(fetch = FetchType.LAZY, mappedBy = "department")private List employees;// getters and setters}@Entitypublic class Employee {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String name;@ManyToOne@JoinColumn(name = "department_id")private Department department;// getters and setters}三、连接池管理数据库连接池通过复用数据库连接,减少了连接的建立和关闭操作,从而提高了应用程序的性能。以下是常用的连接池管理工具及其配置示例。1. HikariCPHikariCP是一个高性能的JDBC连接池,广泛应用于Java项目中。以下是HikariCP的配置示例:package cn.juwatech.demo;import com.zaxxer.hikari.HikariConfig;import com.zaxxer.hikari.HikariDataSource;import javax.sql.DataSource;public class HikariCPExample {public static DataSource getDataSource() {HikariConfig config = new HikariConfig();config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");config.setUsername("user");config.setPassword("password");config.setMaximumPoolSize(10);return new HikariDataSource(config);}}2. Spring Boot中使用HikariCPSpring Boot默认使用HikariCP作为连接池。以下是Spring Boot配置示例:package cn.juwatech.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import org.springframework.beans.factory.annotation.Value;import com.zaxxer.hikari.HikariConfig;import com.zaxxer.hikari.HikariDataSource;import javax.sql.DataSource;@SpringBootApplicationpublic class SpringBootHikariCPApplication {public static void main(String[] args) {SpringApplication.run(SpringBootHikariCPApplication.class, args);}@Beanpublic DataSource dataSource(@Value("${spring.datasource.url}") String url,@Value("${spring.datasource.username}") String username,@Value("${spring.datasource.password}") String password) {HikariConfig config = new HikariConfig();config.setJdbcUrl(url);config.setUsername(username);config.setPassword(password);config.setMaximumPoolSize(10);return new HikariDataSource(config);}}总结通过合理使用索引、优化查询和管理连接池,可以显著提高Java应用程序的数据库性能。这些技术在处理大规模数据和高并发请求时尤为重要————————————————原文链接:https://blog.csdn.net/weixin_44409190/article/details/140782820
  • [技术干货] Java和Mysql——查询性能优化
    前言最近在解决用户反馈的时候发现有些用户查询效率太慢或者直接就查询sql超时(这里配的是默认15秒),然后就在考虑如何在百万级用户的情况下优化性能。一、查询效率缓慢原因Java层面第一层面是java代码原因,最常见的就是java写了太多的暴力循环(例如多重嵌套for循环),导致时间复杂度太高,就导致后端返回结果的时间过于缓慢,让用户体验十分差。mysql层面(数据库)第二层方面就是数据库原因,这里说mysql的情况,最常见的就是有些热数据字段没加索引导致慢查询甚至超时,sql写的不合理各种子查询联表查询,数据库表的设计不合理,还有一种情况是用户的数据量太大(主要原因);二、解决方案Java层面1、优化代码:减少不必要的循环,一般我是把某个业务理解透了,在for循环里边进行适当的continue或break关键字减少循环,如果算法比较厉害的人,可以把时间复杂度降到O(n)甚至O(logN),例如查找某个数用二分查找,也可以用递归通过空间换取性能的方式(注意堆栈溢出)2、异步操作:对某些不需要立马得到结果的数据,可以利用异步操作让用户感觉到加载迅速,先返回一个结果给用户,其余的数据用异步操作。这边可以使用多线程实现异步操作,我发现spring这里是有@Async关键字到方法上实现异步操作,创建线程池和线程都不需要自己担心,多线程的配置可以在配置文件上直接配置即可;还有一种方式是使用消息队列,例如rocketMQ,感兴趣的可以去看看消息队列的相关文章。数据库层面(mysql)1、花更多的钱:有土豪式买更多的数据库服务器,提供更多的节点,这样访问压力小的时候查询性能会更高。2、分库分表:这里分表一般都是水平分表(只根据数据量分),垂直分表的话需要把业务吃的很透,表设计十分合理难度较大,一般可以设定一定的公式进行水平分表,然后对应的用户查询对应的表数据,这样表里面的数据量也比较少,查询性能比较快。3、表设计:咱们也可以从表设计层面进行优化,例如对热数据字段加索引增加查询效率;或者在一些的查询主表里面加一些冗余字段,减少联表查询,不过加了之后需要在对该表进行新增、修改操作的业务都需要把该冗余字段给补上,需要十分熟悉业务。4、sql优化:一般都是sql写的不合理导致索引失效然后sql查询超时,最常见的就是减少子查询,子查询会导致生成一个临时表就相当于又查了一次数据,其余的情况可以利用mybaits的标签if test减少联表,但是这个也是需要十分熟悉业务,并且很多时候你不敢改造这些用了多年的sql。5、缓存:有些重复查询操作,咱们可以直接去缓存里面去拿,这样效率十分高效,公司是使用了redis,但是缓存也需要考虑很多地方,例如缓存击穿,缓存和数据库存放的数据不一致,这是非常致命的问题,一般缓存只能用来一些影响较小但是查询比较多的数据,例如商城的购物车咱们就可以利用缓存,也可以用es做缓存,这种一般都是针对一些列表数据的全文搜索,但这个类似于加冗余字段,各种操作生成的数据都需要同步到es。6、异步:跟上文的java类似,也是利用消息队列进行异步操作。总结目前想到的方案就这些,写的比较乱,这里是记载我目前想法,暂时就没有写实例来体现了,大家有更好的方法可以在评论区指出,我会去科普知识补充上去。————————————————                        原文链接:https://blog.csdn.net/qq_42706375/article/details/127484115
  • [技术干货] JAVA代码性能优化总结大全
    代码优化的目标1.减少代码的体积2.提高代码的运行效率一.代码层面 1. for循环中不要利用 + 号去拼接字符串 在循环次数比较多的for循环中,我们也不要利用 + 号去拼接字符串。具体例子如下:  for(int i=0;i<1000;++){  String str+=i;  }  *.三者在执行速度方面的比较:StringBuilder > StringBuffer > String  对于三者使用的总结:   1.如果要操作少量的数据用 = String2.单线程操作字符串缓冲区 下操作大量数=StringBuilder3.多线程操作字符串缓冲区 下操作大量数据 = StringBufferfor循环建议写法,尽量减少对变量的重复计算,明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:for (int i = 0, int length = list.size(); i < length; i++)这样,在list.size()很大的时候,就减少了很多的消耗。2.System.arraycopy > clone > Arrays.copyOf > for综上所述,当复制大量数据时,使用System.arraycopy()命令。3.切勿把异常放置在循环体内try-catch语句本身性能不高,如果再放到循环体中,无非是雪上加霜。因此在开发中,我们要极力避免。eg.for(int i=0;i<10;i++){try{}catch{}}正确做法try{for(int i=0;i<10;i++){ } }catch{12}尽量指定类、方法的final修饰符带有final修饰符的类是不可派生的,为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。此举能够使性能平均提高50%。5.尽可能使用局部变量调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。6.及时关闭流Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。7.懒加载的策略尽量采用懒加载的策略,即在需要的时候才创建例如:String str = “aaa”;if (i == 1){  list.add(str);}建议替换为:if (i == 1){  String str = “aaa”;  list.add(str);}循环内不要不断创建对象引用例如:for (int i = 1; i <= count; i++){Object obj = new Object();}这种做法会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了,建议为改为:Object obj = null;for (int i = 0; i <= count; i++){obj = new Object();}这样的话,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。9.性能开销尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销。10.尽量在合适的场合使用单例使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:控制资源的使用,通过线程同步来控制资源的并发访问控制实例的产生,以达到节约资源的目的控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信11.尽量避免随意使用静态变量要知道,当某个对象被定义为static的变量所引用,那么gc通常是不会回收这个对象所占有的堆内存的,如:public class A{private static B b = new B();}此时静态变量b的生命周期与A类相同,如果A类不被卸载,那么引用B指向的B对象会常驻内存,直到程序终止12.及时清除不再需要的会话为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多的会话时,如果内存不足,那么操作系统会把部分数据转移到磁盘,应用服务器也可能根据MRU(最近最频繁使用)算法把部分不活跃的会话转储到磁盘,甚至可能抛出内存不足的异常。如果会话要被转储到磁盘,那么必须要先被序列化,在大规模集群中,对对象进行序列化的代价是很昂贵的。因此,当会话不再需要时,应当及时调用HttpSession的invalidate()方法清除会话。13.使用同步代码块替代同步方法这点在多线程模块中的synchronized锁方法块一文中已经讲得很清楚了,除非能确定一整个方法都是需要进行同步的,否则尽量使用同步代码块,避免对那些不需要进行同步的代码也进行了同步,影响了代码执行效率。14.将常量声明为static final,并以大写命名这样在编译期间就可以把这些内容放入常量池中,避免运行期间计算生成常量的值。另外,将常量的名字以大写命名也可以方便区分出常量与变量15.不要创建一些不使用的对象,不要导入一些不使用的类这毫无意义,如果代码中出现”The value of the local variable i is not used”、”The import java.util is never used”,那么请删除这些无用的内容程序运行过程中避免使用反射关于,请参见反射。反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是Method的invoke方法,如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存—-用户只关心和对端交互的时候获取最快的响应速度,并不关心对端的项目启动花多久时间。17.使用数据库连接池和线程池这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程。18.使用带缓冲的输入输出流进行IO操作带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率。19.不要让public方法中有太多的形参public方法即对外提供的方法,如果给这些方法太多形参的话主要有两点坏处:违反了面向对象的编程思想,Java讲求一切都是对象,太多的形参,和面向对象的编程思想并不契合参数太多势必导致方法调用的出错概率增加至于这个”太多”指的是多少个,3、4个吧。比如我们用JDBC写一个insertStudentInfo方法,有10个学生信息字段要插如Student表中,可以把这10个参数封装在一个实体类中,作为insert方法的形参20.字符串变量和字符串常量equals的时候将字符串常量写在前面String str = “123”;if (“123”.equals(str)){…}这么做主要是可以避免空指针异常21.转换最快的方式把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、**String.valueOf(数据)次之、数据+”“最慢。所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断Integer.toString()方法就不说了,直接调用了i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串原文链接:https://blog.csdn.net/xss_lala/article/details/79605537
  • [技术干货] Java 代码性能优化
    1.尽量避免过多过常地创建Java对象尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组来替代对象。2. 尽量使用基本数据类型代替对象 String str = "hello";上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;String str = new String("hello");此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o3.尽量合理的创建HashMap当你要创建一个比较大的hashMap时,充分利用这个构造函数public HashMap(int initialCapacity, float loadFactor);避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。4.尽量减少对变量的重复计算如:for(int i=0;i<list.size();i++)应该改为:for(int i=0,len=list.size();i<len;i++)并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。5.尽量早释放无用对象的引用大部分时,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。例如:Public void test(){ Object obj = new Object(); …… Obj=null; }上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:Public void test(){ Object obj = new Object(); …… Obj=null; //执行耗时,耗内存操作;或调用耗时,耗内存的方法 …… }这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。6. 尽量缓存经常使用的对象尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache进行缓存,他们基本都实现了FIFO/FLU等缓存算法。7. 尽量重用对象特别是String对象的使用中,出现字符串连接情况时应使用StringBuffer代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大的影响。8. 过分的创建对象会消耗系统的大量内存,严重时,会导致内存泄漏,因此,保证过期的对象的及时回收具有重要意义。JVM的GC并非十分智能,因此建议在对象使用完毕后,手动设置成null。9. 不要在循环中使用Try/Catch语句,应把Try/Catch放在循环最外层Error是获取系统错误的类,或者说是虚拟机错误的类。不是所有的错误Exception都能获取到的,虚拟机报错Exception就获取不到,必须用Error获取。10. HaspMap的遍历:Map<String, String[]> paraMap = new HashMap<String, String[]>(); for( Entry<String, String[]> entry : paraMap.entrySet() ) { String appFieldDefId = entry.getKey(); String[] values = entry.getValue(); }利用散列值取出相应的Entry做比较得到结果,取得entry的值之后直接取key和value。————————————————                        原文链接:https://blog.csdn.net/qq_27346503/article/details/110144811
  • [技术干货] 35 个 Java 代码性能优化总结
    代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。  代码优化的目标是:  1、减小代码的体积  2、提高代码运行的效率代码优化细节   1、尽量指定类、方法的final修饰符  带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。此举能够使性能平均提高50%。   2、尽量重用对象  特别是String对象的使用,出现字符串连接时应该使用StringBuilder/StringBuffer代替。由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。   3、尽可能使用局部变量  调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。   4、及时关闭流  Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。   5、尽量减少对变量的重复计算  明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:for (int i = 0; i < list.size(); i++){...}  建议替换为:for (int i = 0, int length = list.size(); i < length; i++){...}  这样,在list.size()很大的时候,就减少了很多的消耗   6、尽量采用懒加载的策略,即在需要的时候才创建  例如:String str = "aaa";if (i == 1){list.add(str);}  建议替换为:if (i == 1){String str = "aaa";list.add(str);}   7、慎用异常  异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。  8、不要在循环中使用try…catch…,应该把其放在最外层  除非不得已。如果毫无理由地这么写了,只要你的领导资深一点、有强迫症一点,八成就要骂你为什么写出这种垃圾代码来了  9、如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度  比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder为例:  (1)StringBuilder()      // 默认分配16个字符的空间  (2)StringBuilder(int size)  // 默认分配size个字符的空间  (3)StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间  可以通过类(这里指的不仅仅是上面的StringBuilder)的来设定它的初始化容量,这样可以明显地提升性能。比如StringBuilder吧,length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中—-这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:  (1)在4096 的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间  (2)把原来的4096个字符拷贝到新的的字符数组中去  这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成new HashMap(128)、new HashMap(256)都可以。   10、当复制大量数据时,使用System.arraycopy()命令   11、乘法和除法使用移位操作  例如:for (val = 0; val < 100000; val += 5){a = val * 8;b = val / 2;}  用移位操作可以极大地提高性能,因为在计算机底层,对位的操作是最方便、最快的,因此建议修改为:for (val = 0; val < 100000; val += 5){a = val << 3;b = val >> 1;}  移位操作虽然快,但是可能会使代码不太好理解,因此最好加上相应的注释。   12、循环内不要不断创建对象引用  例如:for (int i = 1; i <= count; i++){Object obj = new Object();}  这种做法会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了,建议为改为:Object obj = null;for (int i = 0; i <= count; i++) { obj = new Object(); }  这样的话,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。  13、基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用ArrayList  14、尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销  15、不要将数组声明为public static final  因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变  16、尽量在合适的场合使用单例  使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:  (1)控制资源的使用,通过线程同步来控制资源的并发访问  (2)控制实例的产生,以达到节约资源的目的  (3)控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信  17、尽量避免随意使用静态变量  要知道,当某个对象被定义为static的变量所引用,那么gc通常是不会回收这个对象所占有的堆内存的,如:public class A{ private static B b = new B();}  此时静态变量b的生命周期与A类相同,如果A类不被卸载,那么引用B指向的B对象会常驻内存,直到程序终止  18、及时清除不再需要的会话  为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多的会话时,如果内存不足,那么操作系统会把部分数据转移到磁盘,应用服务器也可能根据MRU(最近最频繁使用)算法把部分不活跃的会话转储到磁盘,甚至可能抛出内存不足的异常。如果会话要被转储到磁盘,那么必须要先被序列化,在大规模集群中,对对象进行序列化的代价是很昂贵的。因此,当会话不再需要时,应当及时调用HttpSession的invalidate()方法清除会话。  19、实现RandomAccess接口的集合比如ArrayList,应当使用最普通的for循环而不是foreach循环来遍历  这是JDK推荐给用户的。JDK API对于RandomAccess接口的解释是:实现RandomAccess接口用来表明其支持快速随机访问,此接口的主要目的是允许一般的算法更改其行为,从而将其应用到随机或连续访问列表时能提供良好的性能。实际经验表明,实现RandomAccess接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高。可以使用类似如下的代码作判断:if (list instanceof RandomAccess){ for (int i = 0; i < list.size(); i++){}}else{Iterator<?> iterator = list.iterable(); while (iterator.hasNext()){iterator.next()}}  foreach循环的底层实现原理就是迭代器Iterator,参见Java语法糖1:可变长度参数以及foreach循环原理。所以后半句”反过来,如果是顺序访问的,则使用Iterator会效率更高”的意思就是顺序访问的那些类实例,使用foreach循环去遍历。  20、使用同步代码块替代同步方法  这点在多线程模块中的synchronized锁方法块一文中已经讲得很清楚了,除非能确定一整个方法都是需要进行同步的,否则尽量使用同步代码块,避免对那些不需要进行同步的代码也进行了同步,影响了代码执行效率。  21、将常量声明为static final,并以大写命名  这样在编译期间就可以把这些内容放入常量池中,避免运行期间计算生成常量的值。另外,将常量的名字以大写命名也可以方便区分出常量与变量  22、不要创建一些不使用的对象,不要导入一些不使用的类  这毫无意义,如果代码中出现”The value of the local variable i is not used”、”The import java.util is never used”,那么请删除这些无用的内容  23、程序运行过程中避免使用反射  关于,请参见反射。反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是Method的invoke方法,如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存—-用户只关心和对端交互的时候获取最快的响应速度,并不关心对端的项目启动花多久时间。  24、使用数据库连接池和线程池  这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程  25、使用带缓冲的输入输出流进行IO操作  带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率  26、顺序插入和随机访问比较多的场景使用ArrayList,元素删除和中间插入比较多的场景使用LinkedList  这个,理解ArrayList和LinkedList的原理就知道了  27、不要让public方法中有太多的形参  public方法即对外提供的方法,如果给这些方法太多形参的话主要有两点坏处:  1、违反了面向对象的编程思想,Java讲求一切都是对象,太多的形参,和面向对象的编程思想并不契合  2、参数太多势必导致方法调用的出错概率增加  至于这个”太多”指的是多少个,3、4个吧。比如我们用JDBC写一个insertStudentInfo方法,有10个学生信息字段要插如Student表中,可以把这10个参数封装在一个实体类中,作为insert方法的形参  28、字符串变量和字符串常量equals的时候将字符串常量写在前面  这是一个比较常见的小技巧了,如果有以下代码:String str = "123";if (str.equals("123")) {...}  建议修改为:String str = "123";if ("123".equals(str)){...}  这么做主要是可以避免空指针异常  29、请知道,在java中if (i == 1)和if (1 == i)是没有区别的,但从阅读习惯上讲,建议使用前者  平时有人问,”if (i == 1)”和”if (1== i)”有没有区别,这就要从C/C++讲起。  在C/C++中,”if (i == 1)”判断条件成立,是以0与非0为基准的,0表示false,非0表示true,如果有这么一段代码:int i = 2;if (i == 1){...}else{...}  C/C++判断”i==1″不成立,所以以0表示,即false。但是如果:int i = 2;if (i = 1) { ... }else{ ... }  万一程序员一个不小心,把”if (i == 1)”写成”if (i = 1)”,这样就有问题了。在if之内将i赋值为1,if判断里面的内容非0,返回的就是true了,但是明明i为2,比较的值是1,应该返回的false。这种情况在C/C++的开发中是很可能发生的并且会导致一些难以理解的错误产生,所以,为了避免开发者在if语句中不正确的赋值操作,建议将if语句写为:int i = 2;if (1 == i) { ... }else{ ... }  这样,即使开发者不小心写成了”1 = i”,C/C++编译器也可以第一时间检查出来,因为我们可以对一个变量赋值i为1,但是不能对一个常量赋值1为i。  但是,在Java中,C/C++这种”if (i = 1)”的语法是不可能出现的,因为一旦写了这种语法,Java就会编译报错”Type mismatch: cannot convert from int to boolean”。但是,尽管Java的”if (i == 1)”和”if (1 == i)”在语义上没有任何区别,但是从阅读习惯上讲,建议使用前者会更好些。  30、不要对数组使用toString()方法  看一下对数组使用toString()打印出来的是什么:public static void main(String[] args){ int[] is = new int[]{1, 2, 3};System.out.println(is.toString());}  结果是:[I@18a992f  本意是想打印出数组内容,却有可能因为数组引用is为空而导致空指针异常。不过虽然对数组toString()没有意义,但是对集合toString()是可以打印出集合里面的内容的,因为集合的父类AbstractCollections<E>重写了Object的toString()方法。  31、不要对超出范围的基本数据类型做向下强制转型  这绝不会得到想要的结果:public static void main(String[] args){ long l = 12345678901234L;int i = (int)l;System.out.println(i);}  我们可能期望得到其中的某几位,但是结果却是:  1942892530  解释一下。Java中long是8个字节64位的,所以12345678901234在计算机中的表示应该是:  0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010  一个int型数据是4个字节32位的,从低位取出上面这串二进制数据的前32位是:  0111 0011 1100 1110 0010 1111 1111 0010  这串二进制表示为十进制1942892530,所以就是我们上面的控制台上输出的内容。从这个例子上还能顺便得到两个结论:  1、整型默认的数据类型是int,long l = 12345678901234L,这个数字已经超出了int的范围了,所以最后有一个L,表示这是一个long型数。顺便,浮点型的默认类型是double,所以定义float的时候要写成”"float f = 3.5f”  2、接下来再写一句”int ii = l + i;”会报错,因为long + int是一个long,不能赋值给int  32、公用的集合类中不使用的数据一定要及时remove掉  如果一个集合类是公用的(也就是说不是方法里面的属性),那么这个集合里面的元素是不会自动释放的,因为始终有引用指向它们。所以,如果公用集合里面的某些数据不使用而不去remove掉它们,那么将会造成这个公用集合不断增大,使得系统有内存泄露的隐患。  33、把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据+”"最慢  把一个基本数据类型转为一般有三种方式,我有一个Integer型数据i,可以使用i.toString()、String.valueOf(i)、i+”"三种方式,三种方式的效率如何,看一个测试:public static void main(String[] args){ int loopTime = 50000;Integer i = 0; long startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++){String str = String.valueOf(i);}System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms");startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++){String str = i.toString();}System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms");startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++){String str = i + "";}System.out.println("i + \"\":" + (System.currentTimeMillis() - startTime) + "ms");}  运行结果为:String.valueOf():11ms Integer.toString():5ms i + "":25ms  所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:  1、String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断  2、Integer.toString()方法就不说了,直接调用了  3、i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串  三者对比下来,明显是2最快、1次之、3最慢  34、使用最有效率的方式去遍历Map  遍历Map的方式有很多,通常场景下我们需要的是遍历Map中的Key和Value,那么推荐使用的、效率最高的方式是:public static void main(String[] args){HashMap<String, String> hm = new HashMap<String, String>();hm.put("111", "222");Set<Map.Entry<String, String>> entrySet = hm.entrySet();Iterator<Map.Entry<String, String>> iter = entrySet.iterator(); while (iter.hasNext()){Map.Entry<String, String> entry = iter.next();System.out.println(entry.getKey() + "\t" + entry.getValue());}}  如果你只是想遍历一下这个Map的key值,那用”Set<String> keySet = hm.keySet();”会比较合适一些  35、对资源的close()建议分开操作  意思是,比如我有这么一段代码:try{XXX.close();YYY.close();}catch (Exception e){...}  建议修改为:try{ XXX.close(); }catch (Exception e) { ... }try{ YYY.close(); }catch (Exception e) { ... }  虽然有些麻烦,却能避免资源泄露。我们想,如果没有修改过的代码,万一XXX.close()抛异常了,那么就进入了cath块中了,YYY.close()不会执行,YYY这块资源就不会回收了,一直占用着,这样的代码一多,是可能引起资源句柄泄露的。而改为下面的写法之后,就保证了无论如何XXX和YYY都会被close掉。 原文链接:https://blog.csdn.net/null_cy/article/details/50944627