程序性能优化是一个复杂的话题。往往需要结合具体场景进行性能分析,找出瓶颈提出优化建议。但是,假设我们平时很少关注细节的性能,那么这种情况下,优化这些细节所带来的收益也是相当可观的。接下来,我们就来说说Java代码细节优化的一些小技巧。
职业生涯早期,在做字符串连接操作的时候,肯定会这么写:String a=c+e+d,这个Java语法糖对于开发者来说太方便了。但是如果你在循环中使用“+”,那就得小心了。
复制
String a=null;for(int i=0;i<1000;i++) { a=a+i; }
1.
2.
3.
4.
我们都知道String 是不可变的,因此循环中对 string 的每一次赋值都会在堆内存中创建一个新的 String 对象。在一个循环体中,反复创建多个无用的对象,不仅会占用内存空间,还会影响GC时间。所以说,如果在循环中遇到字符串拼接,就使用 StringBuilder 而不是“+”。
许多初学者喜欢在编写代码时创建线程,这是一种危险的做法。
如果这个线程的创建需要处理大量的请求,很可能导致你的程序频繁的创建和销毁线程,频繁的切换线程上下文,浪费CPU资源,甚至会耗尽内存。
因此,建议使用ThreadPoolExecutor,并配置合适的核心线程数和最大线程数。
我们都知道 ArrayList,HashMap 和 ConcurrentHashMap 等集合类是可以自动扩容的,但是这种自动扩容涉及到底层数组的复制和迁移。如果扩容频繁,肯定会影响程序的性能。所以如果你能估计出大概的容量,请直接配置初始值。
很多人特别喜欢在项目中创建一个常量类,如下:
复制
public class Constant {public static final String TOKEN_HEADER = "x-request-token";public static final Integer CODE_SUCCESS = 0;public static final Integer CODE_REQUEST_FAILED = 1; public static final Integer CODE_REQUEST_RUNNING = 2; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
为什么不用枚举呢?Enum 有强制的类型验证。同时,使用枚举类的性能更高。并且使用 enum 还有更大的优势,它可以与策略模式一起使用来提高程序的可扩展性。例如:
复制
public enum FileType {EXCEL(".xlsx"){@Overridepublic void download(String path) {//do download excel file logic} }, CSV(".csv") {@Overridepublic void download(String path) { //do download csv file logic} };private String suffix;FileType(String suffix) {this.suffix = suffix; }public String getSuffix() {return suffix; }public abstract void download(String path); }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
如代码所示,你可以根据需要动态选择一种策略来下载文件,直接调用FileType.EXCEL.download(),无需关心代码细节。
传统的 IO 已经过时了。强烈推荐使用 NIO 代替传统的 IO。因为传统IO采用阻塞IO模型,请求数据后,线程从数据准备到数据可读都是阻塞的。
而且,传统IO如果要往网卡写数据,需要先把数据写到堆内存,然后再把数据拷贝到堆外的一块内存,再从用户态拷贝数据到内核状态缓冲区。最后CPU通知DMA将数据写入网卡,一共经历了3次拷贝。NiO不仅采用了multiplex IO模型,还可以使用direct memory来减少数据拷贝次数,从而提高性能。
如果你看过一些JDK的源代码,比如HashMap,你会发现代码中有很多移位操作。因为JDK是比较底层的代码,对性能的追求也是极致的。在我们日常的编码中,可以用移位运算来代替一些乘除运算,比如a >> 1 代替 a / 2,a * 16 代替 a << 4。
这个技巧也能在一定程度上提高性能,但是如果你不擅长,那就不要强求,因为当代计算机的性能已经非常强大了,没必要为了一个程序而牺牲代码的可读性。
如果我们设计一个不需要考虑线程安全的类,请用单例模式来使用这个类,这样可以节省内存。幸运的是,对于我们使用的spring框架,Java bean默认是单例的。
假设我们有一个共享文档编辑功能,用户会同时编辑共享文档。为了保证文件的正确性,我们需要使用线程安全synchronized来保证。很多初学者可能会这样写。
复制
public class Test{private Object lock = new Object(); public void write(String username, String fileName) {synchronized(lock) {//do something} } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
如果采用上述方式,只有一个线程可以进入同步代码块执行,其他线程只能挂起等待,即使这些线程可能写入不同的文件。我们可以通过降低锁粒度来提高性能。
复制
public class Test{ public void write(String username, String fileName) {synchronized(fileName.intern()) {//do something} } }
1.
2.
3.
4.
5.
6.
7.
8.
如果你熟悉JVM基础知识,那么就会知道如果一个对象被定义为静态变量,这个变量的引用就不容易被垃圾回收器回收。
复制
public class Test{public static A a = new A(); }
1.
2.
3.
静态变量“a”的生命周期与测试类相同。只要测试类型没有被卸载,“a”的引用对象就会驻留在内存中,直到程序终止。
在应用程序中使用基本数据类型来减少内存消耗并提高程序性能。如果可以使用 int,请不要使用其 Integer 包装类型,使用double 而不是 Double。
基本数据类型的包装类实例存放在堆内存中,每次使用都会在堆内存中创建一个。如果使用基本数据类型,数据存放在栈帧中,栈的访问速度可比堆快很多。