字符串和数字操作
构造字符串
字符串在Java中是不可变的,无论构造,还是截取,得到的总是一个新字符串。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
原有的字符串的value数组直接通过引用赋值给新的字符串的value数组,也就是两个字符串共享一个char[],因此这种构造方法有着最快的构造速度。Java中的String对象被设计为不可变,是指一旦程序获得字符串对象引用,则不必担心这个字符串在别的地方被修改,因为修改总意味着获得一个新的字符串,不可变意味着线程安全,不必担心并发修改。
更多的时候是通过一个char[],或者在某些分布式框架反序列化对象时使用byte[]来构造字符串的,这种情况下性能会非常低。
通过byte[]构造字符串,是一种常见的情况,现在随着分布式和微服务的流行,字符串客户端序列化成byte[],并发送给服务器端。服务器端会有一个反序列化操作,通过byte构造字符串。
通过字节数组构造字符串,主要涉及转码过程,内部会调用StringCoding.decode进行转码。实际负责转码的是charset子类,比如sun.nio.cs.UTF_8的decode方法负责实现字节转码。
字符串拼接
JDK会自动将使用+号做的字符串拼接转化为StringBuilder。
同StringBuilder类似的还有StringBuffer,主要功能都继承自AbstractStringBuilder,提供了线程安全方法。几乎所有场景下的字符串拼接都不涉及线程同步,因此StringBuffer已经很少使用了。使用StringBuilder拼接其他类型,尤其是数字类型时,性能会明显下降,这是因为数字类型转字符串时,在JDK内部做了很多工作。
字符串格式化
可以通过String.format进行格式化,代码如下:
private String para = "gaoming";
private String formatString = "hello %s, nice to meet you";
private String value = String.format(formatString, para);
Benchmark Mode Samples Score Score error Units
c.e.m.j.b.TestStringFormat.stringFormat avgt 5 659.065 92.104 ns/op
c.e.m.j.b.TestStringFormat.stringMessage avgt 5 335.334 5.092 ns/op
MessageFormat类比Formatter性能略好,但两者实际上都会根据模板编译成中间格式,每次运行都会重复这个过程。
SLF4J影响性能的另外一个地方在于当输入参数是3个以上时,会使用可变数组,构造数组完毕后才会调用debug方法,性能也略有降低。
字符串查找
String提供了按照字符串搜索的方法:
- startWith(String str),判断字符串是否以指定的字符串开头。
- endsWith(String str),判断字符串是否以str结尾。
- indexOf(String str),返回第一次出现的指定字符串在此字符串中的索引。
- contains(CharSequence str),判断字符串是否包含str。
如果是查找单个字符,那么建议使用相应的接收char为参数的API,而不是使用String为参数的API,比如String.indexOf('t')性能就好于String.indexOf("t")。
private String str = "你好,java";
private String reg = ".*java.*";
private String key = "java";
private Pattern pattern = Pattern.compile(reg);
@Benchmark
public boolean search() {
return str.matches(reg);
}
@Benchmark
public boolean compileSearch() {
return pattern.matcher(str).matches();
}
@Benchmark
public boolean contain() {
return str.contains(key);
}
Benchmark Mode Samples Score Score error Units
c.e.m.j.b.TestStringSearch.compileSearch avgt 5 100.125 1.545 ns/op
c.e.m.j.b.TestStringSearch.contain avgt 5 5.083 0.501 ns/op
c.e.m.j.b.TestStringSearch.search avgt 5 288.623 22.210 ns/op
替换
String提供了两类方法实现字符串替换功能,一个是仅替换字符,有着最高的性能。另一类是通过正则表达式替换。
intern方法
虚拟机提供了字符串池,用于存放公共的字符串,可以调用String.intern方法,返回一个字符串池中同样内容的字符串。这种机制保证了虚拟机包含较少的字符串,不过这种调用是耗时的。
Benchmark Mode Samples Score Score error Units
c.e.m.j.b.TestIntern.replace avgt 5 64.774 7.413 ns/op
数字装箱
在Java中,将原始数字转换为对应的Number对象的机制叫做装箱。将Number对象转换为对应的原始类型的机制叫做拆箱。在Java中,装箱和拆箱是自动完成的。
int被装箱成Integer,在性能方面是要付出些许代价的,装箱的本质就是将原始类型包裹起来,并保存在堆里。因此装箱后的值需要更多的内存,其对象需要从堆中获取,进而再获得原始值,有一定的性能消耗。
JDK为了避免每次int类型装箱需要创建一个新的Integer对象,内部使用了缓存,保存了一定范围的int装箱对象。
装箱对性能的影响并不是很大,但创建过多的对象会加大垃圾回收的负担。
BigDecimal
通过字符串来构造BigDecimal,才能保证精度不丢失。
BigDecimal能保证精度,但计算会有一定的性能影响。
并发编程和异步编程
不安全的代码
在使用JDK提供的类的时候,通常会考虑对象是否是线程安全的,线程安全意味着可以在多线程下任意使用。
一个典型的线程不安全类是javax.text.SimpleDateFormat,其是用于格式化日期的工具类。
在单线程下使用SimpleDateFormat并没有任何问题,然而在多线程下使用SimpleDateFormat却得不到期望结果,其原因是SimpleDateFormat有一个类变量Calendar,在格式化日期时,Calendar变量会首先被设置为用户输入的date变量。
Queue
BlockingQueue类实现了缓冲,线程生成的数据放到BlockingQueue中,消费线程从BlockingQueue中获得数据。
BlockingQueue的核心方法如下:
- 放入数据:offer(Object)表示如果可能的话,将object加到BlockingQueue中,即如果BlockingQueue可以容纳,则返回true,否则返回false。
- 获取数据:poll(time)取走BlockingQueue中排在首位的对象,若不能立即取出,则可以等time参数规定的时间过后再获取,取不到时返回null。
常见的BlockingQueue实现类如下:
- ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部维护了一个定长的数组,以便缓存队列中的数据对象。因此ArrayBlockingQueue容纳的数据是有限的,如果队列满,则生产者线程无法再放入新的数据,线程阻塞。我们还可以控制ArrayBlockingQueue的内部锁是否采用公平锁,默认采用非公平锁。
- LinkedBlockingQueue:基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列,该队列由一个链表构成,链表既可以固定大小,也可以不限制容量。在不限制容量的情况下,如果生产者的速度大于消费者的速度,则LinkedBlockingQueue的容量会不断增加,系统内存就有可能被消耗殆尽。实际系统建议采用固定大小的ArrayListBlockingQueue。
- PriorityBlockingQueue:基于优先级的无大小限制的队列,通过构造函数传入的Comparator对象来决定优先级。BlockingQueue都是先进先出的,PriorityBlockingQueue可以设定元素的优先级,比如优先级高的请求放入PriorityBlockingQueue,会优先处理。
- DelayQueue:DelayQueue是一个没有限制容量的队列,只有当元素指定的延迟时间到了,才能够从队列中获取到该元素。
- SynchronousQueue:一种无缓冲的等待队列,线程调用take获取元素,必须等待另一个线程调用put设置SynchronousQueue队列的元素,队列没有任何容量,这是一种快速传递元素的方式。
代码性能优化
int转String
int转String是一个耗时操作,因此需要尽量避免发生这种不必要的转化操作。如果实在需要这种转化,那么也有一定的优化空间。一种简单的情况是可以预先将一批int值转化为字符串。
使用Native方法
一个Native Method就是一个调用非Java代码的接口。一个Native Method是这样一个Java的方法:该方法由非Java语言实现,比如C语言,一般来说,Native方法有着更好的性能。
最常用的Native方法有System.arraycopy方法,把原数组的内容复制到目标数组中。src是字符串数组,通过arraycopy把内容复制到dest数组中。
日期格式化
JDK提供了SimpleDateFormat,用于将日期类型格式化成字符串。由于SimpleDateFormat并非是线程安全的,因此不能作为类变量使用。
因此可以预先构造SimpleDateFormat,放到ThreadLocal中。
JDK8提供了线程安全的DateTimeFormatter,还可以这么做日期格式化。
switch优化
在条件判断中,如果有较多的分支判断,那么switch语句通常比if语句的效率更高,if语句会每次取出变量进行比较从而确定处理分支,而switch语句只需取出一次变量,然后根据tableswitch直接找到分支即可。
有时候变量的范围过大,则使用lookup switch取代。lookupswitch会逐个寻找分支,或者使用二分法查找分支。理论上性能相比于TABLESWITCH根据索引直接定位分支较慢。
switch实际上只支持int类型,JDK8支持String类型,是因为在编译的时候,使用hashCode来作为switch的实际值。
优先使用局部变量
当存取类变量的时候,Java使用虚拟机指令GETFIELD获取类变量,如果存取方法的变量,则通过出栈操作获取变量,GETFIELD从Heap中取值,速度较慢,而出栈操作有较快的速度。因此在需要频繁操作类变量的时候,最好先赋值一个局部变量。
预处理
预处理是指相对于需要反复调用的代码,可以尝试提取出公共的只读代码块,处理一次并保留处理结果。反复调用的时候,直接引用处理结果即可,这样能避免每次都处理公共内容。
预分配
JDK中存在大量的预分配空间的代码,比如StringBuilder,会初始分配一段空间,而不必在每次调用append时才分配。当调用append方法的时候,会先检测分配的空间是否足够,则不需要增加空间。
静态方法调用
在Java中,实例方法需要维护虚方法表以支持多态,相比于静态方法,调用实例方法会额外的查询虚方法表的开销。
Benchmark Mode Samples Score Score error Units
c.e.m.j.b.TestVirtualTable.instanceCall thrpt 5 2082658.966 109082.270 ops/ms
c.e.m.j.b.TestVirtualTable.staticCall thrpt 5 3153491.379 103719.883 ops/ms
ThreadLocalRandom
在JDK7之前使用java.util.Random生成伪随机数,Random的生成需要一个种子(seed),Random计算随机数时会根据种子进行计算,如果设置固定的种子,那么生成的随机数也是固定的,这就是伪随机数。
Random是线程安全的,Random实例里面有一个原子性的种子变量用来记录当前种子的值,当要生成新的随机数时,会根据当前种子计算新的种子并更新回原子变量。多线程下使用单个Random实例生成随机数时,多个线程同时计算随机数,计算新的种子时多个线程会竞争同一个原子变量的更新操作。由于原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量的线程进行自旋重试,这样会降低并发性能。
在JDK7提供了ThreadLocalRandom,ThreadLocalRandom在当前线程中维护了一个种子,适合在多线程场景下提供高性能伪随机数的生成。ThreadLocalRandom首先通过current方法获取当前线程的ThreadLocalRandom实例。
Benchmark Mode Samples Score Score error Units
c.e.m.j.b.TestRandom.localRandom thrpt 5 1176726.295 64556.987 ops/ms
c.e.m.j.b.TestRandom.random thrpt 5 7118.795 1472.836 ops/ms