美文网首页编程语言爱好者JavaJava 进阶
java并发编程(九)synchronized原理之锁消除和锁粗

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

作者: 我犟不过你 | 来源:发表于2021-11-30 14:38 被阅读0次

    一、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
                }
            }
        }
    

    相关文章

      网友评论

        本文标题:java并发编程(九)synchronized原理之锁消除和锁粗

        本文链接:https://www.haomeiwen.com/subject/aqzrxrtx.html