// JMH throughput benchmark: about 32 operations per second
@Benchmark
public String measureStringApend() {
String targetString = "";
for (int i = 0; i < 10000; i++) {
targetString += "hello";
}
return targetString;
}
// JMH throughput benchmark: about 5,600 operations per second
@Benchmark
public String measureStringBufferApend() {
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 10000; i++) {
buffer.append(“hello”);
}
// JMH throughput benchmark: about 21,000 operations per second
@Benchmark
public String measureStringBuilderApend() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append(“hello”);
}
// JMH throughput benchmark: about 16,000 operations per second
@Benchmark
public String measureStringBuilderSynchronizedApend() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
synchronized (this) {
builder.append("hello");
}
}
return builder.toString();
}
```
这个基准测试结果显示,虽然基准测试并没有使用多个线程,但是使用了线程同步的代码比不使用线程同步的代码慢。线程同步,就是 StringBuffer 比 StringBuilder 慢的原因之一。
通过上面的基准测试,我们可以得出这样的结论:
频繁的对象创建、销毁,有损代码的效率;
减少内存分配、拷贝、释放的频率,可以提高代码的效率;
即使是单线程环境,使用线程同步依然有损代码的效率。
从上面的基准测试结果,是不是可以得出结论,我们应该使用 StringBuilder 来进行字符串操作呢?我们再来看几个基准测试的例子。
下面的例子,测试的是常量字符串的连接操作。从测试结果,我们可以看出,使用 String 的连接操作,要比使用 StringBuilder 的字符串连接快 5 万倍,这是一个让人惊讶的性能差异。
```
// JMH throughput benchmark: about 1,440,000,000 operations per second
@Benchmark
public void measureSimpleStringApend() {
for (int i = 0; i < 10000; i++) {
String targetString = "Hello, " + "world!";
}
}
// JMH throughput benchmark: about 26,000 operations per second
@Benchmark
public void measureSimpleStringBuilderApend() {
for (int i = 0; i < 10000; i++) {
StringBuilder builder = new StringBuilder();
builder.append(“hello, “);
builder.append(“world!”);
}
}
// JMH throughput benchmark: about 9,000 operations per second
@Benchmark
public void measureVariableStringApend() {
for (int i = 0; i < 10000; i++) {
String targetString = “Hello, " + getAppendix();
}
}
1
2
3
4
5
6
7
8
9
10
// JMH throughput benchmark: about 26,000 operations per second
@Benchmark
public void measureVariableStringBuilderApend() {
for (int i = 0; i < 10000; i++) {
StringBuilder builder = new StringBuilder();
builder.append("hello, ");
builder.append(getAppendix());
}
}
// JMH throughput benchmark: about 5,000 operations per second
@Benchmark
public void measureHashMap() throws IOException {
Map<HashedKey, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put(new HashedKey(i), “value”);
}
}
private static class HashedKey {
final int key;
HashedKey(int key) {
this.key = key;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof HashedKey) {
return key == ((HashedKey)obj).key;
}
// JMH throughput benchmark: about 9.5 operations per second
@Benchmark
public void measureCollidedHashMap() throws IOException {
Map<CollidedKey, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put(new CollidedKey(i), "value");
}
}
private static class CollidedKey {
final int key;
CollidedKey(int key) {
this.key = key;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof CollidedKey) {
return key == ((CollidedKey)obj).key;
}
return false;
}
@Override
public int hashCode() {
return key % 10;
}
}
```
小结
今天,我们主要讨论了一些容易被忽略的性能陷阱。比如,字符串怎么操作才是高效的;Java 常见的内存泄漏;资源关闭的正确方法以及集合的相关性能问题。
我们虽然使用了 Java 作为示例,但是像集合和字符串操作这样的性能问题,并不局限于特定的编程语言,你也可以看看你熟悉的编程语言有没有类似的问题。
一起来动手
这一次的练手题,我们来练习使用 JMH 工具,分析更多的性能问题。在“撞车的哈希值”这一小节,我们测试了 HashMap 的 put 方法,你能不能测试下其他方法以及其他基于哈希值的集合(HashSet,Hashtable)?我们测试的是 10,000 个对象,只有 10 个哈希值。如果 10,000 个对象,有 5,000 个哈希值,性能影响有多大?
下面的这段代码,你能够找到它的性能问题吗?
```
package com.example;
import java.util.Arrays;
import java.util.Random;
public class UserId {
private static final Random random = new Random();
private final byte[] userId = new byte[32];
public UserId() {
random.nextBytes(userId);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof UserId) {
return Arrays.equals(this.userId, ((UserId)obj).userId);
}
return false;
}
@Override
public int hashCode() {
int retVal = 0;
for (int i = 0; i < userId.length; i++) {
retVal += userId[i];
}
return retVal;
}
}
```
我们前面讨论了下面这段代码的性能问题,你能够使用 JMH 测试一个你的改进方案带来的效率提升吗?
```
import java.util.HashMap;
import java.util.Map;
class Solution {
/**
* Given an array of integers, return indices of the two numbers
* such that they add up to a specific target.
*/
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
}
```
另外,你也可以检查一下你手头的代码,看看有没有踩到类似的坑。如果遇到类似的陷阱,看一看能不能改进。
容易被忽略的性能陷阱,有很多种。这些大大小小的经验,需要我们日复一日的积累。如果你有这方面的经验,或者看到这方面的技术,请你分享在留言区,我们一起来学习、积累这些经验。
也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起交流一下。