作为Java后端开发者,我们创作的许多代码直接影响着用户的使用体验。如果后端代码性能不佳,用户在访问网站时就必须花费更多时间等待服务器响应。这可能引发用户投诉甚至用户流失问题。
性能优化是一个广泛而重要的话题。《Java程序性能优化》提到性能优化可分为五个层次:设计优化、代码优化、JVM优化、数据库优化、操作系统优化等。每个层次都涵盖许多方法论和最佳实践。本文无意进行全面详尽的概述,只是列举几个常用的Java代码优化方案,希望读者阅读后能实际应用到自己的代码中。
单例层面
在处理IO操作、数据库连接、配置文件解析加载等耗费大量系统资源的任务时,我们必须限制这些实例的创建,或者始终使用一个共享的实例,以节约系统资源。这种情况下就需要使用单例模式。
批量操作
若有100个请求,逐个执行显然效率较低。将这100个请求合并为一个请求进行批量操作,则能大幅提升效率。
特别是在数据库操作中,批量处理不仅比逐条执行效率更高,还能有效降低数据库连接数,提升应用的QPS上限。
Future模式处理
假设某项任务需花费一定时间执行,为避免无谓的等待,可先获取一个“提货单”——即Future,随后继续处理其他任务,直至“货物”抵达,即任务执行完成并获得结果。这时便可凭借“提货单”提取物品,即通过Future对象获取返回值。
伪代码
public class RealData implements Callable<String> {
protected String data;
public RealData(String data) {
this.data = data;
}
@Override
public String call() throws Exception {
// 通过sleep方法演示业务是缓慢的
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return data;
}
}
public class Application {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask =
new FutureTask<>(new RealData("name"));
ExecutorService executor =
Executors.newFixedThreadPool(1); // 使用线程池
// 执行FutureTask,相当于上例中的client.request("name")发送请求
executor.submit(futureTask);
// 这里可以用一个sleep代替对其他业务逻辑的处理
// 在处理这些业务逻辑的同时,RealData也在创建,充分利用等待时间
Thread.sleep(2000);
// 使用真实数据
// 如果call()没有执行完成,仍会等待
System.out.println("数据=" + futureTask.get());
}
}
线程池思路
合理运用线程池带来三大益处。首先,降低资源消耗:通过重复利用已创建的线程,降低线程的创建与销毁成本。其次,提高响应速度:任务到达时,无需等待线程创建即可立即执行。第三,提升线程可管理性:线程是珍贵资源,无节制地创建会消耗系统资源,降低系统稳定性;线程池能实现统一分配、优化和监控。
自 Java 5 开始,引入了并发编程新API,如Executor框架,内部采用线程池机制,位于java.util.concurrent包中。通过该框架控制线程的启动、执行和关闭,可简化并发编程操作。
伪代码
public class MultiThreadTest {
public static void main(String[] args) {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-%d").build();
ExecutorService executor = new ThreadPoolExecutor(2, 5, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("Hello, world!");
}
});
System.out.println(" ===> Main Thread! ");
}
}
NIO处理
JDK自1.4版本起引入了新的I/O编程类库,即NIO。NIO不仅带来了高效的Buffer和Channel,还引入了基于Selector的非阻塞I/O机制,可以将多个异步I/O操作集中到一个或少数几个线程中进行处理。使用NIO替代阻塞I/O能够提高程序的并发吞吐能力,降低系统开销。
针对每个请求,如果为其单独开启一个线程来处理逻辑,当客户端数据传输是间歇性的而非连续的时,相应线程会处于I/O等待状态,并频繁进行上下文切换。利用NIO引入的Selector机制,可以提升程序的并发效率,改善这种状况。
伪代码
public class NioTest {
static public void main( String args[] ) throws Exception {
FileInputStream fin = new FileInputStream("D:\\test.txt");
// 获取通道
FileChannel fc = fin.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到缓冲区
fc.read(buffer);
buffer.flip();
while (buffer.remaining()>0) {
byte b = buffer.get();
System.out.print(((char)b));
}
fin.close();
}
}
优化锁层面
在并发场景中,频繁使用锁是很常见的情况。然而,锁引发竞争,而竞争又会耗费大量资源。那么,在Java代码中,如何优化锁呢?我们可以考虑以下几个方面:
- 缩短锁持有时间
- 尝试使用同步代码块替代同步方法,从而减少锁的占用时间。
- 降低锁粒度
在并发环境中使用Map时,最好选用ConcurrentHashMap替代HashTable和HashMap(ConcurrentHashMap采用分段锁,锁的粒度更细)。
分离锁
普通锁(例如synchronized)可能导致读写互相阻塞,可以尝试将读操作和写操作分开。
锁粗化
有时我们希望将多次锁的请求合并成一个,以减少频繁加锁、同步和解锁所带来的性能损失。
锁消除
锁消除是指Java虚拟机在JIT编译时,经过运行上下文的扫描,去除那些不会产生共享资源竞争的锁。通过锁消除,可以减少无谓的锁请求时间。
数据传输压缩
在数据传输之前,压缩数据是一种优化方式,可以减少网络传输的数据量,提升传输速度。接收端可解压数据,还原传输内容。压缩后的数据能节省存储介质(如磁盘或内存)空间和网络带宽,从而降低成本。然而,压缩并非无成本之举。数据压缩需要大量CPU计算,并且根据压缩算法的不同,计算复杂度和压缩比都有显著差异。通常需要根据业务情景选择合适的压缩算法。
缓存计算结果
对于相同的用户请求,若每次都重复查询数据库、重复计算,将浪费大量时间和资源。将计算结果缓存至本地内存或使用分布式缓存,可节约宝贵的CPU计算资源,减少数据库重复查询或磁盘I/O。将原本需要磁头物理转动的操作转化为内存中的电子运动,提高响应速度。同时,快速释放线程也提升了应用的吞吐能力。