一、JMH工具
在讲解之前,我们先熟悉一下JMH工具。
JMH 是 OpenJDK 团队开发的一款基准测试工具,一般用于代码的性能调优,精度甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。
下面只介绍我的使用方法,因为我有一个自己的测试项目,所以直接使用maven命令创建一个子项目,切换到工程的最上级目录下,执行如下命令:
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=bssp -DartifactId=bssp-jmh -Dversion=1.0
成功后会得到如下工程:
image.png其中的MyBenchmark类就是我们测试的类,将自己需要测试的代码写入到里面就可以了。
最后使用maven的install或package打包成jar包,得到一个benchmarks.jar,通过java -jar 启动就可以查看测试结果。
问题:使用过程中可能存在打包失败的问题,有可能是此工程与父工程的依赖有冲突导致的,如果发现此类问题,可以直接修改此工程的pom文件,去掉<parent></parent>及包含的内容即可。
二、锁消除
锁消除是发生在编译器级别的一种锁优化方式。
JVM使用JIT(及时编译器)去优化,基于逃逸分析,如果局部变量在运行过程中没有出现逃逸,则可以对其进行优化。
测试两个方法,一个加锁,一个没加锁,都是对i进行++操作,如下所示:
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@Fork(1)
@BenchmarkMode(Mode.AverageTime) // 求平均时间
@Warmup(iterations = 3) // 预热,防止首次执行造成不准确
@Measurement(iterations = 3) // 运行三次
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 输出的单位是纳秒
public class MyBenchmark {
static int i = 0;
@Benchmark // 要进行测试的方法
public void test1() throws Exception {
i++;
}
@Benchmark // 要进行测试的方法
public void test2() throws Exception {
Object lock = new Object();
synchronized (lock) {
i++;
}
}
}
打包后运行benchmarks.jar,看结果:
PS E:\workspace\bssp-cloud\bssp-jmh\target> java -jar .\benchmarks.jar
# VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 3 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: bssp.MyBenchmark.test1
# Run progress: 0.00% complete, ETA 00:00:12
# Fork: 1 of 1
# Warmup Iteration 1: 1.840 ns/op
# Warmup Iteration 2: 1.852 ns/op
# Warmup Iteration 3: 1.845 ns/op
Iteration 1: 1.842 ns/op
Iteration 2: 1.841 ns/op
Iteration 3: 1.847 ns/op
Result: 1.843 ±(99.9%) 0.052 ns/op [Average]
Statistics: (min, avg, max) = (1.841, 1.843, 1.847), stdev = 0.003
Confidence interval (99.9%): [1.792, 1.895]
# VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 3 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: bssp.MyBenchmark.test2
# Run progress: 50.00% complete, ETA 00:00:07
# Fork: 1 of 1
# Warmup Iteration 1: 1.846 ns/op
# Warmup Iteration 2: 1.855 ns/op
# Warmup Iteration 3: 1.857 ns/op
Iteration 1: 1.829 ns/op
Iteration 2: 1.801 ns/op
Iteration 3: 1.834 ns/op
Result: 1.822 ±(99.9%) 0.321 ns/op [Average]
Statistics: (min, avg, max) = (1.801, 1.822, 1.834), stdev = 0.018
Confidence interval (99.9%): [1.501, 2.142]
# Run complete. Total time: 00:00:15
Benchmark Mode Samples Score Score error Units
b.MyBenchmark.test1 avgt 3 1.843 0.052 ns/op
b.MyBenchmark.test2 avgt 3 1.822 0.321 ns/op
如上结果分别展示了详细两次方法测试过程,包括预热时间,每次执行时间,平均时间,和两个方法的平均时间汇总。两次差距并不大。
正常来说添加了synchronized的方法,效率要明显的低于另一个方法,然而结果则不然。
其实是因为jvm锁消除的优化机制存在,就像开篇说的,局部变量lock,并没有逃逸出其作用范围。每一个线程来调用该方法,都会在在其栈帧中创建一个局部变量,多个线程之间是没有影响的,所以jvm经过分析,认为可以将此处的锁消除。
我们可以通过 -XX:-EliminateLocks 参数,去关闭锁消除,然后看一下运行结果:
Benchmark Mode Samples Score Score error Units
b.MyBenchmark.test1 avgt 3 1.851 0.140 ns/op
b.MyBenchmark.test2 avgt 3 22.272 2.121 ns/op
没有使用锁消除的方法,消耗时间增加了十多倍,差距还是很明显的。
jvm通过锁消除机制,极大的提升了代码运行的效率。也反向证明,即使是偏向锁,轻量级锁,还是会造成很大的性能损耗。
三、锁粗化
jvm在编译时会做的优化,本质就是减少加锁以及锁释放的次数。
下面举例几个可能存在锁消除的例子:
1)StringBuffer
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("1");
stringBuffer.append("2");
stringBuffer.append("3");
}
append源码:
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
append方法是synchronized的,当我们重复多次调用一个Stringbuffer的append方法,例如循环等,jvm为了提高效率,就可能发生锁粗化。
2)循环
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
synchronized (Test.class){
// TODO
}
}
}
如上代码在循环内不断的持有锁,释放锁,所以可能发生锁粗化,真正执行时的代码可能会是如下这样:
public static void main(String[] args) {
synchronized (Test.class) {
for (int i = 0; i < 100; i++) {
// TODO
}
}
}
网友评论