Java 17迁移:偏置锁回归
#java #开发日志 #性能 #multithreading

我需要解决什么问题

我们最终将从Java 11转换为Java 17,但是一些性能测试失败了,失败了,大约有50%的回归。
在某些测试中,在Java 11中的Java 11与9秒为6秒。
这仅通过切换运行时就会发生。

所以我必须研究为什么测试失败并解决问题。

我为获取一些数据做了什么

首先,我决定使用剖面师进行测试。我正在使用Intellij IDEA Profiler和默认设置进行烟雾测试。
一旦我运行它,我在Java 11分析报告中缺少的火焰图上发现了奇怪的高原。

Plateaus

所以我决定仔细研究该方法。

深层发掘

该方法看起来像这样:

public class LazyInputStream extends InputStream{
    private InputStream is;
    ...

    protected synchronized InputStream getInstance() {
       if (is == null) {
           is = factory.create();
       }
       return is;
    }
    ...
    public int read() throws IOException {  
        return getInstance().read();  
    }
} 

该方法不会执行任何长时间操作。它仅在需要时创建输入流的实例,并以后使用。
但是我们正在调用外部输入流中每个read的方法。换句话说,每次我们要阅读下一个数据的下一部分。
该方法是synchronized保证在多线程环境中的输入流创建的单个实例。
Java 11分析中没有这种高原。我已经重新运行它,并在不同的机器上检查了几次。

谷歌搜索后,我发现了这个JEP,它说偏差锁是从Java 15禁用的。

什么是偏见锁

偏见锁定为java中的synchronized优化。
当该方法不那么同时,并且只有一个线程通常会获取锁。
JVM在监视器对象中升高了一个标志,该标志有些线程获取锁定,因此重新评价并通过同一线程释放锁定是轻量级的。但是,当另一个线程试图获取偏置锁定时,必须撤销锁。吊销是一个昂贵的操作。

因此,社区决定将偏差锁定并稍后删除。

因为如果您确实有多线程应用程序,则要避免昂贵的偏差锁定吊销,并且如果您的应用程序不是如此多线程,则根本不需要锁。

我如何解决

首先,让我们使用-XX:+UseBiasedLockingjvm选项进行测试以打开偏置锁。
回归问题消失了,但警告出现了:

OpenJDK 64-Bit Server VM warning: Option UseBiasedLocking was deprecated in version 15.0 and will likely be removed in a future release.

好吧,所以让我们更聪明地实现懒惰的初始化,以避免每次获取锁并使用旧时尚,但仍在工作double-checked locking
我发现它是由Suppliers.memoizeguava库中实现的。

public class LazyInputStream extends InputStream {  

    private final Supplier<InputStream> initializer;

    public LazyInputStream(InputStreamFactory factory) {  
        this.initializer = Suppliers.memoize(() -> {  
            try {  
                return factory.create();  
            } catch (IOException e) {  
                throw new IllegalStateException("Failed to create input stream", e);  
            }  
        });  
    }

    ...

    protected synchronized InputStream getInstance() {
       return initialized.get();
    }
    ...
    public int read() throws IOException {  
        return getInstance().read();  
    }

备忘录供应商使用了双重检查锁定的2个场变体,但这是因为供应商可以将null返回为有效值。

public T get() {  
  if (!initialized) {          // boolean flag  
    synchronized (this) {  
      if (!initialized) {      // double check to avoid races
        T t = delegate.get();  // calling delegate to get a value
        value = t;             // and remember it
        initialized = true;    // rise the boolean flag
        return t;  
      }  
    }  
  }  
  return value;  
}

我重新运行测试后,我发现回归并没有完全消失,因此看起来我们有类似的锁,但是它们在火焰图上不太热以至于无法看到。
最后,我们决定使用转弯偏置锁,并继续处理我们的代码以不断改进。

结论

  • ,如果您与我们相似并使用Java 15+,则只能通过打开偏置锁来获得一些性能提升。 但是要重写问题片总是更好的。
  • 在每个Java版本中测试您的代码性能。它将帮助您更早地找到回归,只花一点时间来研究奇怪的高原。

如果您发现该帖子有帮助,请支持我和:
Buy Me A Coffee