作为Java后端开发者,我们创作的许多代码直接影响着用户的使用体验。如果后端代码性能不佳,用户在访问网站时就必须花费更多时间等待服务器响应。这可能引发用户投诉甚至用户流失问题。
性能优化是一个广泛而重要的话题。《Java程序性能优化》提到性能优化可分为五个层次:设计优化、代码优化、JVM优化、数据库优化、操作系统优化等。每个层次都涵盖许多方法论和最佳实践。本文无意进行全面详尽的概述,只是列举几个常用的Java代码优化方案,希望读者阅读后能实际应用到自己的代码中。
在处理IO操作、数据库连接、配置文件解析加载等耗费大量系统资源的任务时,我们必须限制这些实例的创建,或者始终使用一个共享的实例,以节约系统资源。这种情况下就需要使用单例模式。
若有100个请求,逐个执行显然效率较低。将这100个请求合并为一个请求进行批量操作,则能大幅提升效率。
特别是在数据库操作中,批量处理不仅比逐条执行效率更高,还能有效降低数据库连接数,提升应用的QPS上限。
假设某项任务需花费一定时间执行,为避免无谓的等待,可先获取一个“提货单”——即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! "); }}
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。将原本需要磁头物理转动的操作转化为内存中的电子运动,提高响应速度。同时,快速释放线程也提升了应用的吞吐能力。
本文链接:http://www.28at.com/showinfo-26-81731-0.html一篇文章告诉你真实场景下服务端接口性能问题是如何解决的
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com