“This is the 21st day of my participation in the First Challenge 2022. For details: First Challenge 2022”

First, JMH tools

Before we get into that, let’s familiarize ourselves with the JMH tool.

JMH is a benchmark developed by the OpenJDK team for performance tuning of code with nanosecond accuracy for Java and other JVM-based languages.

Since I have a test project of my own, I use the Maven command to create a subproject directly, switch to the highest level of the project directory, and execute the following command:

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=bssp -DartifactId=bssp-jmh -Dversion=1.0
Copy the code

After success, you will get the following projects:

MyBenchmark class is our test class, you need to test the code written into it.

Finally, it is packaged into a jar package using Maven’s Install or package, resulting in a benchmarks. Jar, which allows you to view the test results by launching it through java-jar.

Problem: There may be packaging failure in the process of using, which may be caused by the dependency conflict between this project and the parent project. If such problem is found, you can directly modify the POM file of this project by removing and the contents contained in it.

Two, lock elimination

Lock elimination is a type of lock optimization that occurs at the compiler level.

The JVM uses a JIT (just-in-time compiler) to optimize, based on escape analysis, local variables that can be optimized if they do not escape during runtime.

Test two methods, one is locked, one is not locked, both are ++ operation on I, as shown below:

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.TimeUnit;

@Fork(1)
@BenchmarkMode(Mode.AverageTime) // Find the average time
@Warmup(iterations = 3) // Preheat to prevent inaccuracy caused by the first execution
@Measurement(iterations = 3) // Run three times
@OutputTimeUnit(TimeUnit.NANOSECONDS) // Output is in nanoseconds
public class MyBenchmark {

    static int i = 0;

    @Benchmark // The method to test
    public void test1(a) throws Exception {
        i++;
    }

    @Benchmark // The method to test
    public void test2(a) throws Exception {
        Object lock = new Object();
        synchronized(lock) { i++; }}}Copy the code

Run benchmarks. Jar after packaging to see the result:

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 + / - 0.052 (99.9%)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\jdk18.0._181\jre\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations1,s each
# Measurement: 3 iterations1,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 Iteration1:1.846ns/op
# Warmup Iteration2:1.855ns/op
# Warmup Iteration3:1.857ns/op
Iteration1:1.829ns/op
Iteration2:1.801ns/op
Iteration3:1.834ns/op


Result: 1.822 + / - 0.321 (99.9%)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
Copy the code

The above results show the detailed process of two method tests, including the warm-up time, each execution time, the average time, and the summary of the average time of two methods. It’s not a big difference.

Normally, the synchronized method is significantly less efficient than the other method, but the results are not.

This is because the JVM optimization mechanism for lock elimination exists and, as mentioned above, local variable lock does not escape its scope. Each thread that calls this method creates a local variable in its stack frame. There is no effect between threads, so the JVM analyzes that it can remove the lock.

We can turn off EliminateLocks with the -xx: -eliminatelocks parameter and see the result:

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
Copy the code

Without the use of lock elimination method, the consumption time increased by more than ten times, the gap is still very obvious.

The JVM greatly improves the efficiency of code execution through lock elimination. It also turns out that even biased locking, lightweight locking, can cause a significant performance loss.

Three, lock coarsening

Lock coarsening is an optimization that the JVM does at compile time to reduce the number of times locks are locked and released.

Here are some examples of possible lock elimination:

  • StringBuffer

        public static void main(String[] args) {
            StringBuffer stringBuffer = new StringBuffer();
    
            stringBuffer.append("1");
            stringBuffer.append("2");
            stringBuffer.append("3");
        }
    Copy the code

    Append the source code:

        public synchronized StringBuffer append(String str) {
            toStringCache = null;
            super.append(str);
            return this;
        }
    Copy the code

    The append method is synchronized, and when we call the Append method of a Stringbuffer repeatedly, such as a loop, locking coarcing can occur in the JVM to improve efficiency.

  • cycle

        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                synchronized (Test.class){
                    // TODO }}}Copy the code

    Lock coarsening may occur as the code above holds and releases the lock over and over again in the loop. The actual code might look something like this:

        public static void main(String[] args) {
            synchronized (Test.class) {
                for (int i = 0; i < 100; i++) {
                    // TODO}}}Copy the code