java并发编程(九)synchronized原理之锁消除和锁粗化

「这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战

一、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在编译时会做的优化,本质就是减少加锁以及锁释放的次数。

下面举例几个可能存在锁消除的例子:

  • 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为了提高效率,就可能发生锁粗化。

  • 循环

        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
                }
            }
        }
    复制代码

猜你喜欢

转载自juejin.im/post/7061770543220916261