复杂的字符串连接操作使用 StringBuilder
职业生涯早期,在做字符串连接操作的时候,肯定会这么写:String a=c+e+d,这个Java语法糖对于开发者来说太方便了。但是如果你在循环中使用“+”,那就得小心了。
我们都知道String 是不可变的,因此循环中对 string 的每一次赋值都会在堆内存中创建一个新的 String 对象。在一个循环体中,反复创建多个无用的对象,不仅会占用内存空间,还会影响GC时间。所以说,如果在循环中遇到字符串拼接,就使用 StringBuilder 而不是“+”。
使用 ThreadPoolExecutor 避免手动创建线程
许多初学者喜欢在编写代码时创建线程,这是一种危险的做法。
如果这个线程的创建需要处理大量的请求,很可能导致你的程序频繁的创建和销毁线程,频繁的切换线程上下文,浪费CPU资源,甚至会耗尽内存。
因此,建议使用ThreadPoolExecutor,并配置合适的核心线程数和最大线程数。
为集合预分配适当的容量
我们都知道 ArrayList,HashMap 和 ConcurrentHashMap 等集合类是可以自动扩容的,但是这种自动扩容涉及到底层数组的复制和迁移。如果扩容频繁,肯定会影响程序的性能。所以如果你能估计出大概的容量,请直接配置初始值。
使用枚举而不是常量类
很多人特别喜欢在项目中创建一个常量类,如下:
为什么不用枚举呢?Enum 有强制的类型验证。同时,使用枚举类的性能更高。并且使用 enum 还有更大的优势,它可以与策略模式一起使用来提高程序的可扩展性。例如:
如代码所示,你可以根据需要动态选择一种策略来下载文件,直接调用FileType.EXCEL.download(),无需关心代码细节。
使用 NIO 代替传统 IO
传统的 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来保证。很多初学者可能会这样写。
如果采用上述方式,只有一个线程可以进入同步代码块执行,其他线程只能挂起等待,即使这些线程可能写入不同的文件。我们可以通过降低锁粒度来提高性能。
不要随意使用静态变量
如果你熟悉JVM基础知识,那么就会知道如果一个对象被定义为静态变量,这个变量的引用就不容易被垃圾回收器回收。
静态变量“a”的生命周期与测试类相同。只要测试类型没有被卸载,“a”的引用对象就会驻留在内存中,直到程序终止。
使用基本数据类型
在应用程序中使用基本数据类型来减少内存消耗并提高程序性能。如果可以使用 int,请不要使用其 Integer 包装类型,使用double 而不是 Double。
基本数据类型的包装类实例存放在堆内存中,每次使用都会在堆内存中创建一个。如果使用基本数据类型,数据存放在栈帧中,栈的访问速度可比堆快很多。