-
个示例中,我们使用一个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并发编程的强大功能,构建出高效、稳定、可扩展的并发应用程序。
-
一、线程库 在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 开发过程中,经常会遇到“找不到或无法加载主类”(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 中,处理日期和时间是开发中常见的任务之一,特别是在涉及到多个时区、日期格式、时间计算等需求时。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是一种用于处理集合数据的强大工具。它提供了一种函数式编程的方式来对数据进行操作和转换。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 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中的数据库性能优化:索引、查询和连接池管理大家好,我是微赚淘客系统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
-
前言最近在解决用户反馈的时候发现有些用户查询效率太慢或者直接就查询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
-
代码优化的目标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
-
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
-
代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。代码优化也是一样,如果项目着眼于尽快无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
-
在Java编程的世界里,追求代码的高效性就如同探索一片广袤无垠的未知领域。每一行代码的优化,都可能成为提升程序性能的关键。 本文将深入探讨几种有效的代码优化策略,旨在帮助开发者编写出更加高效的Java应用程序。一、选择合适的算法和数据结构算法的选择和优化是提升代码效率的首要步骤。一个简单的算法可能因为其时间复杂度较低而在处理大数据时显得力不从心。例如,快速排序通常比冒泡排序更高效,因为它的平均时间复杂度为O(n log n),而冒泡排序的时间复杂度为O(n^2)。通过优化算法的时间复杂度,我们可以显著提高程序处理数据的能力。在Java中,选择合适的数据结构同样至关重要。不同的数据结构适用于不同的场景,错误的选择可能会导致性能问题。例如,如果需要频繁地进行查找操作,哈希表是一个不错的选择;而对于有序数据的查找和修改操作,平衡树如红黑树则更为合适。使用正确的数据结构不仅能够提升算法的执行效率,还能节省内存消耗。二、充分利用缓存机制缓存重复使用的计算结果也是提升效率的一种方法。动态规划和记忆化递归是常用的优化技术。未经优化的递归算法可能时间复杂度非常高,例如O(2^n),但通过使用动态规划可以将时间复杂度降低到O(n)。 三、选择正确的类型的基本类型与包装类型的使用也是优化的一个方面。Java中的基本类型(如int、float等)直接存储在栈内存中,而包装类型(如Integer、Float等)则需要存储在堆内存中。因此,在使用循环时,使用基本类型可以显著提高代码的运行速度。然而,在某些情况下,递归算法可能比循环更高效,但这需要根据具体情况进行分析和测试。四、减少动态代码的使用动态代码生成在某些情况下可能会导致Java代码运行缓慢。这是因为动态生成的代码需要额外的编译时间,这可能会影响程序的整体性能。因此,在设计系统时,应尽量减少动态代码的使用,或者将其优化以减少对性能的影响。五、减少对象创建和垃圾回收频繁创建对象会导致垃圾回收器频繁工作,从而影响性能。尽量减少不必要的对象创建是提升性能的重要手段。尽量重用对象,使用池化技术(如连接池、线程池)可以显著提升性能。例如,使用StringBuilder代替字符串拼接,因为StringBuilder可以在内部维护一个可变的字符序列,而不需要每次拼接都创建新的字符串对象。通过这种方式,不仅减少了对象的创建次数,还提高了拼接字符串的效率。六、优化循环结构在循环中避免不必要的计算和方法调用是提升代码效率的另一个关键点。将循环中不变的计算提取到循环外部,可以减少每次迭代的计算量。例如:// 未优化的代码for (int i = 0; i < list.size(); i++) { //循环内逻辑} // 优化后的代码int size = list.size();for (int i = 0; i < size; i++) { //循环内逻辑}通过将list.size()提取到循环外,可以减少每次迭代的计算量,从而提高循环的执行效率。七、使用合适的异常处理机制异常处理是Java编程中不可避免的一部分,但过度使用异常处理会影响性能。尽量使用条件判断替代异常来处理错误情况,只有在必要时才抛出异常。例如:// 未优化的代码try { performOperation(); } catch (SpecificException e) { // exception} // 优化后的代码if (condition) { performOperation();} else { // exception }通过这种方式,可以减少异常处理的开销,从而提高代码的执行效率。八、合理使用并发和多线程并发和多线程是提升性能的重要手段。Java提供了丰富的并发工具类,如java.util.concurrent包中的类。使用这些工具类可以有效地管理线程资源,提高程序的并行度。例如,使用ExecutorService来管理线程池,可以避免手动创建和管理线程的开销。ExecutorService executor = Executors.newFixedThreadPool(50); executor.submit(() -> { // some task}); executor.shutdown();通过使用线程池,可以有效地复用线程资源,提高程序的执行效率。九、使用高效的I/O操作I/O操作通常是应用程序中的一个瓶颈点。优化I/O操作可以提高系统的整体性能。例如,使用缓冲流(BufferedInputStream、BufferedOutputStream)可以提高文件读写的效率。另外,尽量减少网络请求的次数和数据传输量也是提升性能的重要手段。BufferedReader reader = new BufferedReader(new FileReader("openfile.txt"));BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt")); String line; while ((line = reader.readLine()) != null) { writer.write(line); writer.newLine();} reader.close();writer.close();通过使用缓冲流,可以减少I/O操作的频率,提高文件读写的效率。总结优化Java代码是一项复杂且细致的工作,涉及多个方面。通过选择合适的算法和数据结构、减少对象创建和垃圾回收、优化循环结构和异常处理机制、合理使用并发和多线程以及优化I/O操作,可以显著提升Java程序的效率和性能。希望本文介绍的方法能够帮助开发者在实际应用中编写出更高效的Java代码———————————————— 原文链接:https://blog.csdn.net/m0_63998314/article/details/144847848
-
1. 前言在开发任何Java应用的时候,我们都会谈及optimization——优化的概念。作为一个开发者,我们应该尽可能地保证自己写的代码干净、没有缺陷,并且尽可能地考虑性能问题。为此,笔者总结了以下11个你肯定会用到的Java代码性能优化的技巧。编程学习资料点击免费领取2. 11个优化的技巧2.1 避免方法过长我们在定义一个方式的时候,应该考虑到一个方法不应该太长,它就应该是专门是来执行单一功能的。这样其实对维护和性能都有好处。一方面,从维护角度来说,适当长度的方法易读性更强,更容易理解;另一方面,在类的加载和方法调用的过程中,方法会被加载到内存中。如果一个方法太大,处理起来就需要消耗额外的内存和CPU周期。我们应该学会在恰当的逻辑点上将一个长方法拆开。2.2 避免多个if-else语句对于这个优化点,大家应该很熟悉了。但是实际在写代码的时候,还是if-else一撸到底。这样做的话,其实也会影响性能。因为JVM必须对条件进行比较。如果在for、while等循环语句中使用同样的条件,情况会变得更糟糕。如果我们的业务逻辑中有很多的条件,我们可以尝试着将这些条件分组并且返回一个布尔值,然后再将其用于if语句。另外,如果可能的话,我们可以考虑使用switch语句来代替多个if-else。switch语句比if-else有性能优势。 下面我们看一个例子:if (condition1) { if (condition2) { if (condition3 || condition4) { execute ..} else { execute..}复制代码对比上面这段代码,合适的做法应该如下:boolean result = (condition1 && condition2) && (condition3 || condition4)复制代码2.3 避免使用iterator用Java5的foreach风格来写循环确实很方便很简洁,看起来就很酷!但是有的时候耍酷是要付出性能的代价的。例如:for (String str: strs) { . . .}复制代码每次运行代码,如果strs是Iterable的,你将会创建一个新的Iterator对象。这样做会导致更多内存的消耗。如果你对性能有着极致的追求,那么还是建议你使用原始的写法:int size = strs.size();for (int i = 0; i < size; i++) { String value = strs.get(i); . . .}复制代码2.4 避免在集合中获取size在对任何集合进行迭代时,要事先得到集合的大小,而不是在迭代过程中得到它——这样避免多次调用size()方法。下面请看这个例子:List<String> eleList = getData();for (int i = 0; i < eleList.size(); i++) { execute code ..}复制代码对比上面这段代码,合适的做法应该如下:List<String> objList = getData();int size = objList.size();for (int i = 0; i < size; i++) { execute code ..} 复制代码2.5 避免使用+号拼接字符串从JDK5开始,Java编译器就做了优化,使用+号拼接字符串,编译器编译后实际就自动优化为使用StringBuilder。而且String是final类,用String创建的对象无法重复使用。因此,如果我们需要连续拼接,使用+号拼接字符串将导致创建多个String对象,从而会占用更多的堆内存。一般来说,当字符串不多的时候,+号与StringBuilder的拼接效率其实相差无几;但是如果涉及到单线程循环拼接的时候,我们最好还是使用StringBuilder以保证性能上的优化。下面请看一个例子:String str = "sample"; for (int i = 0; i < count; i++) { str = str + "-" + i; }复制代码更合适的做法如下:StringBuilder stringBuilder = new StringBuilder("sample"); for (int i = 0; i < count; i++) { stringBuilder.append("-"); stringBuilder.append(i); }复制代码2.6 尽可能使用基本类型因为基本类型存储在栈内存中,而对象存储在堆内存中。如果可以的话,我们应该尽可能使用基本类型而非对象,因为栈内存的访问速度比堆内存快。因此在某些情况下,定义一个变量或者数组,我们可以使用int而非Integer,double而非Double。2.7 避免使用BigDecimal类BigDecimal类提供了精确的小数值,过度使用这个对象会对性能造成影响,特别是当这个对象被用来在循环中计算某些数值时。BigDecimal在进行计算时要比long或double占用更多的内存。如果精度不受限制,或者我们确认计算值的范围不会超过long或double,我们可以避免使用BigDecimal,而使用long或double,并进行适当的转换。2.8 避免经常创建“代价昂贵”的对象有一些类在应用程序中承载着数据,这些对象的创建开销很大,我们应该避免多次创建。比如说,数据库连接对象,系统配置对象,或者是用户登录的会话对象。这些对象在创建的时候占用了大量资源,我们应该选择重用这些对象,而不是再次创建。对于这些"代价昂贵"的对象,我们尽可能使用单例模式来创建单一实例,并在需要的地方重用它。2.9 使用PreparedStatement而不是Statement现在应该比较少用JDBC API进行SQL查询了,但是我觉得还是有必要了解一下。对于参数化查询,PreparedStatement比Statement更有优势,因为PreparedStatement对象被编译一次并执行多次。Statement对象在每次被调用时都会被编译和执行。此外,PreparedStatement对象是安全的,可以避免SQL注入攻击。2.10 避免使用不必要的日志语句和不正确的日志级别这个建议应该是很普遍的,但是很多代码忽略了这一点。我们在创建调试信息的时候,应该先检查一下当前的日志级别。否则你可能会无意之间创建一条无用的日志信息。 请看例子:log.debug("User [" + userName + "] called method X with [" + i + "]");log.debug(String.format("User [%s] called method X with [%d]", userName, i));复制代码在这种情况下,我们需要执行所有必要的步骤去创建日志信息,而不知道程序到底会不会使用该日志信息。更进一步,如果说这种日志信息的创建涉及到更多资源的占用呢?所以最好是先检查一下当前的日志级别,请看:if(log.isDebugEnabled()) { log.debug("User [" + userName + " ] called method X with [" + i + "] ");}复制代码2.11 选择SQL查询中的必要字段有时,我们需要写SQL来获取数据。此时我们应该避免选择所有数据库列,只选择我们需要的数据库列。选择太多的列会导致数据库查询执行的延迟,也会增加网络流量。请看示例:select * from books where book_id = 6;复制代码对此,我建议这么写:select book_title, book_desc, book_price from books where book_id = 6;复制代码3. 结语很多人认为性能优化是一个复杂的话题,需要大量的经验和知识,这在一定程度上是对的。我们开发一个应用程序并且期望获得尽可能好的性能并不是一件容易的事情。但是,即使你不是性能调优专家,也可以采取一些简单的方法来提高性能。———————————————— 原文链接:https://blog.csdn.net/m0_63171455/article/details/122680235
-
Java 7 ForkJoinPool和 Java 8 的并行Stream有助于并行化东西,这在您将 Java 程序部署到多核处理器机器上时非常有用。与跨网络上的不同机器进行扩展相比,这种并行性的优势在于您几乎可以完全消除延迟效应,因为所有内核都可以访问相同的内存。但是不要被并行的效果所迷惑!记住以下两点:并行性会吞噬你的核心。这对于批处理非常有用,但对于异步服务器(例如 HTTP)来说则是一场噩梦。在过去的几十年里,我们使用单线程 servlet 模型是有充分理由的。因此,并行性仅在扩大规模时才有帮助。并行性对算法的Big O Notation没有影响。如果您的算法是O(n log n),并且您让该算法在c内核上运行,您仍然会有一个O(n log n / c)算法,因为c在您的算法复杂性中是一个微不足道的常数。您将节省挂钟时间,但不会降低复杂性!当然,提高性能的最佳方法是降低算法复杂度。杀手是实现O(1)或准O(1),当然,例如HashMap查找。但这并不总是可能的,更不用说容易了。如果你不能降低复杂性,如果你在真正重要的地方调整你的算法,如果你能找到正确的位置,你仍然可以获得很多性能。假设以下算法的可视化表示: 算法的整体复杂度是,或者如果我们要处理单个数量级。但是,在分析此代码时,您可能会发现一个有趣的场景:O(N3)O(N x O x P)在您的开发框中,左分支 ( N -> M -> Heavy operation) 是您可以在分析器中看到的唯一分支,因为 和 的值O在P您的开发示例数据中很小。然而,在生产中,正确的分支(N -> O -> P -> Easy operation或NOPE)确实造成了麻烦。您的运营团队可能已经使用AppDynamics或DynaTrace或一些类似软件解决了这个问题。如果没有生产数据,您可能会很快得出结论并优化“繁重操作”。你运送到生产环境,你的修复没有效果。除了以下事实之外,没有优化的黄金法则:设计良好的应用程序更容易优化过早的优化不会解决任何性能问题,反而会使您的应用程序设计得不那么好,从而使优化变得更加困难理论够了。让我们假设您找到了正确的分支是问题所在。很可能是一个非常简单的操作在生产中失败了,因为它被调用了很多次(如果N、O和P很大)。请在不可避免算法的叶节点出现问题的情况下阅读本文。这些优化不会帮助您扩展。他们将帮助您暂时节省客户的时间,将整体算法的困难改进推迟到以后!O(N3) 以下是 Java 中最简单的 10 个性能优化:1、使用StringBuilder这应该是几乎所有 Java 代码中的默认设置。尽量避免使用+操作符。当然,您可能会争辩说,它只是StringBuilder的语法糖,例如:String x = "a" + args.length + "b";翻译为:new java.lang.StringBuilder [16]dupldc <String "a"> [18]invokespecial java.lang.StringBuilder(java.lang.String) [20]aload_0 [args]arraylengthinvokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]ldc <String "b"> [27]invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]astore_1 [x]但是,如果稍后您需要使用可选部分修改您的字符串,会发生什么?String x = "a" + args.length + "b"; if (args.length == 1) x = x + args[0];您现在将有第二个StringBuilder,这只是不必要地消耗您的堆内存,给您的 GC 施加压力。改为这样写:StringBuilder x = new StringBuilder("a");x.append(args.length);x.append("b"); if (args.length == 1); x.append(args[0]);在上面的示例中,如果您使用显式StringBuilder实例,或者您依赖 Java 编译器为您创建隐式实例,这可能完全无关紧要。但请记住,我们在N.O.P.E.分支中。我们浪费在像 GC 或分配 aStringBuilder的默认容量这样愚蠢的事情上的每个 CPU 周期,我们都在浪费N x O x P时间。根据经验,始终使用 aStringBuilder而不是+运算符。如果可以的话,如果你的构建更复杂,请保留StringBuilder多个方法的引用。只有一个StringBuilder“遍历”你的整个 SQL AST(抽象语法树) 对于大声喊叫,如果您仍然有StringBuffer参考资料,请将它们替换为StringBuilder. 您几乎不需要同步正在创建的字符串。2、避免正则表达式正则表达式相对便宜和方便。但是,如果您在 N.O.P.E. 分支中,那么它们就是您能做的最糟糕的事情。如果您绝对必须在计算密集型代码部分使用正则表达式,至少缓存Pattern引用而不是一直重新编译它:static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");但是如果你的正则表达式真的很傻String[] parts = ipAddress.split("\.");那么你真的最好求助于普通char[]或基于索引的操作。例如,这个完全不可读的循环做同样的事情:int length = ipAddress.length();int offset = 0;int part = 0;for (int i = 0; i < length; i++) { if (i == length - 1 || ipAddress.charAt(i + 1) == '.') { parts[part] = ipAddress.substring(offset, i + 1); part++; offset = i + 2; }}这也说明了为什么你不应该做任何过早的优化。与split()版本相比,这是不可维护的。挑战:读者中聪明的人可能会发现更快的算法。外卖 正则表达式很有用,但它们是有代价的。如果您深陷于N.O.P.E.分支中,则必须不惜一切代价避免使用正则表达式。请注意各种使用正则表达式的 JDK 字符串方法,例如String.replaceAll(), 或String.split(). 请改用Apache Commons Lang之类的流行库来进行字符串操作。3、不要使用iterator()现在,此建议实际上不适用于一般用例,而仅适用于N.O.P.E.分支的深层。尽管如此,你应该考虑一下。编写 Java-5 风格的 foreach 循环很方便。您可以完全忘记循环内部,并编写:for (String value : strings) { // Do something useful here}但是,每次遇到此循环时,如果strings是一个Iterable,您将创建一个新Iterator实例。如果您使用的是ArrayList,这将ints在您的堆上分配一个 3 的对象:private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = modCount; // ...相反,您可以编写以下等效循环并仅“浪费”堆栈上的单个int值,这非常便宜:int size = strings.size();for (int i = 0; i < size; i++) { String value : strings.get(i); // Do something useful here}……或者,如果您的列表没有真正改变,您甚至可以对它的数组版本进行操作:for (String value : stringArray) { // Do something useful here}从可写性和可读性的角度来看,以及从 API 设计的角度来看,迭代器、Iterable 和 foreach 循环都非常有用。但是,它们会在每次迭代时在堆上创建一个小的新实例。如果你多次运行这个迭代,你要确保避免创建这个无用的实例,而是编写基于索引的迭代。4、不要调用那个方法有些方法简单昂贵。在我们的N.O.P.E.分支示例中,我们在叶子中没有这样的方法,但您可能有一个。让我们假设您的 JDBC 驱动程序需要经历令人难以置信的麻烦来计算ResultSet.wasNull(). 您自己开发的 SQL 框架代码可能如下所示:if (type == Integer.class) { result = (T) wasNull(rs, Integer.valueOf(rs.getInt(index)));}// And then...static final <T> T wasNull(ResultSet rs, T value)throws SQLException { return rs.wasNull() ? null : value;}ResultSet.wasNull() 现在,每次您int从结果集中获得一个时, 都会调用此逻辑。但getInt()合同上写着:返回:列值;如果值为 SQL NULL,则返回值为 0因此,对上述内容的一个简单但可能是巨大的改进将是:static final <T extends Number> T wasNull( ResultSet rs, T value)throws SQLException { return (value == null || (value.intValue() == 0 && rs.wasNull())) ? null : value;}所以,这很简单:要点 不要在算法“叶节点”中调用昂贵的方法,而是缓存调用,或者在方法合约允许的情况下避免调用。5、使用原语和堆栈上面的例子,它使用了很多泛型,因此被迫使用包装器类型byte, short, int, 和long– 至少在泛型在 Java 10 和项目 Valhalla 中专用之前。但是你的代码中可能没有这个约束,所以你应该采取一切措施来替换:// Goes to the heapInteger i = 817598;这样:// Stays on the stackint i = 817598;使用数组时情况会变得更糟:// Three heap objects!Integer[] i = { 1337, 424242 };这样:// One heap object.int[] i = { 1337, 424242 };当您深入到N.O.P.E.分支时,您应该非常小心使用包装器类型。很有可能你会给你的 GC 造成很大的压力,它必须一直在清理你的烂摊子。一个特别有用的优化可能是使用一些原始类型并创建它的大型一维数组,以及几个分隔符变量来指示您的编码对象在数组上的确切位置。trove4jint[]是一个优秀的原始集合库,它比你的平均水平要复杂一些,它与 LGPL 一起提供。例外 此规则有一个例外:和booleanbyte很少有足够的值完全被 JDK 缓存。你可以写:Boolean a1 = true; // ... syntax sugar for:Boolean a2 = Boolean.valueOf(true); Byte b1 = (byte) 123; // ... syntax sugar for:Byte b2 = Byte.valueOf((byte) 123);对于其他整数基本类型的低值也是如此,包括char, short, int, long。但仅当您自动装箱或调用TheType.valueOf()时,才不会在调用构造函数时!永远不要在包装类型上调用构造函数,除非你真的想要一个新实例这个事实也可以帮助你为你的同事写一个复杂的愚人节笑话 堆 外 当然,你可能还想尝试堆外库,尽管它们更多的是战略决策,而不是本地优化。6、避免递归像 Scala 这样的现代函数式编程语言鼓励使用递归,因为它们提供了将尾递归算法优化回迭代算法的方法。如果你的语言支持这样的优化,你可能没问题。但即便如此,算法的最轻微变化也可能会产生一个分支,阻止你的递归是尾递归的。希望编译器会检测到这一点!否则,您可能会浪费大量堆栈帧,而这些堆栈帧可能仅使用几个局部变量就可以实现。当你深入N.O.P.E.分支时,总是更喜欢迭代而不是递归7、使用 entrySet()当您想要遍历 aMap并且需要键和值时,您必须有充分的理由编写以下内容:for (K key : map.keySet()) { V value : map.get(key);}而不是以下内容:for (Entry<K, V> entry : map.entrySet()) { K key = entry.getKey(); V value = entry.getValue();}当你在N.O.P.E.分支时,无论如何你都应该警惕地图,因为大量的O(1)地图访问操作仍然是大量的操作。而且访问也不是免费的。但至少,如果您不能没有地图,请使用它entrySet()来迭代它们!无论如何,该Map.Entry实例都在那里,您只需要访问它。entrySet()在 map 迭代过程中同时需要键和值时 始终使用。8、使用 EnumSet 或 EnumMap在某些情况下,映射中可能的键的数量是预先知道的——例如在使用配置映射时。如果该数字相对较小,您应该真正考虑使用EnumSetor EnumMap,而不是常规HashSetor HashMap。这很容易通过查看来解释EnumMap.put():private transient Object[] vals; public V put(K key, V value) { // ... int index = key.ordinal(); vals[index] = maskNull(value); // ...}这个实现的本质是,我们有一个索引值数组,而不是一个哈希表。当插入一个新值时,为了查找映射条目,我们所要做的就是向enum查询它的常数序数,该常数序数是由Java编译器在每个enum类型上生成的。如果这是一个全局配置映射(即只有一个实例),增加的访问速度将帮助EnumMap大大超过HashMap,它可能使用更少的堆内存,但必须在每个键上运行hashCode()和equals()。Enum和EnumMap是非常亲密的朋友。当您使用类似于枚举的结构作为键时,请实际考虑将这些结构作为枚举,并在EnumMap中使用它们作为键。9、优化你的 hashCode() 和 equals() 方法如果不能使用EnumMap,至少优化hashCode()和equals()方法。一个好的hashCode()方法是必要的,因为它将防止进一步调用开销大得多的equals(),因为它将为每个实例集生成更多不同的散列桶。在每个类层次结构中,都可能有流行的和简单的对象。hashCode()最简单、最快的实现是这样的:// AbstractTable, a common Table base implementation: @Overridepublic int hashCode() { // [#1938] This is a much more efficient hashCode() // implementation compared to that of standard // QueryParts return name.hashCode();}name表名在哪里。我们甚至不考虑表的模式或任何其他属性,因为表名通常在数据库中足够不同。此外,它name是一个字符串,所以它里面已经有一个缓存hashCode()值。注释很重要,因为AbstractTableextends是任何AST(抽象语法树)元素AbstractQueryPart的通用基础实现。通用 AST 元素没有任何属性,因此它不能对优化实现做出任何假设。因此,被覆盖的方法如下所示:hashCode()// AbstractQueryPart, a common AST element// base implementation: @Overridepublic int hashCode() { // This is a working default implementation. // It should be overridden by concrete subclasses, // to improve performance return create().renderInlined(this).hashCode();}换句话说,必须触发整个 SQL 渲染工作流来计算一个普通 AST 元素的哈希码。事情变得更有趣equals()// AbstractTable, a common Table base implementation: @Overridepublic boolean equals(Object that) { if (this == that) { return true; } // [#2144] Non-equality can be decided early, // without executing the rather expensive // implementation of AbstractQueryPart.equals() if (that instanceof AbstractTable) { if (StringUtils.equals(name, (((AbstractTable<?>) that).name))) { return super.equals(that); } return false; } return false;}第一件事:总是(不仅在NOPE 分支中)提前中止每个equals()方法,如果:- this == argument- this "incompatible type" argument- 请注意,后一个条件包括argument == null, 如果您instanceof用于检查兼容类型。我们之前在10 Subtle Best Practices when Coding Java中对此进行了博文。现在,在明显情况下尽早中止比较之后,您可能还希望在可以做出部分决定时尽早中止比较。例如,约定Table.equals()是两个表被认为是相等的,它们必须具有相同的名称,而不管具体的实现类型如何。例如,这两项不可能相等:- com.example.generated.Tables.MY_TABLE- DSL.tableByName("MY_OTHER_TABLE")如果argument 不能等于this,并且我们可以轻松地检查它,那么让我们这样做并在检查失败时中止。如果检查成功,我们仍然可以从super. 鉴于宇宙中的大多数对象都不相等,我们将通过快捷方式节省大量 CPU 时间。10、在集合中思考,而不是在单个元素最后但并非最不重要的一点是,有一件事与 Java 无关,但适用于任何语言。此外,我们将离开N.O.P.E.分支,因为此建议可能只会帮助您从 迁移到,或类似的东西。不幸的是,许多程序员从简单的本地算法的角度来思考。他们正在逐步解决问题,一个分支一个分支,一个循环一个循环,一个方法一个方法。这就是命令式和/或函数式编程风格。虽然在从纯命令式到面向对象(仍然是命令式)再到函数式编程时,对“更大的图景”进行建模变得越来越容易,但所有这些风格都缺乏只有 SQL 和 R 以及类似语言才有的东西:声明式编程。在 SQL 中(我们喜欢它,因为这是O(N3)O(n log n)) 你可以声明你想从你的数据库中得到的结果,而不会对算法产生任何影响。然后,数据库可以考虑所有可用的元数据(例如约束、键、索引等),以找出可能的最佳算法。从理论上讲,这从一开始就是SQL 和关系演算背后的主要思想。使用集合的主要优点是您的算法将变得更加简洁。而不是:// Pre-Java 8Set result = new HashSet();for (Object candidate : someSet)if (someOtherSet.contains(candidate))result.add(candidate);// Even Java 8 doesn’t really helpsomeSet.stream().filter(someOtherSet::contains).collect(Collectors.toSet()); 有些人可能会争辩说,函数式编程和 Java 8 将帮助您编写更简单、更简洁的算法。这不一定是真的。您可以将命令式 Java-7 循环转换为功能性 Java-8 Stream 集合,但您仍在编写相同的算法。编写类似 SQL 的表达式是不同的。SomeSet 相交 SomeOtherSet可以通过实现引擎以 1000 种方式实现。EnumSet正如我们今天所了解的,在运行操作之前将这两个集合自动转换为明智的做法INTERSECT。也许我们可以在INTERSECT不进行低级调用的情况下将其并行化Stream.parallel()11、结论在本文中,我们讨论了在N.O.P.E.分支上进行的优化,即在高复杂度算法的深处。 - 每个查询仅在单个上生成StringBuilder- 我们的模板引擎实际上是解析字符,而不是使用正则表达式- 我们尽可能使用数组,尤其是在迭代侦听器时- 我们远离我们不必调用的 JDBC 方法- 等等…———————————————— 原文链接:https://blog.csdn.net/weixin_42261238/article/details/137353891
-
性能优化是每个开发者和架构师都必须关注的重要领域,尤其是在企业级应用和高并发系统中,性能的好坏直接影响到系统的稳定性、用户体验以及资源消耗。Java 是一种高效、稳定的编程语言,但要确保应用程序在高负载环境下保持优秀的性能,仍然需要对代码和系统进行针对性的优化。本文将深入探讨一些常见的 Java 性能优化技巧,并通过示例代码帮助开发者提升应用程序的执行效率,降低资源消耗。目录性能优化的基本原则内存管理与优化CPU 优化技巧I/O 性能优化多线程与并发优化JVM 性能调优数据库性能优化总结1. 性能优化的基本原则性能优化通常需要平衡多个方面:时间、空间、可维护性和开发成本。以下是一些优化的基本原则:避免过早优化:在代码的初期阶段,应该专注于功能实现,避免过早进行性能优化。只有在性能瓶颈出现时,才应考虑进行优化。局部优化与全局优化:优化时要注意区分局部优化与全局优化,局部优化只会影响某一部分功能,而全局优化会影响系统的整体性能。定期性能分析与监控:通过工具进行性能分析,找到性能瓶颈所在,而不是依靠猜测。2. 内存管理与优化内存管理是 Java 性能优化中不可忽视的一部分。Java 通过垃圾回收机制来自动管理内存,但这并不意味着我们完全不需要关心内存管理。以下是一些优化技巧:2.1 避免频繁的垃圾回收尽量减少对象的创建:频繁的对象创建和销毁会导致垃圾回收的频繁触发,从而影响性能。尽量使用对象池来复用对象。// 使用对象池复用对象public class ObjectPool { private List<SomeObject> pool = new ArrayList<>(); public SomeObject getObject() { if (pool.isEmpty()) { return new SomeObject(); } return pool.remove(pool.size() - 1); } public void releaseObject(SomeObject object) { pool.add(object); }}避免使用过多的临时对象:特别是在循环中,避免每次迭代都创建临时对象。// 不推荐的做法for (int i = 0; i < 100000; i++) { String temp = "Item " + i; // 每次都创建新的对象}// 优化后的做法StringBuilder builder = new StringBuilder();for (int i = 0; i < 100000; i++) { builder.append("Item ").append(i); // 使用 StringBuilder 避免频繁创建新对象}2.2 内存泄漏检测内存泄漏会导致应用程序的内存占用不断增加,最终可能导致系统崩溃。常用的内存泄漏检查工具有:VisualVM:用于监控 JVM 的内存使用情况,并且可以生成堆转储文件来查看对象的引用情况。Eclipse MAT(Memory Analyzer Tool):帮助分析堆转储文件,找出潜在的内存泄漏问题。3. CPU 优化技巧CPU 的使用效率直接影响到应用程序的响应速度和吞吐量。以下是一些常见的 CPU 优化技巧:3.1 避免过多的线程上下文切换减少线程数量:线程创建和销毁会消耗 CPU 资源。避免线程的过度创建,使用线程池进行线程的管理和复用。// 使用线程池管理线程ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建固定大小的线程池for (int i = 0; i < 1000; i++) { executorService.submit(() -> { // 执行任务 });}3.2 优化算法和数据结构选择合适的算法:算法的时间复杂度直接影响程序的运行速度。对于计算密集型的操作,尽量选择效率更高的算法。选择合适的数据结构:不同的数据结构有不同的访问效率。例如,查询频繁的操作应选择 HashMap,而对于有序的数据集合,则优先选择 TreeMap 或 TreeSet。// 使用 HashMap 提高查找效率Map<String, String> map = new HashMap<>();map.put("key", "value");String result = map.get("key"); // 查找操作效率高4. I/O 性能优化I/O 操作是影响 Java 应用性能的另一个重要因素。优化 I/O 性能可以显著提升程序的响应速度。常见的优化方法包括:4.1 使用缓冲流缓冲输入输出流(BufferedReader、BufferedWriter 等)能显著提高 I/O 操作的效率,尤其是在大量读取或写入数据时。缓冲区的存在可以减少每次 I/O 操作时的磁盘访问。// 使用缓冲输入流try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); }}4.2 批量处理数据批量读取和写入:对于需要大量读取或写入数据的操作,尽量批量处理数据,减少 I/O 操作的次数。// 批量读取文件try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) { String[] lines = new String[1000]; int index = 0; while ((lines[index] = reader.readLine()) != null) { index++; if (index == 1000) { // 批量处理数据 processBatch(lines); index = 0; } } if (index > 0) { processBatch(lines); }}5. 多线程与并发优化Java 的多线程和并发编程是提升应用程序性能的关键。以下是一些常见的优化技巧:5.1 线程池的使用使用线程池:线程池能有效管理和复用线程,避免线程的频繁创建和销毁。通过配置合适的线程池大小,可以优化 CPU 和内存的使用。// 创建线程池ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建固定大小的线程池for (int i = 0; i < 1000; i++) { executorService.submit(() -> { // 执行任务 });}5.2 锁优化减少锁的粒度:锁的粒度越小,竞争就越少,性能也就越高。避免对整个方法或类进行加锁,而是只对真正需要加锁的部分加锁。// 锁粒度优化public class Counter { private int count = 0; public synchronized void increment() { count++; } // 优化后的锁粒度 public void incrementOptimized() { synchronized (this) { count++; } }}6. JVM 性能调优JVM 是 Java 应用程序运行的核心,优化 JVM 的配置可以有效提升程序的性能。6.1 调整堆内存大小堆内存的调整:可以通过调整 JVM 启动参数来设置堆内存的初始大小和最大大小,避免频繁的垃圾回收。# 设置初始堆内存和最大堆内存-Xms512m -Xmx1024m6.2 调整垃圾回收器选择合适的垃圾回收器:根据应用场景选择适合的垃圾回收器。例如,对于低延迟的应用,G1 垃圾回收器是一个不错的选择;对于高吞吐量的应用,Parallel GC 更适合。# 使用 G1 垃圾回收器-XX:+UseG1GC7. 数据库性能优化数据库是应用性能的瓶颈之一,优化数据库的操作可以大幅提高系统的响应速度。7.1 索引优化创建适当的索引:通过合理的索引设计,可以显著提升查询效率,避免全表扫描。-- 创建索引以提高查询性能CREATE INDEX idx_name ON table_name(column_name);7.2 批量操作批量插入:对于大量数据的插入操作,使用批量插入可以大大减少数据库的负载。// 批量插入数据String sql = "INSERT INTO table_name (column1, column2) VALUES (?, ?)";try (PreparedStatement stmt = connection.prepareStatement(sql)) { for (int i = 0; i < 1000; i++) { stmt.setString(1, "value1"); stmt.setString(2, "value2"); stmt.addBatch(); } stmt.executeBatch();}8. 总结Java 性能优化是一个持续的过程,需要根据具体的应用场景和性能瓶颈来选择合适的优化策略。通过合理的内存管理、算法优化、多线程处理、JVM 调优等方法,我们可以显著提升应用的性能和稳定性。性能优化是一个复杂而细致的过程,但通过不断的实践和学习,你可以使你的 Java 应用在面对高并发、大流量等场景时依然保持高效、稳定。————————————————原文链接:https://blog.csdn.net/mmc123125/article/details/143667289
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签